diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..b0f58f84f0 --- /dev/null +++ b/.github/copilot-instructions.md @@ -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. \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f2c373add..f0d8537035 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/.github/workflows/pr-merge.yaml b/.github/workflows/pr-merge.yaml index 5f216100c3..2bae051ea5 100644 --- a/.github/workflows/pr-merge.yaml +++ b/.github/workflows/pr-merge.yaml @@ -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 }} diff --git a/platformio.ini b/platformio.ini index 9bdf58d341..d3b22653ff 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index ee2b177143..d11d1f50b8 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -28,7 +28,6 @@ lib_deps = ${esp8266.lib_deps} ; robtillaart/SHT85@~0.3.3 ; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug ; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library -; bitbank2/PNGdec@^1.0.1 ;; used for POV display uncomment following ; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE build_unflags = ${common.build_unflags} diff --git a/usermods/pov_display/README.md b/usermods/pov_display/README.md new file mode 100644 index 0000000000..2b1659085f --- /dev/null +++ b/usermods/pov_display/README.md @@ -0,0 +1,48 @@ +## POV Display usermod + +This usermod adds a new effect called “POV Image”. + +![the usermod at work](pov_display.gif?raw=true) + +###How does it work? +With proper configuration (see below) the main segment will display a single row of pixels from an image stored on the ESP. +It displays the image row by row at a high refresh rate. +If you move the pixel segment at the right speed, you will see the full image floating in the air thanks to the persistence of vision. +RGB LEDs only (no RGBW), with grouping set to 1 and spacing set to 0. +Best results with high-density strips (e.g., 144 LEDs/m). + +To get it working: +- Resize your image. The height must match the number of LEDs in your strip/segment. +- Rotate your image 90° clockwise (height becomes width). +- Upload a BMP image (24-bit, uncompressed) to the ESP filesystem using the “/edit” URL. +- Select the “POV Image” effect. +- Set the segment name to the absolute filesystem path of the image (e.g., “/myimage.bmp”). +- The path is case-sensitive and must start with “/”. +- Rotate the pixel strip at approximately 20 RPM. +- Tune as needed so that one full revolution maps to the image width (if the image appears stretched or compressed, adjust RPM slightly). +- Enjoy the show! + +Notes: +- Only 24-bit uncompressed BMP files are supported. +- The image must fit into ~64 KB of RAM (width × height × 3 bytes, plus row padding to a 4-byte boundary). +- Examples (approximate, excluding row padding): + - 128×128 (49,152 bytes) fits. + - 160×160 (76,800 bytes) does NOT fit. + - 96×192 (55,296 bytes) fits; padding may add a small overhead. +- If the rendered image appears mirrored or upside‑down, rotate 90° the other way or flip horizontally in your editor and try again. +- The path must be absolute. + +### Requirements +- 1D rotating LED strip/segment (POV setup). Ensure the segment length equals the number of physical LEDs. +- BMP image saved as 24‑bit, uncompressed (no alpha, no palette). +- Sufficient free RAM (~64 KB) for the image buffer. + +### Troubleshooting +- Nothing displays: verify the file exists at the exact absolute path (case‑sensitive) and is a 24‑bit uncompressed BMP. +- Garbled colors or wrong orientation: re‑export as 24‑bit BMP and retry the rotation/flip guidance above. +- Image too large: reduce width and/or height until it fits within ~64 KB (see examples). +- Path issues: confirm you uploaded the file via the “/edit” URL and can see it in the filesystem browser. + +### Safety +- Secure the rotating assembly and keep clear of moving parts. +- Balance the strip/hub to minimize vibration before running at speed. \ No newline at end of file diff --git a/usermods/pov_display/bmpimage.cpp b/usermods/pov_display/bmpimage.cpp new file mode 100644 index 0000000000..2aea5c8d6e --- /dev/null +++ b/usermods/pov_display/bmpimage.cpp @@ -0,0 +1,146 @@ +#include "bmpimage.h" +#define BUF_SIZE 64000 + +byte * _buffer = nullptr; + +uint16_t read16(File &f) { + uint16_t result; + f.read((uint8_t *)&result,2); + return result; +} + +uint32_t read32(File &f) { + uint32_t result; + f.read((uint8_t *)&result,4); + return result; +} + +bool BMPimage::init(const char * fn) { + File bmpFile; + int bmpDepth; + //first, check if filename exists + if (!WLED_FS.exists(fn)) { + return false; + } + + bmpFile = WLED_FS.open(fn); + if (!bmpFile) { + _valid=false; + return false; + } + + //so, the file exists and is opened + // Parse BMP header + uint16_t header = read16(bmpFile); + if(header != 0x4D42) { // BMP signature + _valid=false; + bmpFile.close(); + return false; + } + + //read and ingnore file size + read32(bmpFile); + (void)read32(bmpFile); // Read & ignore creator bytes + _imageOffset = read32(bmpFile); // Start of image data + // Read DIB header + read32(bmpFile); + _width = read32(bmpFile); + _height = read32(bmpFile); + if(read16(bmpFile) != 1) { // # planes -- must be '1' + _valid=false; + bmpFile.close(); + return false; + } + bmpDepth = read16(bmpFile); // bits per pixel + if((bmpDepth != 24) || (read32(bmpFile) != 0)) { // 0 = uncompressed { + _width=0; + _valid=false; + bmpFile.close(); + return false; + } + // If _height is negative, image is in top-down order. + // This is not canon but has been observed in the wild. + if(_height < 0) { + _height = -_height; + } + //now, we have successfully got all the basics + // BMP rows are padded (if needed) to 4-byte boundary + _rowSize = (_width * 3 + 3) & ~3; + //check image size - if it is too large, it will be unusable + if (_rowSize*_height>BUF_SIZE) { + _valid=false; + bmpFile.close(); + return false; + } + + bmpFile.close(); + // Ensure filename fits our buffer (segment name length constraint). + size_t len = strlen(fn); + if (len > WLED_MAX_SEGNAME_LEN) { + return false; + } + strncpy(filename, fn, sizeof(filename)); + filename[sizeof(filename) - 1] = '\0'; + _valid = true; + return true; +} + +void BMPimage::clear(){ + strcpy(filename, ""); + _width=0; + _height=0; + _rowSize=0; + _imageOffset=0; + _loaded=false; + _valid=false; +} + +bool BMPimage::load(){ + const size_t size = (size_t)_rowSize * (size_t)_height; + if (size > BUF_SIZE) { + return false; + } + File bmpFile = WLED_FS.open(filename); + if (!bmpFile) { + return false; + } + + if (_buffer != nullptr) free(_buffer); + _buffer = (byte*)malloc(size); + if (_buffer == nullptr) return false; + + bmpFile.seek(_imageOffset); + const size_t readBytes = bmpFile.read(_buffer, size); + bmpFile.close(); + if (readBytes != size) { + _loaded = false; + return false; + } + _loaded = true; + return true; +} + +byte* BMPimage::line(uint16_t n){ + if (_loaded) { + return (_buffer+n*_rowSize); + } else { + return NULL; + } +} + +uint32_t BMPimage::pixelColor(uint16_t x, uint16_t y){ + uint32_t pos; + byte b,g,r; //colors + if (! _loaded) { + return 0; + } + if ( (x>=_width) || (y>=_height) ) { + return 0; + } + pos=y*_rowSize + 3*x; + //get colors. Note that in BMP files, they go in BGR order + b= _buffer[pos++]; + g= _buffer[pos++]; + r= _buffer[pos]; + return (r<<16|g<<8|b); +} diff --git a/usermods/pov_display/bmpimage.h b/usermods/pov_display/bmpimage.h new file mode 100644 index 0000000000..a83d1fa904 --- /dev/null +++ b/usermods/pov_display/bmpimage.h @@ -0,0 +1,50 @@ +#ifndef _BMPIMAGE_H +#define _BMPIMAGE_H +#include "Arduino.h" +#include "wled.h" + +/* + * This class describes a bitmap image. Each object refers to a bmp file on + * filesystem fatfs. + * To initialize, call init(), passign to it name of a bitmap file + * at the root of fatfs filesystem: + * + * BMPimage myImage; + * myImage.init("logo.bmp"); + * + * For performance reasons, before actually usign the image, you need to load + * it from filesystem to RAM: + * myImage.load(); + * All load() operations use the same reserved buffer in RAM, so you can only + * have one file loaded at a time. Before loading a new file, always unload the + * previous one: + * myImage.unload(); + */ + +class BMPimage { + public: + int height() {return _height; } + int width() {return _width; } + int rowSize() {return _rowSize;} + bool isLoaded() {return _loaded; } + bool load(); + void unload() {_loaded=false; } + byte * line(uint16_t n); + uint32_t pixelColor(uint16_t x,uint16_t y); + bool init(const char* fn); + void clear(); + char * getFilename() {return filename;}; + + private: + char filename[WLED_MAX_SEGNAME_LEN+1]=""; + int _width=0; + int _height=0; + int _rowSize=0; + int _imageOffset=0; + bool _loaded=false; + bool _valid=false; +}; + +extern byte * _buffer; + +#endif diff --git a/usermods/pov_display/library.json.disabled b/usermods/pov_display/library.json similarity index 54% rename from usermods/pov_display/library.json.disabled rename to usermods/pov_display/library.json index 2dd944a8af..461b1e2d48 100644 --- a/usermods/pov_display/library.json.disabled +++ b/usermods/pov_display/library.json @@ -1,7 +1,5 @@ { "name:": "pov_display", "build": { "libArchive": false}, - "dependencies": { - "bitbank2/PNGdec":"^1.0.3" - } + "platforms": ["espressif32"] } diff --git a/usermods/pov_display/pov.cpp b/usermods/pov_display/pov.cpp new file mode 100644 index 0000000000..ea5a43ed68 --- /dev/null +++ b/usermods/pov_display/pov.cpp @@ -0,0 +1,47 @@ +#include "pov.h" + +POV::POV() {} + +void POV::showLine(const byte * line, uint16_t size){ + uint16_t i, pos; + uint8_t r, g, b; + if (!line) { + // All-black frame on null input + for (i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, CRGB::Black); + } + strip.show(); + lastLineUpdate = micros(); + return; + } + for (i = 0; i < SEGLEN; i++) { + if (i < size) { + pos = 3 * i; + // using bgr order + b = line[pos++]; + g = line[pos++]; + r = line[pos]; + SEGMENT.setPixelColor(i, CRGB(r, g, b)); + } else { + SEGMENT.setPixelColor(i, CRGB::Black); + } + } + strip.show(); + lastLineUpdate = micros(); +} + +bool POV::loadImage(const char * filename){ + if(!image.init(filename)) return false; + if(!image.load()) return false; + currentLine=0; + return true; +} + +int16_t POV::showNextLine(){ + if (!image.isLoaded()) return 0; + //move to next line + showLine(image.line(currentLine), image.width()); + currentLine++; + if (currentLine == image.height()) {currentLine=0;} + return currentLine; +} diff --git a/usermods/pov_display/pov.h b/usermods/pov_display/pov.h new file mode 100644 index 0000000000..cb543d2ea7 --- /dev/null +++ b/usermods/pov_display/pov.h @@ -0,0 +1,42 @@ +#ifndef _POV_H +#define _POV_H +#include "bmpimage.h" + + +class POV { + public: + POV(); + + /* Shows one line. line should be pointer to array which holds pixel colors + * (3 bytes per pixel, in BGR order). Note: 3, not 4!!! + * size should be size of array (number of pixels, not number of bytes) + */ + void showLine(const byte * line, uint16_t size); + + /* Reads from file an image and making it current image */ + bool loadImage(const char * filename); + + /* Show next line of active image + Retunrs the index of next line to be shown (not yet shown!) + If it retunrs 0, it means we have completed showing the image and + next call will start again + */ + int16_t showNextLine(); + + //time since strip was last updated, in micro sec + uint32_t timeSinceUpdate() {return (micros()-lastLineUpdate);} + + + BMPimage * currentImage() {return ℑ} + + char * getFilename() {return image.getFilename();} + + private: + BMPimage image; + int16_t currentLine=0; //next line to be shown + uint32_t lastLineUpdate=0; //time in microseconds +}; + + + +#endif diff --git a/usermods/pov_display/pov_display.cpp b/usermods/pov_display/pov_display.cpp index b2b91f7d5e..ac68e1b209 100644 --- a/usermods/pov_display/pov_display.cpp +++ b/usermods/pov_display/pov_display.cpp @@ -1,88 +1,75 @@ #include "wled.h" -#include +#include "pov.h" -void * openFile(const char *filename, int32_t *size) { - f = WLED_FS.open(filename); - *size = f.size(); - return &f; -} - -void closeFile(void *handle) { - if (f) f.close(); -} - -int32_t readFile(PNGFILE *pFile, uint8_t *pBuf, int32_t iLen) -{ - int32_t iBytesRead; - iBytesRead = iLen; - File *f = static_cast(pFile->fHandle); - // Note: If you read a file all the way to the last byte, seek() stops working - if ((pFile->iSize - pFile->iPos) < iLen) - iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around - if (iBytesRead <= 0) - return 0; - iBytesRead = (int32_t)f->read(pBuf, iBytesRead); - pFile->iPos = f->position(); - return iBytesRead; -} +static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;;"; -int32_t seekFile(PNGFILE *pFile, int32_t iPosition) -{ - int i = micros(); - File *f = static_cast(pFile->fHandle); - f->seek(iPosition); - pFile->iPos = (int32_t)f->position(); - i = micros() - i; - return pFile->iPos; -} - -void draw(PNGDRAW *pDraw) { - uint16_t usPixels[SEGLEN]; - png.getLineAsRGB565(pDraw, usPixels, PNG_RGB565_LITTLE_ENDIAN, 0xffffffff); - for(int x=0; x < SEGLEN; x++) { - uint16_t color = usPixels[x]; - byte r = ((color >> 11) & 0x1F); - byte g = ((color >> 5) & 0x3F); - byte b = (color & 0x1F); - SEGMENT.setPixelColor(x, RGBW32(r,g,b,0)); - } - strip.show(); -} +static POV s_pov; uint16_t mode_pov_image(void) { - const char * filepath = SEGMENT.name; - int rc = png.open(filepath, openFile, closeFile, readFile, seekFile, draw); - if (rc == PNG_SUCCESS) { - rc = png.decode(NULL, 0); - png.close(); - return FRAMETIME; - } + Segment& mainseg = strip.getMainSegment(); + const char* segName = mainseg.name; + if (!segName) { + return FRAMETIME; + } + // Only proceed for files ending with .bmp (case-insensitive) + size_t segLen = strlen(segName); + if (segLen < 4) return FRAMETIME; + const char* ext = segName + (segLen - 4); + // compare case-insensitive to ".bmp" + if (!((ext[0]=='.') && + (ext[1]=='b' || ext[1]=='B') && + (ext[2]=='m' || ext[2]=='M') && + (ext[3]=='p' || ext[3]=='P'))) { return FRAMETIME; + } + + const char* current = s_pov.getFilename(); + if (current && strcmp(segName, current) == 0) { + s_pov.showNextLine(); + return FRAMETIME; + } + + static unsigned long s_lastLoadAttemptMs = 0; + unsigned long nowMs = millis(); + // Retry at most twice per second if the image is not yet loaded. + if (nowMs - s_lastLoadAttemptMs < 500) return FRAMETIME; + s_lastLoadAttemptMs = nowMs; + s_pov.loadImage(segName); + return FRAMETIME; } -class PovDisplayUsermod : public Usermod -{ - public: - static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;1"; +class PovDisplayUsermod : public Usermod { +protected: + bool enabled = false; //WLEDMM + const char *_name; //WLEDMM + bool initDone = false; //WLEDMM + unsigned long lastTime = 0; //WLEDMM +public: - PNG png; - File f; + PovDisplayUsermod(const char *name, bool enabled) + : enabled(enabled) , _name(name) {} + + void setup() override { + strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE); + //initDone removed (unused) + } - void setup() { - strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE); - } - void loop() { - } + void loop() override { + // if usermod is disabled or called during strip updating just exit + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!enabled || strip.isUpdating()) return; - uint16_t getId() - { - return USERMOD_ID_POV_DISPLAY; + // do your magic here + if (millis() - lastTime > 1000) { + lastTime = millis(); } + } - void connected() {} + uint16_t getId() override { + return USERMOD_ID_POV_DISPLAY; + } }; - -static PovDisplayUsermod pov_display; -REGISTER_USERMOD(pov_display); \ No newline at end of file +static PovDisplayUsermod pov_display("POV Display", false); +REGISTER_USERMOD(pov_display); diff --git a/usermods/pov_display/pov_display.gif b/usermods/pov_display/pov_display.gif new file mode 100644 index 0000000000..58f8ee0c1f Binary files /dev/null and b/usermods/pov_display/pov_display.gif differ diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a680de64de..f3163dc18e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7528,9 +7528,9 @@ uint16_t mode_2Ddistortionwaves() { byte valueG = gdistort + ((a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 ))<<1); byte valueB = bdistort + ((a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 ))<<1); - valueR = gamma8(cos8_t(valueR)); - valueG = gamma8(cos8_t(valueG)); - valueB = gamma8(cos8_t(valueB)); + valueR = cos8_t(valueR); + valueG = cos8_t(valueG); + valueB = cos8_t(valueB); if(SEGMENT.palette == 0) { // use RGB values (original color mode) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp old mode 100755 new mode 100644 index 32e34faf98..2f8d5515fd --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -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(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(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(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(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(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(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 @@ -278,11 +282,13 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) { _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings _t->_start = millis(); // restart countdown _t->_dur = dur; + _t->_prevPaletteBlends = 0; 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 +304,12 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) { #endif for (int i=0; i_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 }; } @@ -364,6 +369,7 @@ void Segment::beginDraw(uint16_t prog) { // minimum blend time is 100ms maximum is 65535ms #ifndef WLED_SAVE_RAM unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; + if(noOfBlends > 255) noOfBlends = 255; // safety check for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48); Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette #else @@ -425,14 +431,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 +456,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(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 +583,8 @@ Segment &Segment::setName(const char *newName) { if (newLen) { if (name) d_free(name); // free old name name = static_cast(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 +1087,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(pos << 8), 255, 255), rgb); + return rgb | (w << 24); // add white channel } /* @@ -1196,8 +1194,9 @@ void WS2812FX::finalizeInit() { if (busEnd > _length) _length = busEnd; // This must be done after all buses have been created, as some kinds (parallel I2S) interact bus->begin(); - bus->setBrightness(bri); + bus->setBrightness(scaledBri(bri)); } + BusManager::initializeABL(); // init brightness limiter DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); Segment::maxWidth = _length; @@ -1210,10 +1209,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(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 +1256,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 @@ -1299,7 +1298,7 @@ static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t static uint8_t _subtract (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; } static uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); } static uint8_t _average (uint8_t a, uint8_t b) { return (a + b) >> 1; } -#ifdef CONFIG_IDF_TARGET_ESP32C3 +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) static uint8_t _multiply (uint8_t a, uint8_t b) { return ((a * b) + 255) >> 8; } // faster than division on C3 but slightly less accurate #else static uint8_t _multiply (uint8_t a, uint8_t b) { return (a * b) / 255; } // origianl uses a & b in range [0,1] @@ -1310,10 +1309,10 @@ static uint8_t _darken (uint8_t a, uint8_t b) { return a < b ? a : b; } static uint8_t _screen (uint8_t a, uint8_t b) { return 255 - _multiply(~a,~b); } // 255 - (255-a)*(255-b)/255 static uint8_t _overlay (uint8_t a, uint8_t b) { return b < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } static uint8_t _hardlight (uint8_t a, uint8_t b) { return a < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } -#ifdef CONFIG_IDF_TARGET_ESP32C3 -static uint8_t _softlight (uint8_t a, uint8_t b) { return (((b * b * (255 - 2 * a) + 255) >> 8) + 2 * a * b + 255) >> 8; } // Pegtop's formula (1 - 2a)b^2 + 2ab +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) +static uint8_t _softlight (uint8_t a, uint8_t b) { return (((b * b * (255 - 2 * a))) + ((2 * a * b + 256) << 8)) >> 16; } // Pegtop's formula (1 - 2a)b^2 #else -static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a) / 255 + 2 * a * b) / 255; } // Pegtop's formula (1 - 2a)b^2 + 2ab +static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a) + 255 * 2 * a * b) / (255 * 255); } // Pegtop's formula (1 - 2a)b^2 + 2ab #endif static uint8_t _dodge (uint8_t a, uint8_t b) { return _divide(~a,b); } static uint8_t _burn (uint8_t a, uint8_t b) { return ~_divide(a,~b); } @@ -1344,11 +1343,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 +1455,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 @@ -1555,67 +1551,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { Segment::setClippingRect(0, 0); // disable clipping for overlays } -// To disable brightness limiter we either set output max current to 0 or single LED current to 0 -static uint8_t estimateCurrentAndLimitBri(uint8_t brightness, uint32_t *pixels) { - unsigned milliAmpsMax = BusManager::ablMilliampsMax(); - if (milliAmpsMax > 0) { - unsigned milliAmpsTotal = 0; - unsigned avgMilliAmpsPerLED = 0; - unsigned lengthDigital = 0; - bool useWackyWS2815PowerModel = false; - - for (size_t i = 0; i < BusManager::getNumBusses(); i++) { - const Bus *bus = BusManager::getBus(i); - if (!(bus && bus->isDigital() && bus->isOk())) continue; - unsigned maPL = bus->getLEDCurrent(); - if (maPL == 0 || bus->getMaxCurrent() > 0) continue; // skip buses with 0 mA per LED or max current per bus defined (PP-ABL) - if (maPL == 255) { - useWackyWS2815PowerModel = true; - maPL = 12; // WS2815 uses 12mA per channel - } - avgMilliAmpsPerLED += maPL * bus->getLength(); - lengthDigital += bus->getLength(); - // sum up the usage of each LED on digital bus - uint32_t busPowerSum = 0; - for (unsigned j = 0; j < bus->getLength(); j++) { - uint32_t c = pixels[j + bus->getStart()]; - byte r = R(c), g = G(c), b = B(c), w = W(c); - if (useWackyWS2815PowerModel) { //ignore white component on WS2815 power calculation - busPowerSum += (max(max(r,g),b)) * 3; - } else { - busPowerSum += (r + g + b + w); - } - } - // RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less - if (bus->hasWhite()) { - busPowerSum *= 3; - busPowerSum >>= 2; //same as /= 4 - } - // powerSum has all the values of channels summed (max would be getLength()*765 as white is excluded) so convert to milliAmps - milliAmpsTotal += (busPowerSum * maPL * brightness) / (765*255); - } - if (lengthDigital > 0) { - avgMilliAmpsPerLED /= lengthDigital; - - if (milliAmpsMax > MA_FOR_ESP && avgMilliAmpsPerLED > 0) { //0 mA per LED and too low numbers turn off calculation - unsigned powerBudget = (milliAmpsMax - MA_FOR_ESP); //80/120mA for ESP power - if (powerBudget > lengthDigital) { //each LED uses about 1mA in standby, exclude that from power budget - powerBudget -= lengthDigital; - } else { - powerBudget = 0; - } - if (milliAmpsTotal > powerBudget) { - //scale brightness down to stay in current limit - unsigned scaleB = powerBudget * 255 / milliAmpsTotal; - brightness = ((brightness * scaleB) >> 8) + 1; - } - } - } - } - return brightness; -} - void WS2812FX::show() { + if (!_pixels) return; // no pixels allocated, nothing to show + unsigned long showNow = millis(); size_t diff = showNow - _lastShow; @@ -1640,10 +1578,6 @@ void WS2812FX::show() { show_callback callback = _callback; if (callback) callback(); // will call setPixelColor or setRealtimePixelColor - // determine ABL brightness - uint8_t newBri = estimateCurrentAndLimitBri(_brightness, _pixels); - if (newBri != _brightness) BusManager::setBrightness(newBri); - // paint actual pixels int oldCCT = Bus::getCCT(); // store original CCT value (since it is global) // when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1) @@ -1654,7 +1588,11 @@ void WS2812FX::show() { if (_pixelCCT) { // cctFromRgb already exluded at allocation if (i == 0 || _pixelCCT[i-1] != _pixelCCT[i]) BusManager::setSegmentCCT(_pixelCCT[i], correctWB); } - BusManager::setPixelColor(getMappedPixelIndex(i), realtimeMode && arlsDisableGammaCorrection ? _pixels[i] : gamma32(_pixels[i])); + + uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32) + if(c > 0 && !(realtimeMode && arlsDisableGammaCorrection)) + c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss + BusManager::setPixelColor(getMappedPixelIndex(i), c); } Bus::setCCT(oldCCT); // restore old CCT for ABL adjustments @@ -1666,9 +1604,6 @@ void WS2812FX::show() { // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods BusManager::show(); - // restore brightness for next frame - if (newBri != _brightness) BusManager::setBrightness(_brightness); - if (diff > 0) { // skip calculation if no time has passed size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math _cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1); // "+FPS_CALC_AVG/2" for proper rounding @@ -1733,7 +1668,7 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { if (_brightness == 0) { //unfreeze all segments on power off for (const Segment &seg : _segments) seg.freeze = false; // freeze is mutable } - BusManager::setBrightness(b); + BusManager::setBrightness(scaledBri(b)); if (!direct) { unsigned long t = millis(); if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon @@ -1888,7 +1823,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 +1947,7 @@ bool WS2812FX::deserializeMap(unsigned n) { } d_free(customMappingTable); - customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // do not use SPI RAM + customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer DRAM for speed if (customMappingTable) { DEBUG_PRINTF_P(PSTR("ledmap allocated: %uB\n"), sizeof(uint16_t)*getLengthTotal()); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 21061a0989..8b684a5f69 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1118,7 +1118,7 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, allocsuccess = true; break; // allocation succeeded } - numparticles /= 2; // cut number of particles in half and try again + numparticles = ((numparticles / 2) + 3) & ~0x03; // cut number of particles in half and try again, must be 4 byte aligned PSPRINTLN(F("PS 2D alloc failed, trying with less particles...")); } if (!allocsuccess) { @@ -1815,7 +1815,7 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso allocsuccess = true; break; // allocation succeeded } - numparticles /= 2; // cut number of particles in half and try again + numparticles = ((numparticles / 2) + 3) & ~0x03; // cut number of particles in half and try again, must be 4 byte aligned PSPRINTLN(F("PS 1D alloc failed, trying with less particles...")); } if (!allocsuccess) { diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index a83b29bdee..9067746973 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -5,6 +5,8 @@ #include #include #ifdef ARDUINO_ARCH_ESP32 +#include +#include "src/dependencies/network/Network.h" // for isConnected() (& WiFi) #include "driver/ledc.h" #include "soc/ledc_struct.h" #if !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)) @@ -20,11 +22,13 @@ #include "core_esp8266_waveform.h" #endif #include "const.h" +#include "colors.h" #include "pin_manager.h" #include "bus_manager.h" #include "bus_wrapper.h" #include +extern bool mDNSenabled; extern bool cctICused; extern bool useParallelI2S; @@ -102,7 +106,7 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { } else { cct = (approximateKelvinFromRGB(c) - 1900) >> 5; // convert K (from RGB value) to relative format } - + //0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold) if (cct < _cctBlend) ww = 255; else ww = ((255-cct) * 255) / (255 - _cctBlend); @@ -141,6 +145,7 @@ BusDigital::BusDigital(const BusConfig &bc, uint8_t nr) if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; } _frequencykHz = 0U; + _colorSum = 0; _pins[0] = bc.pins[0]; if (is2Pin(bc.type)) { if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { @@ -183,80 +188,62 @@ BusDigital::BusDigital(const BusConfig &bc, uint8_t nr) //Stay safe with high amperage and have a reasonable safety margin! //I am NOT to be held liable for burned down garages or houses! -// To disable brightness limiter we either set output max current to 0 or single LED current to 0 -uint8_t BusDigital::estimateCurrentAndLimitBri() const { - bool useWackyWS2815PowerModel = false; - byte actualMilliampsPerLed = _milliAmpsPerLed; - - if (_milliAmpsMax < MA_FOR_ESP/BusManager::getNumBusses() || actualMilliampsPerLed == 0) { //0 mA per LED and too low numbers turn off calculation - return _bri; - } +// note on ABL implementation: +// ABL is set up in finalizeInit() +// scaled color channels are summed in BusDigital::setPixelColor() +// the used current is estimated and limited in BusManager::show() +// if limit is set too low, brightness is limited to 1 to at least show some light +// to disable brightness limiter for a bus, set LED current to 0 +void BusDigital::estimateCurrent() { + uint32_t actualMilliampsPerLed = _milliAmpsPerLed; if (_milliAmpsPerLed == 255) { - useWackyWS2815PowerModel = true; + // use wacky WS2815 power model, see WLED issue #549 + _colorSum *= 3; // sum is sum of max value for each color, need to multiply by three to account for clrUnitsPerChannel being 3*255 actualMilliampsPerLed = 12; // from testing an actual strip } + // _colorSum has all the values of color channels summed, max would be getLength()*(3*255 + (255 if hasWhite()): convert to milliAmps + uint32_t clrUnitsPerChannel = hasWhite() ? 4*255 : 3*255; + _milliAmpsTotal = ((uint64_t)_colorSum * actualMilliampsPerLed) / clrUnitsPerChannel + getLength(); // add 1mA standby current per LED to total (WS2812: ~0.7mA, WS2815: ~2mA) +} - unsigned powerBudget = (_milliAmpsMax - MA_FOR_ESP/BusManager::getNumBusses()); //80/120mA for ESP power - if (powerBudget > getLength()) { //each LED uses about 1mA in standby, exclude that from power budget - powerBudget -= getLength(); - } else { - powerBudget = 0; - } - - uint32_t busPowerSum = 0; - for (unsigned i = 0; i < getLength(); i++) { //sum up the usage of each LED - uint32_t c = getPixelColor(i); // always returns original or restored color without brightness scaling - byte r = R(c), g = G(c), b = B(c), w = W(c); - - if (useWackyWS2815PowerModel) { //ignore white component on WS2815 power calculation - busPowerSum += (max(max(r,g),b)) * 3; +void BusDigital::applyBriLimit(uint8_t newBri) { + // a newBri of 0 means calculate per-bus brightness limit + if (newBri == 0) { + if (_milliAmpsLimit == 0 || _milliAmpsTotal == 0) return; // ABL not used for this bus + newBri = 255; + + if (_milliAmpsLimit > getLength()) { // each LED uses about 1mA in standby + if (_milliAmpsTotal > _milliAmpsLimit) { + // scale brightness down to stay in current limit + newBri = ((uint32_t)_milliAmpsLimit * 255) / _milliAmpsTotal + 1; // +1 to avoid 0 brightness + _milliAmpsTotal = _milliAmpsLimit; + } } else { - busPowerSum += (r + g + b + w); + newBri = 1; // limit too low, set brightness to 1, this will dim down all colors to minimum since we use video scaling + _milliAmpsTotal = getLength(); // estimate bus current as minimum } } - if (hasWhite()) { //RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less - busPowerSum *= 3; - busPowerSum >>= 2; //same as /= 4 + if (newBri < 255) { + uint8_t cctWW = 0, cctCW = 0; + unsigned hwLen = _len; + if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus + for (unsigned i = 0; i < hwLen; i++) { + uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); // need to revert color order for correct color scaling and CCT calc in case white is swapped + uint32_t c = PolyBus::getPixelColor(_busPtr, _iType, i, co); + c = color_fade(c, newBri, true); // apply additional dimming note: using inline version is a bit faster but overhead of getPixelColor() dominates the speed impact by far + if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW); + PolyBus::setPixelColor(_busPtr, _iType, i, c, co, (cctCW<<8) | cctWW); // repaint all pixels with new brightness + } } - // powerSum has all the values of channels summed (max would be getLength()*765 as white is excluded) so convert to milliAmps - BusDigital::_milliAmpsTotal = (busPowerSum * actualMilliampsPerLed * _bri) / (765*255); - - uint8_t newBri = _bri; - if (BusDigital::_milliAmpsTotal > powerBudget) { - //scale brightness down to stay in current limit - unsigned scaleB = powerBudget * 255 / BusDigital::_milliAmpsTotal; - newBri = (_bri * scaleB) / 256 + 1; - BusDigital::_milliAmpsTotal = powerBudget; - //_milliAmpsTotal = (busPowerSum * actualMilliampsPerLed * newBri) / (765*255); - } - return newBri; + _colorSum = 0; // reset for next frame } void BusDigital::show() { - BusDigital::_milliAmpsTotal = 0; if (!_valid) return; - - uint8_t cctWW = 0, cctCW = 0; - unsigned newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal (TODO: could use PolyBus::CalcTotalMilliAmpere()) - if (newBri < _bri) { - PolyBus::setBrightness(_busPtr, _iType, newBri); // limit brightness to stay within current limits - unsigned hwLen = _len; - if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus - for (unsigned i = 0; i < hwLen; i++) { - // use 0 as color order, actual order does not matter here as we just update the channel values as-is - uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, i, 0), _bri); - if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW); // this will unfortunately corrupt (segment) CCT data on every bus - PolyBus::setPixelColor(_busPtr, _iType, i, c, 0, (cctCW<<8) | cctWW); // repaint all pixels with new brightness - } - } PolyBus::show(_busPtr, _iType, _skip); // faster if buffer consistency is not important (no skipped LEDs) - // restore bus brightness to its original value - // this is done right after show, so this is only OK if LED updates are completed before show() returns - // or async show has a separate buffer (ESP32 RMT and I2S are ok) - if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, _bri); } bool BusDigital::canShow() const { @@ -264,12 +251,6 @@ bool BusDigital::canShow() const { return PolyBus::canShow(_busPtr, _iType); } -void BusDigital::setBrightness(uint8_t b) { - if (_bri == b) return; - Bus::setBrightness(b); - PolyBus::setBrightness(_busPtr, _iType, b); -} - //If LEDs are skipped, it is possible to use the first as a status LED. //TODO only show if no new show due in the next 50ms void BusDigital::setStatusPixel(uint32_t c) { @@ -283,13 +264,25 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; if (hasWhite()) c = autoWhiteCalc(c); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT + c = color_fade(c, _bri, true); // apply brightness + + if (BusManager::_useABL) { + // if using ABL, sum all color channels to estimate current and limit brightness in show() + uint8_t r = R(c), g = G(c), b = B(c); + if (_milliAmpsPerLed < 255) { // normal ABL + _colorSum += r + g + b + W(c); + } else { // wacky WS2815 power model, ignore white channel, use max of RGB (issue #549) + _colorSum += ((r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b)); + } + } + if (_reversed) pix = _len - pix -1; pix += _skip; - unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs unsigned pOld = pix; pix = IC_INDEX_WS2812_1CH_3X(pix); - uint32_t cOld = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, pix, co),_bri); + uint32_t cOld = PolyBus::getPixelColor(_busPtr, _iType, pix, co); switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) case 0: c = RGBW32(R(cOld), W(c) , B(cOld), 0); break; case 1: c = RGBW32(W(c) , G(cOld), B(cOld), 0); break; @@ -306,17 +299,17 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); } -// returns original color if global buffering is enabled, else returns lossly restored color from bus +// returns lossly restored color from bus uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { if (!_valid) return 0; if (_reversed) pix = _len - pix -1; pix += _skip; - const unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - unsigned r = R(c); - unsigned g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? - unsigned b = _reversed ? G(c) : B(c); + uint8_t r = R(c); + uint8_t g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? + uint8_t b = _reversed ? G(c) : B(c); switch (pix % 3) { // get only the single channel case 0: c = RGBW32(g, g, g, g); break; case 1: c = RGBW32(r, r, r, r); break; @@ -468,10 +461,7 @@ void BusPwm::setPixelColor(unsigned pix, uint32_t c) { if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT } - uint8_t r = R(c); - uint8_t g = G(c); - uint8_t b = B(c); - uint8_t w = W(c); + uint8_t r = R(c), g = G(c), b = B(c), w = W(c); switch (_type) { case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation @@ -646,10 +636,7 @@ BusOnOff::BusOnOff(const BusConfig &bc) void BusOnOff::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel c = autoWhiteCalc(c); - uint8_t r = R(c); - uint8_t g = G(c); - uint8_t b = B(c); - uint8_t w = W(c); + uint8_t r = R(c), g = G(c), b = B(c), w = W(c); _data = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; } @@ -699,6 +686,10 @@ BusNetwork::BusNetwork(const BusConfig &bc) _hasCCT = false; _UDPchannels = _hasWhite + 3; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); + #ifdef ARDUINO_ARCH_ESP32 + _hostname = bc.text; + resolveHostname(); // resolve hostname to IP address if needed + #endif _data = (uint8_t*)d_calloc(_len, _UDPchannels); _valid = (_data != nullptr); DEBUGBUS_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); @@ -733,6 +724,19 @@ size_t BusNetwork::getPins(uint8_t* pinArray) const { return 4; } +#ifdef ARDUINO_ARCH_ESP32 +void BusNetwork::resolveHostname() { + static unsigned long nextResolve = 0; + if (Network.isConnected() && millis() > nextResolve && _hostname.length() > 0) { + nextResolve = millis() + 600000; // resolve only every 10 minutes + IPAddress clnt; + if (mDNSenabled) clnt = MDNS.queryHost(_hostname); + else WiFi.hostByName(_hostname.c_str(), clnt); + if (clnt != IPAddress()) _client = clnt; + } +} +#endif + // credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusNetwork::getLEDTypes() { return { @@ -918,6 +922,13 @@ void BusManager::on() { } } } + #else + for (auto &bus : busses) if (bus->isVirtual()) { + // virtual/network bus should check for IP change if hostname is specified + // otherwise there are no endpoints to force DNS resolution + BusNetwork &b = static_cast(*bus); + b.resolveHostname(); + } #endif #ifdef ESP32_DATA_IDLE_HIGH esp32RMTInvertIdle(); @@ -937,13 +948,13 @@ void BusManager::off() { #ifdef ESP32_DATA_IDLE_HIGH esp32RMTInvertIdle(); #endif + _gMilliAmpsUsed = 0; // reset, assume no LED idle current if relay is off } void BusManager::show() { - _gMilliAmpsUsed = 0; + applyABL(); // apply brightness limit, updates _gMilliAmpsUsed for (auto &bus : busses) { bus->show(); - _gMilliAmpsUsed += bus->getUsedCurrent(); } } @@ -976,6 +987,85 @@ bool BusManager::canAllShow() { return true; } +void BusManager::initializeABL() { + _useABL = false; // reset + if (_gMilliAmpsMax > 0) { + // check global brightness limit + for (auto &bus : busses) { + if (bus->isDigital() && bus->getLEDCurrent() > 0) { + _useABL = true; // at least one bus has valid LED current + return; + } + } + } else { + // check per bus brightness limit + unsigned numABLbuses = 0; + for (auto &bus : busses) { + if (bus->isDigital() && bus->getLEDCurrent() > 0 && bus->getMaxCurrent() > 0) + numABLbuses++; // count ABL enabled buses + } + if (numABLbuses > 0) { + _useABL = true; // at least one bus has ABL set + uint32_t ESPshare = MA_FOR_ESP / numABLbuses; // share of ESP current per ABL bus + for (auto &bus : busses) { + if (bus->isDigital()) { + BusDigital &busd = static_cast(*bus); + uint32_t busLength = busd.getLength(); + uint32_t busDemand = busLength * busd.getLEDCurrent(); + uint32_t busMax = busd.getMaxCurrent(); + if (busMax > ESPshare) busMax -= ESPshare; + if (busMax < busLength) busMax = busLength; // give each LED 1mA, ABL will dim down to minimum + if (busDemand == 0) busMax = 0; // no LED current set, disable ABL for this bus + busd.setCurrentLimit(busMax); + } + } + } + } +} + +void BusManager::applyABL() { + if (_useABL) { + unsigned milliAmpsSum = 0; // use temporary variable to always return a valid _gMilliAmpsUsed to UI + unsigned totalLEDs = 0; + for (auto &bus : busses) { + if (bus->isDigital() && bus->isOk()) { + BusDigital &busd = static_cast(*bus); + busd.estimateCurrent(); // sets _milliAmpsTotal, current is estimated for all buses even if they have the limit set to 0 + if (_gMilliAmpsMax == 0) + busd.applyBriLimit(0); // apply per bus ABL limit, updates _milliAmpsTotal if limit reached + milliAmpsSum += busd.getUsedCurrent(); + totalLEDs += busd.getLength(); // sum total number of LEDs for global Limit + } + } + // check global current limit and apply global ABL limit, total current is summed above + if (_gMilliAmpsMax > 0) { + uint8_t newBri = 255; + uint32_t globalMax = _gMilliAmpsMax > MA_FOR_ESP ? _gMilliAmpsMax - MA_FOR_ESP : 1; // subtract ESP current consumption, fully limit if too low + if (globalMax > totalLEDs) { // check if budget is larger than standby current + if (milliAmpsSum > globalMax) { + newBri = globalMax * 255 / milliAmpsSum + 1; // scale brightness down to stay in current limit, +1 to avoid 0 brightness + milliAmpsSum = globalMax; // update total used current + } + } else { + newBri = 1; // limit too low, set brightness to minimum + milliAmpsSum = totalLEDs; // estimate total used current as minimum + } + + // apply brightness limit to each bus, if its 255 it will only reset _colorSum + for (auto &bus : busses) { + if (bus->isDigital() && bus->isOk()) { + BusDigital &busd = static_cast(*bus); + if (busd.getLEDCurrent() > 0) // skip buses with LED current set to 0 + busd.applyBriLimit(newBri); + } + } + } + _gMilliAmpsUsed = milliAmpsSum; + } + else + _gMilliAmpsUsed = 0; // reset, we have no current estimation without ABL +} + ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } @@ -991,3 +1081,4 @@ uint16_t BusDigital::_milliAmpsTotal = 0; std::vector> BusManager::busses; uint16_t BusManager::_gMilliAmpsUsed = 0; uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; +bool BusManager::_useABL = false; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index f183e4b5bd..fe70a05170 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -133,6 +133,7 @@ class Bus { virtual uint16_t getUsedCurrent() const { return 0; } virtual uint16_t getMaxCurrent() const { return 0; } virtual size_t getBusSize() const { return sizeof(Bus); } + virtual const String getCustomText() const { return String(); } inline bool hasRGB() const { return _hasRgb; } inline bool hasWhite() const { return _hasWhite; } @@ -215,7 +216,7 @@ class Bus { uint8_t _autoWhiteMode; // global Auto White Calculation override static uint8_t _gAWM; - // _cct has the following menaings (see calculateCCT() & BusManager::setSegmentCCT()): + // _cct has the following meanings (see calculateCCT() & BusManager::setSegmentCCT()): // -1 means to extract approximate CCT value in K from RGB (in calcualteCCT()) // [0,255] is the exact CCT value where 0 means warm and 255 cold // [1900,10060] only for color correction expressed in K (colorBalanceFromKelvin()) @@ -237,7 +238,6 @@ class BusDigital : public Bus { void show() override; bool canShow() const override; - void setBrightness(uint8_t b) override; void setStatusPixel(uint32_t c) override; [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; @@ -249,6 +249,9 @@ class BusDigital : public Bus { uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } uint16_t getUsedCurrent() const override { return _milliAmpsTotal; } uint16_t getMaxCurrent() const override { return _milliAmpsMax; } + void setCurrentLimit(uint16_t milliAmps) { _milliAmpsLimit = milliAmps; } + void estimateCurrent(); // estimate used current from summed colors + void applyBriLimit(uint8_t newBri); size_t getBusSize() const override; void begin() override; void cleanup(); @@ -261,8 +264,10 @@ class BusDigital : public Bus { uint8_t _pins[2]; uint8_t _iType; uint16_t _frequencykHz; - uint8_t _milliAmpsPerLed; uint16_t _milliAmpsMax; + uint8_t _milliAmpsPerLed; + uint16_t _milliAmpsLimit; + uint32_t _colorSum; // total color value for the bus, updated in setPixelColor(), used to estimate current void *_busPtr; static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() @@ -277,8 +282,6 @@ class BusDigital : public Bus { } return c; } - - uint8_t estimateCurrentAndLimitBri() const; }; @@ -342,6 +345,10 @@ class BusNetwork : public Bus { size_t getBusSize() const override { return sizeof(BusNetwork) + (isOk() ? _len * _UDPchannels : 0); } void show() override; void cleanup(); + #ifdef ARDUINO_ARCH_ESP32 + void resolveHostname(); + const String getCustomText() const override { return _hostname; } + #endif static std::vector getLEDTypes(); @@ -351,6 +358,9 @@ class BusNetwork : public Bus { uint8_t _UDPchannels; bool _broadcastLock; uint8_t *_data; + #ifdef ARDUINO_ARCH_ESP32 + String _hostname; + #endif }; @@ -368,8 +378,9 @@ struct BusConfig { uint16_t frequency; uint8_t milliAmpsPerLed; uint16_t milliAmpsMax; + String text; - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, String sometext = "") : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) @@ -379,6 +390,7 @@ struct BusConfig { , frequency(clock_kHz) , milliAmpsPerLed(maPerLed) , milliAmpsMax(maMax) + , text(sometext) { refreshReq = (bool) GET_BIT(busType,7); type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) @@ -412,8 +424,8 @@ struct BusConfig { }; -//fine tune power estimation constants for your setup -//you can set it to 0 if the ESP is powered by USB and the LEDs by external +// milliamps used by ESP (for power estimation) +// you can set it to 0 if the ESP is powered by USB and the LEDs by external #ifndef MA_FOR_ESP #ifdef ESP8266 #define MA_FOR_ESP 80 //how much mA does the ESP use (Wemos D1 about 80mA) @@ -428,6 +440,7 @@ namespace BusManager { //extern std::vector busses; extern uint16_t _gMilliAmpsUsed; extern uint16_t _gMilliAmpsMax; + extern bool _useABL; #ifdef ESP32_DATA_IDLE_HIGH void esp32RMTInvertIdle() ; @@ -443,6 +456,8 @@ namespace BusManager { //inline uint16_t ablMilliampsMax() { unsigned sum = 0; for (auto &bus : busses) sum += bus->getMaxCurrent(); return sum; } inline uint16_t ablMilliampsMax() { return _gMilliAmpsMax; } // used for compatibility reasons (and enabling virtual global ABL) inline void setMilliampsMax(uint16_t max) { _gMilliAmpsMax = max;} + void initializeABL(); // setup automatic brightness limiter parameters, call once after buses are initialized + void applyABL(); // apply automatic brightness limiter, global or per bus void useParallelOutput(); // workaround for inaccessible PolyBus bool hasParallelOutput(); // workaround for inaccessible PolyBus diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 5d8f306f5e..2fe077182e 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -3,7 +3,7 @@ #define BusWrapper_h //#define NPB_CONF_4STEP_CADENCE -#include "NeoPixelBusLg.h" +#include "NeoPixelBus.h" //Hardware SPI Pins #define P_8266_HS_MOSI 13 @@ -141,65 +141,65 @@ /*** ESP8266 Neopixel methods ***/ #ifdef ESP8266 //RGB -#define B_8266_U0_NEO_3 NeoPixelBusLg //3 chan, esp8266, gpio1 -#define B_8266_U1_NEO_3 NeoPixelBusLg //3 chan, esp8266, gpio2 -#define B_8266_DM_NEO_3 NeoPixelBusLg //3 chan, esp8266, gpio3 -#define B_8266_BB_NEO_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin but 16) +#define B_8266_U0_NEO_3 NeoPixelBus //3 chan, esp8266, gpio1 +#define B_8266_U1_NEO_3 NeoPixelBus //3 chan, esp8266, gpio2 +#define B_8266_DM_NEO_3 NeoPixelBus //3 chan, esp8266, gpio3 +#define B_8266_BB_NEO_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) //RGBW -#define B_8266_U0_NEO_4 NeoPixelBusLg //4 chan, esp8266, gpio1 -#define B_8266_U1_NEO_4 NeoPixelBusLg //4 chan, esp8266, gpio2 -#define B_8266_DM_NEO_4 NeoPixelBusLg //4 chan, esp8266, gpio3 -#define B_8266_BB_NEO_4 NeoPixelBusLg //4 chan, esp8266, bb (any pin) +#define B_8266_U0_NEO_4 NeoPixelBus //4 chan, esp8266, gpio1 +#define B_8266_U1_NEO_4 NeoPixelBus //4 chan, esp8266, gpio2 +#define B_8266_DM_NEO_4 NeoPixelBus //4 chan, esp8266, gpio3 +#define B_8266_BB_NEO_4 NeoPixelBus //4 chan, esp8266, bb (any pin) //400Kbps -#define B_8266_U0_400_3 NeoPixelBusLg //3 chan, esp8266, gpio1 -#define B_8266_U1_400_3 NeoPixelBusLg //3 chan, esp8266, gpio2 -#define B_8266_DM_400_3 NeoPixelBusLg //3 chan, esp8266, gpio3 -#define B_8266_BB_400_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin) +#define B_8266_U0_400_3 NeoPixelBus //3 chan, esp8266, gpio1 +#define B_8266_U1_400_3 NeoPixelBus //3 chan, esp8266, gpio2 +#define B_8266_DM_400_3 NeoPixelBus //3 chan, esp8266, gpio3 +#define B_8266_BB_400_3 NeoPixelBus //3 chan, esp8266, bb (any pin) //TM1814 (RGBW) -#define B_8266_U0_TM1_4 NeoPixelBusLg -#define B_8266_U1_TM1_4 NeoPixelBusLg -#define B_8266_DM_TM1_4 NeoPixelBusLg -#define B_8266_BB_TM1_4 NeoPixelBusLg +#define B_8266_U0_TM1_4 NeoPixelBus +#define B_8266_U1_TM1_4 NeoPixelBus +#define B_8266_DM_TM1_4 NeoPixelBus +#define B_8266_BB_TM1_4 NeoPixelBus //TM1829 (RGB) -#define B_8266_U0_TM2_3 NeoPixelBusLg -#define B_8266_U1_TM2_3 NeoPixelBusLg -#define B_8266_DM_TM2_3 NeoPixelBusLg -#define B_8266_BB_TM2_3 NeoPixelBusLg +#define B_8266_U0_TM2_3 NeoPixelBus +#define B_8266_U1_TM2_3 NeoPixelBus +#define B_8266_DM_TM2_3 NeoPixelBus +#define B_8266_BB_TM2_3 NeoPixelBus //UCS8903 -#define B_8266_U0_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio1 -#define B_8266_U1_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio2 -#define B_8266_DM_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio3 -#define B_8266_BB_UCS_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin but 16) +#define B_8266_U0_UCS_3 NeoPixelBus //3 chan, esp8266, gpio1 +#define B_8266_U1_UCS_3 NeoPixelBus //3 chan, esp8266, gpio2 +#define B_8266_DM_UCS_3 NeoPixelBus //3 chan, esp8266, gpio3 +#define B_8266_BB_UCS_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) //UCS8904 RGBW -#define B_8266_U0_UCS_4 NeoPixelBusLg //4 chan, esp8266, gpio1 -#define B_8266_U1_UCS_4 NeoPixelBusLg //4 chan, esp8266, gpio2 -#define B_8266_DM_UCS_4 NeoPixelBusLg //4 chan, esp8266, gpio3 -#define B_8266_BB_UCS_4 NeoPixelBusLg //4 chan, esp8266, bb (any pin) +#define B_8266_U0_UCS_4 NeoPixelBus //4 chan, esp8266, gpio1 +#define B_8266_U1_UCS_4 NeoPixelBus //4 chan, esp8266, gpio2 +#define B_8266_DM_UCS_4 NeoPixelBus //4 chan, esp8266, gpio3 +#define B_8266_BB_UCS_4 NeoPixelBus //4 chan, esp8266, bb (any pin) //APA106 -#define B_8266_U0_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio1 -#define B_8266_U1_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio2 -#define B_8266_DM_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio3 -#define B_8266_BB_APA106_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin but 16) +#define B_8266_U0_APA106_3 NeoPixelBus //3 chan, esp8266, gpio1 +#define B_8266_U1_APA106_3 NeoPixelBus //3 chan, esp8266, gpio2 +#define B_8266_DM_APA106_3 NeoPixelBus //3 chan, esp8266, gpio3 +#define B_8266_BB_APA106_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) //FW1906 GRBCW -#define B_8266_U0_FW6_5 NeoPixelBusLg //esp8266, gpio1 -#define B_8266_U1_FW6_5 NeoPixelBusLg //esp8266, gpio2 -#define B_8266_DM_FW6_5 NeoPixelBusLg //esp8266, gpio3 -#define B_8266_BB_FW6_5 NeoPixelBusLg //esp8266, bb +#define B_8266_U0_FW6_5 NeoPixelBus //esp8266, gpio1 +#define B_8266_U1_FW6_5 NeoPixelBus //esp8266, gpio2 +#define B_8266_DM_FW6_5 NeoPixelBus //esp8266, gpio3 +#define B_8266_BB_FW6_5 NeoPixelBus //esp8266, bb //WS2805 GRBCW -#define B_8266_U0_2805_5 NeoPixelBusLg //esp8266, gpio1 -#define B_8266_U1_2805_5 NeoPixelBusLg //esp8266, gpio2 -#define B_8266_DM_2805_5 NeoPixelBusLg //esp8266, gpio3 -#define B_8266_BB_2805_5 NeoPixelBusLg //esp8266, bb +#define B_8266_U0_2805_5 NeoPixelBus //esp8266, gpio1 +#define B_8266_U1_2805_5 NeoPixelBus //esp8266, gpio2 +#define B_8266_DM_2805_5 NeoPixelBus //esp8266, gpio3 +#define B_8266_BB_2805_5 NeoPixelBus //esp8266, bb //TM1914 (RGB) -#define B_8266_U0_TM1914_3 NeoPixelBusLg -#define B_8266_U1_TM1914_3 NeoPixelBusLg -#define B_8266_DM_TM1914_3 NeoPixelBusLg -#define B_8266_BB_TM1914_3 NeoPixelBusLg +#define B_8266_U0_TM1914_3 NeoPixelBus +#define B_8266_U1_TM1914_3 NeoPixelBus +#define B_8266_DM_TM1914_3 NeoPixelBus +#define B_8266_BB_TM1914_3 NeoPixelBus //Sm16825 (RGBWC) -#define B_8266_U0_SM16825_5 NeoPixelBusLg -#define B_8266_U1_SM16825_5 NeoPixelBusLg -#define B_8266_DM_SM16825_5 NeoPixelBusLg -#define B_8266_BB_SM16825_5 NeoPixelBusLg +#define B_8266_U0_SM16825_5 NeoPixelBus +#define B_8266_U1_SM16825_5 NeoPixelBus +#define B_8266_DM_SM16825_5 NeoPixelBus +#define B_8266_BB_SM16825_5 NeoPixelBus #endif /*** ESP32 Neopixel methods ***/ @@ -245,84 +245,84 @@ #endif //RGB -#define B_32_RN_NEO_3 NeoPixelBusLg // ESP32, S2, S3, C3 -//#define B_32_IN_NEO_3 NeoPixelBusLg // ESP32 (dynamic I2S selection) -#define B_32_I2_NEO_3 NeoPixelBusLg // ESP32, S2, S3 (automatic I2S selection, see typedef above) -#define B_32_IP_NEO_3 NeoPixelBusLg // parallel I2S (ESP32, S2, S3) +#define B_32_RN_NEO_3 NeoPixelBus // ESP32, S2, S3, C3 +//#define B_32_IN_NEO_3 NeoPixelBus // ESP32 (dynamic I2S selection) +#define B_32_I2_NEO_3 NeoPixelBus // ESP32, S2, S3 (automatic I2S selection, see typedef above) +#define B_32_IP_NEO_3 NeoPixelBus // parallel I2S (ESP32, S2, S3) //RGBW -#define B_32_RN_NEO_4 NeoPixelBusLg -#define B_32_I2_NEO_4 NeoPixelBusLg -#define B_32_IP_NEO_4 NeoPixelBusLg // parallel I2S +#define B_32_RN_NEO_4 NeoPixelBus +#define B_32_I2_NEO_4 NeoPixelBus +#define B_32_IP_NEO_4 NeoPixelBus // parallel I2S //400Kbps -#define B_32_RN_400_3 NeoPixelBusLg -#define B_32_I2_400_3 NeoPixelBusLg -#define B_32_IP_400_3 NeoPixelBusLg // parallel I2S +#define B_32_RN_400_3 NeoPixelBus +#define B_32_I2_400_3 NeoPixelBus +#define B_32_IP_400_3 NeoPixelBus // parallel I2S //TM1814 (RGBW) -#define B_32_RN_TM1_4 NeoPixelBusLg -#define B_32_I2_TM1_4 NeoPixelBusLg -#define B_32_IP_TM1_4 NeoPixelBusLg // parallel I2S +#define B_32_RN_TM1_4 NeoPixelBus +#define B_32_I2_TM1_4 NeoPixelBus +#define B_32_IP_TM1_4 NeoPixelBus // parallel I2S //TM1829 (RGB) -#define B_32_RN_TM2_3 NeoPixelBusLg -#define B_32_I2_TM2_3 NeoPixelBusLg -#define B_32_IP_TM2_3 NeoPixelBusLg // parallel I2S +#define B_32_RN_TM2_3 NeoPixelBus +#define B_32_I2_TM2_3 NeoPixelBus +#define B_32_IP_TM2_3 NeoPixelBus // parallel I2S //UCS8903 -#define B_32_RN_UCS_3 NeoPixelBusLg -#define B_32_I2_UCS_3 NeoPixelBusLg -#define B_32_IP_UCS_3 NeoPixelBusLg // parallel I2S +#define B_32_RN_UCS_3 NeoPixelBus +#define B_32_I2_UCS_3 NeoPixelBus +#define B_32_IP_UCS_3 NeoPixelBus // parallel I2S //UCS8904 -#define B_32_RN_UCS_4 NeoPixelBusLg -#define B_32_I2_UCS_4 NeoPixelBusLg -#define B_32_IP_UCS_4 NeoPixelBusLg// parallel I2S +#define B_32_RN_UCS_4 NeoPixelBus +#define B_32_I2_UCS_4 NeoPixelBus +#define B_32_IP_UCS_4 NeoPixelBus// parallel I2S //APA106 -#define B_32_RN_APA106_3 NeoPixelBusLg -#define B_32_I2_APA106_3 NeoPixelBusLg -#define B_32_IP_APA106_3 NeoPixelBusLg // parallel I2S +#define B_32_RN_APA106_3 NeoPixelBus +#define B_32_I2_APA106_3 NeoPixelBus +#define B_32_IP_APA106_3 NeoPixelBus // parallel I2S //FW1906 GRBCW -#define B_32_RN_FW6_5 NeoPixelBusLg -#define B_32_I2_FW6_5 NeoPixelBusLg -#define B_32_IP_FW6_5 NeoPixelBusLg // parallel I2S +#define B_32_RN_FW6_5 NeoPixelBus +#define B_32_I2_FW6_5 NeoPixelBus +#define B_32_IP_FW6_5 NeoPixelBus // parallel I2S //WS2805 RGBWC -#define B_32_RN_2805_5 NeoPixelBusLg -#define B_32_I2_2805_5 NeoPixelBusLg -#define B_32_IP_2805_5 NeoPixelBusLg // parallel I2S +#define B_32_RN_2805_5 NeoPixelBus +#define B_32_I2_2805_5 NeoPixelBus +#define B_32_IP_2805_5 NeoPixelBus // parallel I2S //TM1914 (RGB) -#define B_32_RN_TM1914_3 NeoPixelBusLg -#define B_32_I2_TM1914_3 NeoPixelBusLg -#define B_32_IP_TM1914_3 NeoPixelBusLg // parallel I2S +#define B_32_RN_TM1914_3 NeoPixelBus +#define B_32_I2_TM1914_3 NeoPixelBus +#define B_32_IP_TM1914_3 NeoPixelBus // parallel I2S //Sm16825 (RGBWC) -#define B_32_RN_SM16825_5 NeoPixelBusLg -#define B_32_I2_SM16825_5 NeoPixelBusLg -#define B_32_IP_SM16825_5 NeoPixelBusLg // parallel I2S +#define B_32_RN_SM16825_5 NeoPixelBus +#define B_32_I2_SM16825_5 NeoPixelBus +#define B_32_IP_SM16825_5 NeoPixelBus // parallel I2S #endif //APA102 #ifdef WLED_USE_ETHERNET // fix for #2542 (by @BlackBird77) -#define B_HS_DOT_3 NeoPixelBusLg //hardware HSPI (was DotStarEsp32DmaHspi5MhzMethod in NPB @ 2.6.9) +#define B_HS_DOT_3 NeoPixelBus //hardware HSPI (was DotStarEsp32DmaHspi5MhzMethod in NPB @ 2.6.9) #else -#define B_HS_DOT_3 NeoPixelBusLg //hardware VSPI +#define B_HS_DOT_3 NeoPixelBus //hardware VSPI #endif -#define B_SS_DOT_3 NeoPixelBusLg //soft SPI +#define B_SS_DOT_3 NeoPixelBus //soft SPI //LPD8806 -#define B_HS_LPD_3 NeoPixelBusLg -#define B_SS_LPD_3 NeoPixelBusLg +#define B_HS_LPD_3 NeoPixelBus +#define B_SS_LPD_3 NeoPixelBus //LPD6803 -#define B_HS_LPO_3 NeoPixelBusLg -#define B_SS_LPO_3 NeoPixelBusLg +#define B_HS_LPO_3 NeoPixelBus +#define B_SS_LPO_3 NeoPixelBus //WS2801 #ifdef WLED_USE_ETHERNET -#define B_HS_WS1_3 NeoPixelBusLg>, NeoGammaNullMethod> +#define B_HS_WS1_3 NeoPixelBus>> #else -#define B_HS_WS1_3 NeoPixelBusLg +#define B_HS_WS1_3 NeoPixelBus #endif -#define B_SS_WS1_3 NeoPixelBusLg +#define B_SS_WS1_3 NeoPixelBus //P9813 -#define B_HS_P98_3 NeoPixelBusLg -#define B_SS_P98_3 NeoPixelBusLg +#define B_HS_P98_3 NeoPixelBus +#define B_SS_P98_3 NeoPixelBus // 48bit & 64bit to 24bit & 32bit RGB(W) conversion #define toRGBW32(c) (RGBW32((c>>40)&0xFF, (c>>24)&0xFF, (c>>8)&0xFF, (c>>56)&0xFF)) @@ -896,102 +896,6 @@ class PolyBus { } } - static void setBrightness(void* busPtr, uint8_t busType, uint8_t b) { - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - // I2S1 bus or paralell buses - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - #endif - #endif - case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_LPD_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_LPD_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_LPO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_LPO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_WS1_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_WS1_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_P98_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_P98_3: (static_cast(busPtr))->SetLuminance(b); break; - } - } - [[gnu::hot]] static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { RgbwColor col(0,0,0,0); switch (busType) { diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index f2c5ceb708..9350539373 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -57,14 +57,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { #endif JsonObject id = doc["id"]; + getStringFromJson(serverDescription, id["name"], sizeof(serverDescription)); // legacy behaviour getStringFromJson(hostName, id[F("mdns")], sizeof(hostName)); if (strlen(hostName) == 0) { mDNSenabled = false; // if no host name is set, disable mDNS - sprintf_P(hostName, PSTR("wled-%.*s"), 6, escapedMac.c_str() + 6); + prepareHostname(hostName, sizeof(hostName)-1); } - getStringFromJson(serverDescription, id["name"], sizeof(serverDescription)); #ifndef WLED_DISABLE_ALEXA getStringFromJson(alexaInvocationName, id[F("inv")], sizeof(alexaInvocationName)); #endif @@ -253,7 +253,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh - busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax); + String host = elm[F("text")] | String(); + busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, host); doInitBusses = true; // finalization done in beginStrip() if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } @@ -397,7 +398,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { 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 @@ -536,7 +537,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"]; @@ -791,9 +791,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 @@ -819,6 +842,7 @@ bool deserializeConfigFromFS() { void serializeConfigToFS() { serializeConfigSec(); + backupConfig(); // backup before writing new config DEBUG_PRINTLN(F("Writing settings to /cfg.json...")); @@ -996,6 +1020,7 @@ void serializeConfig(JsonObject root) { ins[F("freq")] = bus->getFrequency(); ins[F("maxpwr")] = bus->getMaxCurrent(); ins[F("ledma")] = bus->getLEDCurrent(); + ins[F("text")] = bus->getCustomText(); } JsonArray hw_com = hw.createNestedArray(F("com")); @@ -1060,7 +1085,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 @@ -1360,4 +1384,4 @@ void serializeConfigSec() { if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); -} \ No newline at end of file +} diff --git a/wled00/colors.cpp b/wled00/colors.cpp index da52bd4f75..bf2b69d73a 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -8,7 +8,7 @@ * color blend function, based on FastLED blend function * the calculation for each color is: result = (A*(amountOfA) + A + B*(amountOfB) + B) / 256 with amountOfA = 255 - amountOfB */ -uint32_t color_blend(uint32_t color1, uint32_t color2, uint8_t blend) { +uint32_t IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend) { // min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated (poorman's SIMD; https://github.com/wled/WLED/pull/4568#discussion_r1986587221) uint32_t rb1 = color1 & TWO_CHANNEL_MASK; // extract R & B channels from color1 @@ -64,26 +64,26 @@ uint32_t color_add(uint32_t c1, uint32_t c2, bool preserveCR) * fades color toward black * if using "video" method the resulting color will never become black unless it is already black */ - -uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) -{ +uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) { + if (c1 == 0 || amount == 0) return 0; // black or no change if (amount == 255) return c1; - if (c1 == BLACK || amount == 0) return BLACK; - uint32_t scaledcolor; // color order is: W R G B from MSB to LSB - uint32_t scale = amount; // 32bit for faster calculation uint32_t addRemains = 0; - if (!video) scale++; // add one for correct scaling using bitshifts - else { // video scaling: make sure colors do not dim to zero if they started non-zero - addRemains = R(c1) ? 0x00010000 : 0; - addRemains |= G(c1) ? 0x00000100 : 0; - addRemains |= B(c1) ? 0x00000001 : 0; - addRemains |= W(c1) ? 0x01000000 : 0; + + if (!video) amount++; // add one for correct scaling using bitshifts + else { + // video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue + uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels + uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation + uint8_t quarterMax = maxc >> 2; // note: using half of max results in color artefacts + addRemains = r && r > quarterMax ? 0x00010000 : 0; + addRemains |= g && g > quarterMax ? 0x00000100 : 0; + addRemains |= b && b > quarterMax ? 0x00000001 : 0; + addRemains |= w ? 0x01000000 : 0; } const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; - uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * scale) >> 8) & TWO_CHANNEL_MASK; // scale red and blue - uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * scale) & ~TWO_CHANNEL_MASK; // scale white and green - scaledcolor = (rb | wg) + addRemains; - return scaledcolor; + uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue + uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK; // scale white and green + return (rb | wg) + addRemains; } /* @@ -92,7 +92,7 @@ uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) note: inputs are 32bit to speed up the function, useful input value ranges are 0-255 */ uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten) { - if(rgb == 0 | hueShift + lighten + brighten == 0) return rgb; // black or no change + if (rgb == 0 | hueShift + lighten + brighten == 0) return rgb; // black or no change CHSV32 hsv; rgb2hsv(rgb, hsv); //convert to HSV hsv.h += (hueShift << 8); // shift hue (hue is 16 bits) @@ -104,8 +104,7 @@ uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_ } // 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes) -uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) -{ +uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) { if (blendType == LINEARBLEND_NOWRAP) { index = (index * 0xF0) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping } @@ -120,16 +119,16 @@ uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t else ++entry; unsigned f2 = (lo4 << 4); unsigned f1 = 256 - f2; - red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8; // note: using color_blend() is 20% slower + red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8; // note: using color_blend() is slower green1 = (green1 * f1 + (unsigned)entry->g * f2) >> 8; - blue1 = (blue1 * f1 + (unsigned)entry->b * f2) >> 8; + blue1 = (blue1 * f1 + (unsigned)entry->b * f2) >> 8; } if (brightness < 255) { // note: zero checking could be done to return black but that is hardly ever used so it is omitted - // actually color_fade(c1, brightness) + // actually same as color_fade(), using color_fade() is slower uint32_t scale = brightness + 1; // adjust for rounding (bitshift) - red1 = (red1 * scale) >> 8; // note: using color_fade() is 30% slower + red1 = (red1 * scale) >> 8; green1 = (green1 * scale) >> 8; - blue1 = (blue1 * scale) >> 8; + blue1 = (blue1 * scale) >> 8; } return RGBW32(red1,green1,blue1,0); } @@ -589,10 +588,13 @@ uint8_t NeoGammaWLEDMethod::gammaT_inv[256]; void NeoGammaWLEDMethod::calcGammaTable(float gamma) { float gamma_inv = 1.0f / gamma; // inverse gamma - for (size_t i = 0; i < 256; i++) { + for (size_t i = 1; i < 256; i++) { gammaT[i] = (int)(powf((float)i / 255.0f, gamma) * 255.0f + 0.5f); - gammaT_inv[i] = (int)(powf((float)i / 255.0f, gamma_inv) * 255.0f + 0.5f); + gammaT_inv[i] = (int)(powf(((float)i - 0.5f) / 255.0f, gamma_inv) * 255.0f + 0.5f); + //DEBUG_PRINTF_P(PSTR("gammaT[%d] = %d gammaT_inv[%d] = %d\n"), i, gammaT[i], i, gammaT_inv[i]); } + gammaT[0] = 0; + gammaT_inv[0] = 0; } uint8_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct(uint8_t value) @@ -601,21 +603,6 @@ uint8_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct(uint8_t value) return gammaT[value]; } -// used for color gamma correction -uint32_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct32(uint32_t color) -{ - if (!gammaCorrectCol) return color; - uint8_t w = W(color); - uint8_t r = R(color); - uint8_t g = G(color); - uint8_t b = B(color); - w = gammaT[w]; - r = gammaT[r]; - g = gammaT[g]; - b = gammaT[b]; - return RGBW32(r, g, b, w); -} - uint32_t IRAM_ATTR_YN NeoGammaWLEDMethod::inverseGamma32(uint32_t color) { if (!gammaCorrectCol) return color; diff --git a/wled00/colors.h b/wled00/colors.h new file mode 100644 index 0000000000..376959fd65 --- /dev/null +++ b/wled00/colors.h @@ -0,0 +1,144 @@ +#pragma once +#ifndef WLED_COLORS_H +#define WLED_COLORS_H + +/* + * Color structs and color utility functions + */ +#include +#include "FastLED.h" + +#define ColorFromPalette ColorFromPaletteWLED // override fastled version + +// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color +// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts +// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB +struct CRGBW { + union { + uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB) + struct { + uint8_t b; + uint8_t g; + uint8_t r; + uint8_t w; + }; + uint8_t raw[4]; // Access as an array in the order B, G, R, W + }; + + // Default constructor + inline CRGBW() __attribute__((always_inline)) = default; + + // Constructor from a 32-bit color (0xWWRRGGBB) + constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {} + + // Constructor with r, g, b, w values + constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {} + + // Constructor from CRGB + constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {} + + // Access as an array + inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; } + + // Assignment from 32-bit color + inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; } + + // Assignment from r, g, b, w + inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; } + + // Conversion operator to uint32_t + inline operator uint32_t() const __attribute__((always_inline)) { + return color32; + } + /* + // Conversion operator to CRGB + inline operator CRGB() const __attribute__((always_inline)) { + return CRGB(r, g, b); + } + + CRGBW& scale32 (uint8_t scaledown) // 32bit math + { + if (color32 == 0) return *this; // 2 extra instructions, worth it if called a lot on black (which probably is true) adding check if scaledown is zero adds much more overhead as its 8bit + uint32_t scale = scaledown + 1; + uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue + uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green + color32 = rb | wg; + return *this; + }*/ + +}; + +struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions + union { + struct { + uint16_t h; // hue + uint8_t s; // saturation + uint8_t v; // value + }; + uint32_t raw; // 32bit access + }; + inline CHSV32() __attribute__((always_inline)) = default; // default constructor + + /// Allow construction from hue, saturation, and value + /// @param ih input hue + /// @param is input saturation + /// @param iv input value + inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v + : h(ih), s(is), v(iv) {} + inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v + : h((uint16_t)ih << 8), s(is), v(iv) {} + inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV + : h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {} + inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV +}; +extern bool gammaCorrectCol; +// similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) +class NeoGammaWLEDMethod { + public: + [[gnu::hot]] static uint8_t Correct(uint8_t value); // apply Gamma to single channel + [[gnu::hot]] static uint32_t inverseGamma32(uint32_t color); // apply inverse Gamma to RGBW32 color + static void calcGammaTable(float gamma); // re-calculates & fills gamma tables + static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; } // get value from Gamma table (WLED specific, not used by NPB) + static inline uint8_t rawInverseGamma8(uint8_t val) { return gammaT_inv[val]; } // get value from inverse Gamma table (WLED specific, not used by NPB) + static inline uint32_t Correct32(uint32_t color) { // apply Gamma to RGBW32 color (WLED specific, not used by NPB) + if (!gammaCorrectCol) return color; // no gamma correction + uint8_t w = byte(color>>24), r = byte(color>>16), g = byte(color>>8), b = byte(color); // extract r, g, b, w channels + w = gammaT[w]; r = gammaT[r]; g = gammaT[g]; b = gammaT[b]; + return (uint32_t(w) << 24) | (uint32_t(r) << 16) | (uint32_t(g) << 8) | uint32_t(b); + } + private: + static uint8_t gammaT[]; + static uint8_t gammaT_inv[]; +}; +#define gamma32(c) NeoGammaWLEDMethod::Correct32(c) +#define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) +#define gamma32inv(c) NeoGammaWLEDMethod::inverseGamma32(c) +#define gamma8inv(c) NeoGammaWLEDMethod::rawInverseGamma8(c) +[[gnu::hot, gnu::pure]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend); +inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); }; +[[gnu::hot, gnu::pure]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false); +[[gnu::hot, gnu::pure]] uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten); +[[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); +CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); +CRGBPalette16 generateRandomPalette(); +void loadCustomPalettes(); +extern std::vector customPalettes; +inline size_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } +inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } +void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); +void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); +void rgb2hsv(const uint32_t rgb, CHSV32& hsv); +inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 16) | (byte(c.g) << 8) | (byte(c.b)))), hsv); return CHSV(hsv); } // CRGB to hsv +void colorKtoRGB(uint16_t kelvin, byte* rgb); +void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb +void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO +void colorRGBtoXY(const byte* rgb, float* xy); // only defined if huesync disabled TODO +void colorFromDecOrHexString(byte* rgb, const char* in); +bool colorFromHexString(byte* rgb, const char* in); +uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); +uint16_t approximateKelvinFromRGB(uint32_t rgb); +void setRandomColor(byte* rgb); + +[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false); + +#endif diff --git a/wled00/data/common.js b/wled00/data/common.js index 658346e754..5a98b4fe1f 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -13,7 +13,7 @@ function isN(n) { return !isNaN(parseFloat(n)) && isFinite(n); } // isNumber // https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer function isF(n) { return n === +n && n !== (n|0); } // isFloat function isI(n) { return n === +n && n === (n|0); } // isInteger -function toggle(el) { gId(el).classList.toggle("hide"); gId('No'+el).classList.toggle("hide"); } +function toggle(el) { gId(el).classList.toggle("hide"); let n = gId('No'+el); if (n) n.classList.toggle("hide"); } function tooltip(cont=null) { d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ element.addEventListener("pointerover", ()=>{ diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 3716f7ccd8..22f1987e93 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -329,7 +329,7 @@
- +

diff --git a/wled00/data/index.js b/wled00/data/index.js index f23b5949fd..fe154783d0 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -685,6 +685,7 @@ function parseInfo(i) { gId("filter2D").classList.remove('hide'); gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='';}); } + gId("updBt").style.display = (i.opt & 1) ? '':'none'; // if (i.noaudio) { // gId("filterVol").classList.add("hide"); // gId("filterFreq").classList.add("hide"); diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index f678df8f3b..928da11753 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -284,6 +284,8 @@ gId("dig"+n+"l").style.display = (isD2P(t) || isPWM(t)) ? "inline":"none"; // bus clock speed / PWM speed (relative) (not On/Off) gId("rev"+n).innerHTML = isAna(t) ? "Inverted output":"Reversed"; // change reverse text for analog else (rotated 180°) //gId("psd"+n).innerHTML = isAna(t) ? "Index:":"Start:"; // change analog start description + gId("net"+n+"h").style.display = isNet(t) && !is8266() ? "block" : "none"; // show host field for network types except on ESP8266 + if (!isNet(t) || is8266()) d.Sf["HS"+n].value = ""; // cleart host field if not network type or ESP8266 }); // display global white channel overrides gId("wc").style.display = (gRGBW) ? 'inline':'none'; @@ -466,6 +468,7 @@ +
Host: .local

Reversed:

Skip first LEDs:

Off Refresh:
@@ -905,7 +908,6 @@

Advanced


Use harmonic Random Cycle palette:
- Use "rainbow" color wheel:
Target refresh rate: FPS diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index 7f46270495..15104baa53 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -53,13 +53,13 @@

Security & Update setup

Factory reset:
All settings and presets will be erased.

⚠ Unencrypted transmission. An attacker on the same network can intercept form data!
-
+

Software Update


Enable ArduinoOTA:
Only allow update from same network/WiFi:
⚠ If you are using multiple VLANs (i.e. IoT or guest network) either set PIN or disable this option.
- Disabling this option will make your device less secure.

+ Disabling this option will make your device less secure.

Backup & Restore

⚠ Restoring presets/configuration will OVERWRITE your current presets/configuration.
@@ -76,7 +76,7 @@

About

A huge thank you to everyone who helped me create WLED!

(c) 2016-2024 Christian Schwinne
Licensed under the EUPL v1.2 license

- Server message: Response error!
+ Installed version: WLED ##VERSION##
diff --git a/wled00/data/style.css b/wled00/data/style.css index d33f69341e..005e4ecbbb 100644 --- a/wled00/data/style.css +++ b/wled00/data/style.css @@ -79,6 +79,9 @@ input { input:disabled { color: #888; } +input:invalid { + color: #f00; +} input[type="text"], input[type="number"], input[type="password"], @@ -202,4 +205,4 @@ td { #btns select { width: 144px; } -} \ No newline at end of file +} diff --git a/wled00/data/update.htm b/wled00/data/update.htm index 8b39b1ccef..ef923920b7 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -27,7 +27,7 @@

WLED Software Update

- Installed version: ##VERSION##
+ Installed version: WLED ##VERSION##
Download the latest binary:
diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 98cfe28fb0..4d7c7b666c 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -191,7 +191,7 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 // only change brightness if value changed if (bri != e131_data[dataOffset]) { bri = e131_data[dataOffset]; - strip.setBrightness(scaledBri(bri), false); + strip.setBrightness(bri, false); stateUpdated(CALL_MODE_WS_SEND); } return; diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 56c6aba499..91d8e6259b 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -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(); @@ -69,133 +73,6 @@ typedef struct WiFiConfig { } } wifi_config; -//colors.cpp -#define ColorFromPalette ColorFromPaletteWLED // override fastled version - -// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color -// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts -// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB -struct CRGBW { - union { - uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB) - struct { - uint8_t b; - uint8_t g; - uint8_t r; - uint8_t w; - }; - uint8_t raw[4]; // Access as an array in the order B, G, R, W - }; - - // Default constructor - inline CRGBW() __attribute__((always_inline)) = default; - - // Constructor from a 32-bit color (0xWWRRGGBB) - constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {} - - // Constructor with r, g, b, w values - constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {} - - // Constructor from CRGB - constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {} - - // Access as an array - inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; } - - // Assignment from 32-bit color - inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; } - - // Assignment from r, g, b, w - inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; } - - // Conversion operator to uint32_t - inline operator uint32_t() const __attribute__((always_inline)) { - return color32; - } - /* - // Conversion operator to CRGB - inline operator CRGB() const __attribute__((always_inline)) { - return CRGB(r, g, b); - } - - CRGBW& scale32 (uint8_t scaledown) // 32bit math - { - if (color32 == 0) return *this; // 2 extra instructions, worth it if called a lot on black (which probably is true) adding check if scaledown is zero adds much more overhead as its 8bit - uint32_t scale = scaledown + 1; - uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue - uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green - color32 = rb | wg; - return *this; - }*/ - -}; - -struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions - union { - struct { - uint16_t h; // hue - uint8_t s; // saturation - uint8_t v; // value - }; - uint32_t raw; // 32bit access - }; - inline CHSV32() __attribute__((always_inline)) = default; // default constructor - - /// Allow construction from hue, saturation, and value - /// @param ih input hue - /// @param is input saturation - /// @param iv input value - inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v - : h(ih), s(is), v(iv) {} - inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v - : h((uint16_t)ih << 8), s(is), v(iv) {} - inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV - : h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {} - inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV -}; -// similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) -class NeoGammaWLEDMethod { - public: - [[gnu::hot]] static uint8_t Correct(uint8_t value); // apply Gamma to single channel - [[gnu::hot]] static uint32_t Correct32(uint32_t color); // apply Gamma to RGBW32 color (WLED specific, not used by NPB) - [[gnu::hot]] static uint32_t inverseGamma32(uint32_t color); // apply inverse Gamma to RGBW32 color - static void calcGammaTable(float gamma); // re-calculates & fills gamma tables - static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; } // get value from Gamma table (WLED specific, not used by NPB) - static inline uint8_t rawInverseGamma8(uint8_t val) { return gammaT_inv[val]; } // get value from inverse Gamma table (WLED specific, not used by NPB) - private: - static uint8_t gammaT[]; - static uint8_t gammaT_inv[]; -}; -#define gamma32(c) NeoGammaWLEDMethod::Correct32(c) -#define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) -#define gamma32inv(c) NeoGammaWLEDMethod::inverseGamma32(c) -#define gamma8inv(c) NeoGammaWLEDMethod::rawInverseGamma8(c) -[[gnu::hot, gnu::pure]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend); -inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); }; -[[gnu::hot, gnu::pure]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false); -[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); -[[gnu::hot, gnu::pure]] uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten); -[[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); -CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); -CRGBPalette16 generateRandomPalette(); -void loadCustomPalettes(); -extern std::vector customPalettes; -inline size_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } -inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } -void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); -void rgb2hsv(const uint32_t rgb, CHSV32& hsv); -inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 16) | (byte(c.g) << 8) | (byte(c.b)))), hsv); return CHSV(hsv); } // CRGB to hsv -void colorKtoRGB(uint16_t kelvin, byte* rgb); -void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb -void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO -void colorRGBtoXY(const byte* rgb, float* xy); // only defined if huesync disabled TODO -void colorFromDecOrHexString(byte* rgb, const char* in); -bool colorFromHexString(byte* rgb, const char* in); -uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); -uint16_t approximateKelvinFromRGB(uint32_t rgb); -void setRandomColor(byte* rgb); - //dmx_output.cpp void initDMXOutput(); void handleDMXOutput(); @@ -223,6 +100,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(); @@ -507,6 +389,7 @@ size_t printSetFormValue(Print& settingsScript, const char* key, int val); size_t printSetFormValue(Print& settingsScript, const char* key, const char* val); size_t printSetFormIndex(Print& settingsScript, const char* key, int index); size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val); +void prepareHostname(char* hostname, size_t maxLen = 32); [[gnu::pure]] bool isAsterisksOnly(const char* str, byte maxLen); bool requestJSONBufferLock(uint8_t moduleID=255); void releaseJSONBufferLock(); @@ -580,6 +463,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 { diff --git a/wled00/file.cpp b/wled00/file.cpp index c1960e616c..108c41bd44 100644 --- a/wled00/file.cpp +++ b/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(); + } +} + diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index aa4ae2e161..691ede1ac5 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -58,7 +58,7 @@ void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t // set multiple pixels if upscaling for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) { for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) { - activeSeg->setPixelColorXY(outX + i, outY + j, gamma8(red), gamma8(green), gamma8(blue)); + activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue); } } } diff --git a/wled00/json.cpp b/wled00/json.cpp index 4414681023..e8ebaaba29 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -312,7 +312,7 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) jsonTransitionOnce = true; if (seg.isInTransition()) seg.startTransition(0); // setting transition time to 0 will stop transition in next frame strip.setTransition(0); - strip.setBrightness(scaledBri(bri), true); + strip.setBrightness(bri, true); // freeze and init to black if (!seg.freeze) { diff --git a/wled00/led.cpp b/wled00/led.cpp index 43771f9d53..35f5003679 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -57,7 +57,7 @@ void toggleOnOff() //scales the brightness with the briMultiplier factor byte scaledBri(byte in) { - unsigned val = ((uint16_t)in*briMultiplier)/100; + unsigned val = ((unsigned)in*briMultiplier)/100; if (val > 255) val = 255; return (byte)val; } @@ -68,7 +68,7 @@ void applyBri() { if (realtimeOverride || !(realtimeMode && arlsForceMaxBri)) { //DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); - strip.setBrightness(scaledBri(briT)); + strip.setBrightness(briT); } } diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 62f2961047..2b62c480c3 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -11,6 +11,11 @@ #warning "MQTT topics length > 32 is not recommended for compatibility with usermods!" #endif +static const char* sTopicFormat PROGMEM = "%.*s/%s"; + +// parse payload for brightness, ON/OFF or toggle +// briLast is used to remember last brightness value in case of ON/OFF or toggle +// bri is set to 0 if payload is "0" or "OFF" or "false" static void parseMQTTBriPayload(char* payload) { if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);} @@ -30,22 +35,18 @@ static void onMqttConnect(bool sessionPresent) char subuf[MQTT_MAX_TOPIC_LEN + 9]; if (mqttDeviceTopic[0] != 0) { - strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); - mqtt->subscribe(subuf, 0); - strcat_P(subuf, PSTR("/col")); + mqtt->subscribe(mqttDeviceTopic, 0); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "col"); mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); - strcat_P(subuf, PSTR("/api")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "api"); mqtt->subscribe(subuf, 0); } if (mqttGroupTopic[0] != 0) { - strlcpy(subuf, mqttGroupTopic, MQTT_MAX_TOPIC_LEN + 1); + mqtt->subscribe(mqttGroupTopic, 0); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttGroupTopic, "col"); mqtt->subscribe(subuf, 0); - strcat_P(subuf, PSTR("/col")); - mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttGroupTopic, MQTT_MAX_TOPIC_LEN + 1); - strcat_P(subuf, PSTR("/api")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttGroupTopic, "api"); mqtt->subscribe(subuf, 0); } @@ -54,8 +55,7 @@ static void onMqttConnect(bool sessionPresent) DEBUG_PRINTLN(F("MQTT ready")); #ifndef USERMOD_SMARTNEST - strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); - strcat_P(subuf, PSTR("/status")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "status"); mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT #endif @@ -136,7 +136,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } // Print adapter for flat buffers -namespace { +namespace { class bufferPrint : public Print { char* _buf; size_t _size, _offset; @@ -172,21 +172,21 @@ void publishMqtt() char subuf[MQTT_MAX_TOPIC_LEN + 16]; sprintf_P(s, PSTR("%u"), bri); - strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); - strcat_P(subuf, PSTR("/g")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "g"); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) sprintf_P(s, PSTR("#%06X"), (colPri[3] << 24) | (colPri[0] << 16) | (colPri[1] << 8) | (colPri[2])); - strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); - strcat_P(subuf, PSTR("/c")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "c"); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "status"); + mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT + // TODO: use a DynamicBufferList. Requires a list-read-capable MQTT client API. DynamicBuffer buf(1024); bufferPrint pbuf(buf.data(), buf.size()); XML_response(pbuf); - strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); - strcat_P(subuf, PSTR("/v")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "v"); mqtt->publish(subuf, 0, retainMqttMsg, buf.data(), pbuf.size()); // optionally retain message (#2263) #endif } @@ -213,14 +213,26 @@ bool initMqtt() { mqtt->setServer(mqttIP, mqttPort); } else { - mqtt->setServer(mqttServer, mqttPort); + #ifdef ARDUINO_ARCH_ESP32 + String mqttMDNS = mqttServer; + mqttMDNS.toLowerCase(); // make sure we have a lowercase hostname + int pos = mqttMDNS.indexOf(F(".local")); + if (pos > 0) mqttMDNS.remove(pos); // remove .local domain if present (and anything following it) + if (mDNSenabled && mqttMDNS.length() > 0 && mqttMDNS.indexOf('.') < 0) { // if mDNS is enabled and server does not have domain + mqttIP = MDNS.queryHost(mqttMDNS.c_str()); + if (mqttIP != IPAddress()) // if MDNS resolved the hostname + mqtt->setServer(mqttIP, mqttPort); + else + mqtt->setServer(mqttServer, mqttPort); + } else + #endif + mqtt->setServer(mqttServer, mqttPort); } mqtt->setClientId(mqttClientID); if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); #ifndef USERMOD_SMARTNEST - strlcpy(mqttStatusTopic, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); - strcat_P(mqttStatusTopic, PSTR("/status")); + snprintf_P(mqttStatusTopic, sizeof(mqttStatusTopic)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "status"); mqtt->setWill(mqttStatusTopic, 0, true, "offline"); // LWT message #endif mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME); diff --git a/wled00/set.cpp b/wled00/set.cpp index e9dc3aa65d..a933b5be22 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -64,7 +64,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } strlcpy(hostName, request->arg(F("CM")).c_str(), sizeof(hostName)); - if (strlen(hostName) == 0) sprintf_P(hostName, PSTR("wled-%.*s"), 6, escapedMac.c_str() + 6); // hostname must not be empty + if (strlen(hostName) == 0) prepareHostname(hostName, sizeof(hostName)-1); #ifdef ARDUINO_ARCH_ESP32 #ifdef WLED_USE_ETHERNET ETH.setHostname(hostName); @@ -158,6 +158,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed; unsigned length, start, maMax; uint8_t pins[5] = {255, 255, 255, 255, 255}; + String text; // this will set global ABL max current used when per-port ABL is not used unsigned ablMilliampsMax = request->arg(F("MA")).toInt(); @@ -191,6 +192,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM) char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED mA char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max mA + char hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others) if (!request->hasArg(lp)) { DEBUG_PRINTF_P(PSTR("# of buses: %d\n"), s+1); break; @@ -241,9 +243,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) maMax = request->arg(ma).toInt() * request->hasArg(F("PPL")); // if PP-ABL is disabled maMax (per bus) must be 0 } type |= request->hasArg(rf) << 7; // off refresh override + text = request->arg(hs).substring(0,31); // actual finalization is done in WLED::loop() (removing old busses and adding new) // this may happen even before this loop is finished so we do "doInitBusses" after the loop - busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax); + busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, text); busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed @@ -365,7 +368,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(); diff --git a/wled00/src/dependencies/network/Network.cpp b/wled00/src/dependencies/network/Network.cpp index d86bf127fd..dbc2707887 100644 --- a/wled00/src/dependencies/network/Network.cpp +++ b/wled00/src/dependencies/network/Network.cpp @@ -73,17 +73,13 @@ void NetworkClass::localMAC(uint8_t* MAC) bool NetworkClass::isConnected() { -#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED) || ETH.localIP()[0] != 0; -#else - return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED); -#endif + return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED) || isEthernet(); } bool NetworkClass::isEthernet() { #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - return (ETH.localIP()[0] != 0); + return (ETH.localIP()[0] != 0) && ETH.linkUp(); #endif return false; } diff --git a/wled00/udp.cpp b/wled00/udp.cpp index bdb60c363a..8e0f654a86 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -424,7 +424,7 @@ void realtimeLock(uint32_t timeoutMs, byte md) } // if strip is off (bri==0) and not already in RTM if (briT == 0) { - strip.setBrightness(scaledBri(briLast), true); + strip.setBrightness(briLast, true); } } @@ -434,14 +434,14 @@ void realtimeLock(uint32_t timeoutMs, byte md) realtimeMode = md; if (realtimeOverride) return; - if (arlsForceMaxBri) strip.setBrightness(scaledBri(255), true); + if (arlsForceMaxBri) strip.setBrightness(255, true); if (briT > 0 && md == REALTIME_MODE_GENERIC) strip.show(); } void exitRealtime() { if (!realtimeMode) return; if (realtimeOverride == REALTIME_OVERRIDE_ONCE) realtimeOverride = REALTIME_OVERRIDE_NONE; - strip.setBrightness(scaledBri(bri), true); + strip.setBrightness(bri, true); realtimeTimeout = 0; // cancel realtime mode immediately realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately realtimeIP[0] = 0; diff --git a/wled00/util.cpp b/wled00/util.cpp index 2a3808d200..1306e9a202 100644 --- a/wled00/util.cpp +++ b/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 +#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 @@ -113,6 +123,30 @@ size_t printSetClassElementHTML(Print& settingsScript, const char* key, const in return settingsScript.printf_P(PSTR("d.getElementsByClassName(\"%s\")[%d].innerHTML=\"%s\";"), key, index, val); } +// prepare a unique hostname based on the last 6 digits of the MAC address +// if no mDNS name or serverDescription is set, otherwise use cmDNS or serverDescription +// the hostname will be at most 24 characters long, starting with "wled-" +// and containing only alphanumeric characters and hyphens +// the hostname will not end with a hyphen and will be null-terminated +void prepareHostname(char* hostname, size_t maxLen) +{ + // create a unique hostname based on the last 6 digits of the MAC address if no serverDescription is set + sprintf_P(hostname, PSTR("wled-%*s"), 6, escapedMac.c_str() + 6); + const char *pC = serverDescription; // use serverDescription, this method is not called if hostName is set + unsigned pos = 5; // keep "wled-" from unique name + while (*pC && pos < maxLen) { // while !null and not over length + if (isalnum(*pC)) { // if the current char is alpha-numeric append it to the hostname + hostname[pos++] = *pC; + } else if (*pC == ' ' || *pC == '_' || *pC == '-' || *pC == '+' || *pC == '!' || *pC == '?' || *pC == '*') { + hostname[pos++] = '-'; + } + // else do nothing - no leading hyphens and do not include hyphens for all other characters. + pC++; + } + // last character must not be hyphen + while (pos > 4 && hostname[pos-1] == '-') pos--; + hostname[pos] = '\0'; // terminate string (leave at least "wled") +} bool isAsterisksOnly(const char* str, byte maxLen) { @@ -681,6 +715,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 diff --git a/wled00/wled.cpp b/wled00/wled.cpp index b800d18d6a..8cac510f09 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -1,7 +1,10 @@ #define WLED_DEFINE_GLOBAL_VARS //only in one source file, wled.cpp! #include "wled.h" #include "wled_ethernet.h" -#include +#ifdef WLED_ENABLE_AOTA + #define NO_OTA_PORT + #include +#endif #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET) #include "soc/soc.h" @@ -105,8 +108,8 @@ void WLED::loop() if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) // block stuff if WARLS/Adalight is enabled { if (apActive) dnsServer.processNextRequest(); - #ifndef WLED_DISABLE_OTA - if (WLED_CONNECTED && aOtaEnabled && !otaLock && correctPIN) ArduinoOTA.handle(); + #ifdef WLED_ENABLE_AOTA + if (Network.isConnected() && aOtaEnabled && !otaLock && correctPIN) ArduinoOTA.handle(); #endif handleNightlight(); yield(); @@ -187,12 +190,10 @@ void WLED::loop() doInitBusses = false; DEBUG_PRINTLN(F("Re-init busses.")); bool aligned = strip.checkSegmentAlignment(); //see if old segments match old bus(ses) - BusManager::removeAll(); strip.finalizeInit(); // will create buses and also load default ledmap if present - BusManager::setBrightness(bri); // fix re-initialised bus' brightness #4005 if (aligned) strip.makeAutoSegments(); else strip.fixInvalidSegments(); - BusManager::setBrightness(bri); // fix re-initialised bus' brightness + BusManager::setBrightness(scaledBri(bri)); // fix re-initialised bus' brightness #4005 and #4824 configNeedsWrite = true; } if (loadLedmap >= 0) { @@ -407,6 +408,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 @@ -420,10 +424,15 @@ void WLED::setup() escapedMac.toLowerCase(); // generate host name if no compile time default is set - if (strcmp(hostName, DEFAULT_MDNS_NAME) == 0) sprintf_P(hostName, PSTR("wled-%.*s"), 6, escapedMac.c_str() + 6); + if (strcmp(hostName, DEFAULT_MDNS_NAME) == 0) prepareHostname(hostName, sizeof(hostName)-1); 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()); @@ -471,7 +480,7 @@ void WLED::setup() if (mqttClientID[0] == 0) sprintf_P(mqttClientID, PSTR("WLED-%*s"), 6, escapedMac.c_str() + 6); #endif -#ifndef WLED_DISABLE_OTA +#ifdef WLED_ENABLE_AOTA if (aOtaEnabled) { ArduinoOTA.onStart([]() { #ifdef ESP8266 @@ -696,9 +705,8 @@ void WLED::initInterfaces() alexaInit(); #endif -#ifndef WLED_DISABLE_OTA - if (aOtaEnabled) - ArduinoOTA.begin(); +#ifdef WLED_ENABLE_AOTA + if (aOtaEnabled) ArduinoOTA.begin(); #endif // Set up mDNS responder: @@ -769,7 +777,7 @@ void WLED::handleConnection() if (stac != stacO) { stacO = stac; DEBUG_PRINTF_P(PSTR("Connected AP clients: %d\n"), (int)stac); - if (!WLED_CONNECTED && wifiConfigured) { // trying to connect, but not connected + if (!Network.isConnected() && wifiConfigured) { // trying to connect, but not connected if (stac) WiFi.disconnect(); // disable search so that AP can work else @@ -844,7 +852,7 @@ void WLED::handleConnection() } // If status LED pin is allocated for other uses, does nothing -// else blink at 1Hz when WLED_CONNECTED is false (no WiFi, ?? no Ethernet ??) +// else blink at 1Hz when Network.isConnected() is false (no WiFi, ?? no Ethernet ??) // else blink at 2Hz when MQTT is enabled but not connected // else turn the status LED off #if defined(STATUSLED) @@ -858,7 +866,7 @@ void WLED::handleStatusLED() } #endif - if (WLED_CONNECTED) { + if (Network.isConnected()) { c = RGBW32(0,255,0,0); ledStatusType = 2; } else if (WLED_MQTT_CONNECTED) { diff --git a/wled00/wled.h b/wled00/wled.h index d0c506858a..d9261dbc8d 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -21,6 +21,12 @@ // You are required to disable over-the-air updates: //#define WLED_DISABLE_OTA // saves 14kb +#ifdef WLED_ENABLE_AOTA + #if defined(WLED_DISABLE_OTA) + #warning WLED_DISABLE_OTA was defined but it will be ignored due to WLED_ENABLE_AOTA. + #endif + #undef WLED_DISABLE_OTA +#endif // You can choose some of these features to disable: //#define WLED_DISABLE_ALEXA // saves 11kb @@ -121,10 +127,6 @@ #endif #include #include -#ifndef WLED_DISABLE_OTA - #define NO_OTA_PORT - #include -#endif #include #include "src/dependencies/time/TimeLib.h" #include "src/dependencies/timezone/Timezone.h" @@ -192,6 +194,7 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #include "fcn_declare.h" #include "NodeStruct.h" #include "pin_manager.h" +#include "colors.h" #include "bus_manager.h" #include "FX.h" @@ -589,7 +592,7 @@ WLED_GLOBAL bool otaLock _INIT(true); // prevents OTA firmware update WLED_GLOBAL bool otaLock _INIT(false); // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks #endif WLED_GLOBAL bool wifiLock _INIT(false); // prevents access to WiFi settings when OTA lock is enabled -#ifndef WLED_DISABLE_OTA +#ifdef WLED_ENABLE_AOTA WLED_GLOBAL bool aOtaEnabled _INIT(true); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on #else WLED_GLOBAL bool aOtaEnabled _INIT(false); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on @@ -624,7 +627,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); @@ -1025,11 +1027,7 @@ WLED_GLOBAL volatile uint8_t jsonBufferLock _INIT(0); WLED_GLOBAL unsigned loops _INIT(0); #endif -#ifdef ARDUINO_ARCH_ESP32 - #define WLED_CONNECTED (WiFi.status() == WL_CONNECTED || ETH.localIP()[0] != 0) -#else - #define WLED_CONNECTED (WiFi.status() == WL_CONNECTED) -#endif +#define WLED_CONNECTED (Network.isConnected()) #ifndef WLED_AP_SSID_UNIQUE #define WLED_SET_AP_SSID() do { \ diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index a80e7a0c25..f69dc91690 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -1,9 +1,11 @@ #include "wled.h" -#ifdef ESP8266 - #include -#else - #include +#ifndef WLED_DISABLE_OTA + #ifdef ESP8266 + #include + #else + #include + #endif #endif #include "html_ui.h" #include "html_settings.h" @@ -387,6 +389,7 @@ void initServer() createEditHandler(correctPIN); static const char _update[] PROGMEM = "/update"; +#ifndef WLED_DISABLE_OTA //init ota page server.on(_update, HTTP_GET, [](AsyncWebServerRequest *request){ if (otaLock) { @@ -408,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){ @@ -426,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); @@ -446,14 +453,17 @@ void initServer() } } }); +#else + const auto notSupported = [](AsyncWebServerRequest *request){ + serveMessage(request, 501, FPSTR(s_notimplemented), F("This build does not support OTA update."), 254); + }; + server.on(_update, HTTP_GET, notSupported); + server.on(_update, HTTP_POST, notSupported, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){}); +#endif #ifdef WLED_ENABLE_DMX server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ - request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap , dmxProcessor); - }); -#else - server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ - serveMessage(request, 501, FPSTR(s_notimplemented), F("DMX support is not enabled in this build."), 254); + request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap, dmxProcessor); }); #endif @@ -657,6 +667,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { case SUBPAGE_DMX : content = PAGE_settings_dmx; len = PAGE_settings_dmx_length; break; #endif case SUBPAGE_UM : content = PAGE_settings_um; len = PAGE_settings_um_length; break; +#ifndef WLED_DISABLE_OTA case SUBPAGE_UPDATE : content = PAGE_update; len = PAGE_update_length; #ifdef ARDUINO_ARCH_ESP32 if (request->hasArg(F("revert")) && inLocalSubnet(request->client()->remoteIP()) && Update.canRollBack()) { @@ -670,6 +681,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { } #endif break; +#endif #ifndef WLED_DISABLE_2D case SUBPAGE_2D : content = PAGE_settings_2D; len = PAGE_settings_2D_length; break; #endif diff --git a/wled00/xml.cpp b/wled00/xml.cpp index bedecb731a..7c0be8fe0f 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -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 pins = obj[key].as(); 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)
\\\"%s\\\"
(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); @@ -312,6 +330,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED current char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max per-port PSU current + char hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others) settingsScript.print(F("addLEDs(1);")); uint8_t pins[5]; int nPins = bus->getPins(pins); @@ -351,6 +370,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormValue(settingsScript,sp,speed); printSetFormValue(settingsScript,la,bus->getLEDCurrent()); printSetFormValue(settingsScript,ma,bus->getMaxCurrent()); + printSetFormValue(settingsScript,hs,bus->getCustomText().c_str()); sumMa += bus->getMaxCurrent(); } printSetFormValue(settingsScript,PSTR("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); @@ -381,7 +401,6 @@ 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); @@ -594,11 +613,14 @@ 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); - #ifdef WLED_DISABLE_OTA //hide settings if not compiled + #ifdef WLED_DISABLE_OTA + settingsScript.print(F("toggle('OTA');")); // hide update section + #endif + #ifndef WLED_ENABLE_AOTA settingsScript.print(F("toggle('aOTA');")); // hide ArduinoOTA checkbox #endif } @@ -653,16 +675,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
%s
(%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