Compare commits

..

9 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
7f6778a126 Add support for custom error message strings in ErrorLogEntry
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-10-04 05:28:15 +00:00
copilot-swe-agent[bot]
7d79c52e20 Add error log title and restore color-coded error messages
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-28 14:00:47 +00:00
copilot-swe-agent[bot]
7ce7c93bd7 Update error log styling - full width and text wrapping enabled
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-28 13:25:33 +00:00
copilot-swe-agent[bot]
4b6df49e90 Refactor error logging system - move from global to local scope, use shorter JSON command
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-28 13:17:00 +00:00
copilot-swe-agent[bot]
756697ae78 Improve error log formatting - remove colored lines, center align area, 24h timestamps, larger font
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-28 11:00:20 +00:00
copilot-swe-agent[bot]
61b11e185c Implement server-side error logging system with client-side timestamp calculation
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-28 06:32:01 +00:00
copilot-swe-agent[bot]
cc827c8235 Fix error logging system based on feedback - restore detailed toasts, fix positioning and icon
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-27 10:26:41 +00:00
copilot-swe-agent[bot]
07f408e53d Implement comprehensive error logging system with user-accessible error log
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-24 19:34:46 +00:00
copilot-swe-agent[bot]
c03ced5539 Initial plan 2025-09-24 19:23:01 +00:00
16 changed files with 336 additions and 638 deletions

View File

@@ -30,27 +30,6 @@ The build has two main phases:
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`
- List all targets: `pio run --list-targets`
## Before Finishing Work
**CRITICAL: You MUST complete ALL of these steps before marking your work as complete:**
1. **Run the test suite**: `npm test` -- Set timeout to 2+ minutes. NEVER CANCEL.
- All tests MUST pass
- If tests fail, fix the issue before proceeding
2. **Build at least one hardware environment**: `pio run -e esp32dev` -- Set timeout to 30+ minutes. NEVER CANCEL.
- Choose `esp32dev` as it's a common, representative environment
- See "Hardware Compilation" section above for the full list of common environments
- The build MUST complete successfully without errors
- If the build fails, fix the issue before proceeding
- **DO NOT skip this step** - it validates that firmware compiles with your changes
3. **For web UI changes only**: Manually test the interface
- See "Manual Testing Scenarios" section below
- Verify the UI loads and functions correctly
**If any of these validation steps fail, you MUST fix the issues before finishing. Do NOT mark work as complete with failing builds or tests.**
## Validation and Testing
### Web UI Testing
@@ -65,7 +44,7 @@ The build has two main phases:
- **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`
- **MANDATORY: Always run a hardware build before finishing** (see "Before Finishing Work" section below)
- **Always run a build for the common environment before finishing**
### Manual Testing Scenarios
After making changes to web UI, always test:
@@ -121,16 +100,10 @@ package.json # Node.js dependencies and scripts
## Build Timing and Timeouts
**IMPORTANT: Use these timeout values when running builds:**
- **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum
- **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum
- **Hardware builds** (`pio run -e [target]`): 15-20 minutes typical for first build - Set timeout to 1800 seconds (30 minutes) minimum
- Subsequent builds are faster due to caching
- First builds download toolchains and dependencies which takes significant time
- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation require patience
**When validating your changes before finishing, you MUST wait for the hardware build to complete successfully. Set the timeout appropriately and be patient.**
- **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
@@ -156,17 +129,11 @@ package.json # Node.js dependencies and scripts
- **Hardware builds require appropriate ESP32/ESP8266 development board**
## CI/CD Pipeline
**The GitHub Actions CI workflow will:**
The GitHub Actions workflow:
1. Installs Node.js and Python dependencies
2. Runs `npm test` to validate build system (MUST pass)
3. Builds web UI with `npm run build` (automatically run by PlatformIO)
4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all)
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
**To ensure CI success, you MUST locally:**
- Run `npm test` and ensure it passes
- Run `pio run -e esp32dev` (or another common environment from "Hardware Compilation" section) and ensure it completes successfully
- If either fails locally, it WILL fail in CI
**Match this workflow in your local development to ensure CI success. Do not mark work complete until you have validated builds locally.**
Match this workflow in your local development to ensure CI success.

View File

@@ -450,7 +450,7 @@ board_build.partitions = ${esp32.large_partitions}
board_upload.flash_size = 8MB
board_upload.maximum_size = 8388608
; board_build.f_flash = 80000000L
board_build.flash_mode = dio
; board_build.flash_mode = qio
[env:esp32dev_16M]
board = esp32dev

View File

@@ -1,504 +1,4 @@
# Usermod user FX
This usermod is a common place to put various users WLED effects. It lets you load your own custom effects or bring back deprecated ones—without touching core WLED source code.
This Usermod is a common place to put various user's LED effects.
Multiple Effects can be specified inside this single usermod, as we will illustrate below. You will be able to define them with custom names, sliders, etc. as with any other Effect.
* [How The Usermod Works](./README.md#how-the-usermod-works)
* [Basic Syntax for WLED Effect Creation](./README.md#basic-syntax-for-wled-effect-creation)
* [Understanding 2D WLED Effects](./README.md#understanding-2d-wled-effects)
* [The Metadata String](./README.md#the-metadata-string)
* [Understanding 1D WLED Effects](./README.md#understanding-1d-wled-effects)
* [Combining Multiple Effects in this Usermod](./README.md#combining-multiple-effects-in-this-usermod)
* [Compiling](./README.md#compiling)
* [Change Log](./README.md#change-log)
* [Contact Us](./README.md#contact-us)
## How The Usermod Works
The `user_fx.cpp` file can be broken down into four main parts:
* **static effect definition** - This is a static LED setting that is displayed if an effect fails to initialize.
* **User FX function definition(s)** - This area is where you place the FX code for all of the custom effects you want to use. This mainly includes the FX code and the static variable containing the [metadata string](https://kno.wled.ge/interfaces/json-api/#effect-metadata).
* **Usermod Class definition(s)** - The class definition defines the blueprint from which all your custom Effects (or any usermod, for that matter) are created.
* **Usermod registration** - All usermods have to be registered so that they are able to be compiled into your binary.
We will go into greater detail on how custom effects work in the usermod and how to go about creating your own in the section below.
## Basic Syntax for WLED Effect Creation
WLED effects generally follow a certain procedure for their operation:
1. Determine dimension of segment
2. Calculate new state if needed
3. Implement a loop that calculates color for each pixel and sets it using `SEGMENT.setPixelColor()`
4. The function is called at current frame rate.
Below are some helpful variables and functions to know as you start your journey towards WLED effect creation:
| Syntax Element | Size | Description |
| :---------------------------------------------- | :----- | :---------- |
| [`SEGMENT.speed / intensity / custom1 / custom2`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L450) | 8-bit | These read-only variables help you control aspects of your custom effect using the UI sliders. You can edit these variables through the UI sliders when WLED is running your effect. (These variables can be controlled by the API as well.) Note that while `SEGMENT.intensity` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. The other three bits are used by the boolean parameters `SEGMENT.check1` through `SEGMENT.check3` and are bit-packed to conserve data size and memory. |
| [`SEGMENT.custom3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L454) | 5-bit | Another optional UI slider for custom effect control. While `SEGMENT.speed` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. |
| [`SEGMENT.check1 / check2 / check3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L455) | 1-bit | These variables are boolean parameters which show up as checkbox options in the User Interface. They are bit-packed along with `SEGMENT.custom3` to conserve data size and memory. |
| [`SEGENV.aux0 / aux1`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L467) | 16-bit | These are state variables that persists between function calls, and they are free to be overwritten by the user for any use case. |
| [`SEGENV.step`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L465) | 32-bit | This is a timestamp variable that contains the last update time. It is initially set during effect initialization to 0, and then it updates with the elapsed time after each frame runs. |
| [`SEGENV.call`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L466) | 32-bit | A counter for how many times this effect function has been invoked since it started. |
| [`strip.now`](https://github.com/wled/WLED/blob/main/wled00/FX.h) | 32-bit | Current timestamp in milliseconds. (Equivalent to `millis()`, but use `strip.now()` instead.) `strip.now` respects the timebase, which can be used to advance or reset effects in a preset. This can be useful to sync multiple segments. |
| [`SEGLEN / SEG_W / SEG_H`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L116) | 16-bit | These variables are macros that help define the length and width of your LED strip/matrix segment. |
| [`SEGPALETTE`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L115) | --- | Macro that gets the currently selected palette for the currently processing segment. |
| [`hw_random8()`](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/fcn_declare.h#L548) | 8-bit | One of several functions that generates a random integer. (All of the "hw_" functions are similar to the FastLED library's random functions, but in WLED they use true hardware-based randomness instead of a pseudo random number. In short, they are better and faster.) |
| [`SEGCOLOR(x)`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L114) | 32-bit | Macro that gets user-selected colors from UI, where x is an integer 1, 2, or 3 for primary, secondary, and tertiary colors, respectively. |
| [`SEGMENT.setPixelColor`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) / [`setPixelColorXY`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_2Dfcn.cpp) | 32-bit | Function that paints one pixel. `setPixelColor` is 1D; `setPixelColorXY` expects `(x, y)` and an RGBW color value. |
| [`SEGMENT.color_wheel()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1092) | 32-bit | Input 0255 to get a color. Transitions r→g→b→r. In HSV terms, `pos` is H. Note: only returns palette color unless the Default palette is selected. |
| [`SEGMENT.color_from_palette()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1093) | 32-bit | Gets a single color from the currently selected palette for a segment. (This function which should be favoured over `ColorFromPalette()` because this function returns an RGBW color with white from the `SEGCOLOR` passed, while also respecting the setting for palette wrapping. On the other hand, `ColorFromPalette()` simply gets the RGB palette color.) |
| [`fade_out()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1012) | --- | fade out function, higher rate = quicker fade. fading is highly dependent on frame rate (higher frame rates, faster fading). each frame will fade at max 9% or as little as 0.8%. |
| [`fadeToBlackBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | can be used to fade all pixels to black. |
| [`fadeToSecondaryBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | fades all pixels to secondary color. |
| [`move()`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) | --- | Moves/shifts pixels in the desired direction. |
| [`blur / blur2d`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1053) | --- | Blurs all pixels for the desired segment. Blur also has the boolean option `smear`, which, when activated, does not fade the blurred pixel(s). |
You will see how these syntax elements work in the examples below.
## Understanding 2D WLED Effects
In this section we give some advice to those who are new to WLED Effect creation. We will illustrate how to load in multiple Effects using this single usermod, and we will do a deep dive into the anatomy of a 1D Effect as well as a 2D Effect.
(Special thanks to @mryndzionek for offering this "Diffusion Fire" 2D Effect for this tutorial.)
### Imports
The first line of the code imports the [wled.h](https://github.com/wled/WLED/blob/main/wled00/wled.h) file into this module. Importing `wled.h` brings all of the variables, files, and functions listed in the table above (and more) into your custom effect for you to use.
```cpp
#include "wled.h"
```
### Static Effect Definition
The next code block is the `mode_static` definition. This is usually left as `SEGMENT.fill(SEGCOLOR(0));` to leave all pixels off if the effect fails to load, but in theory one could use this as a 'fallback effect' to take on a different behavior, such as displaying some other color instead of leaving the pixels off.
### User Effect Definitions
Pre-loaded in this template is an example 2D Effect called "Diffusion Fire". (This is the name that would be shown in the UI once the binary is compiled and run on your device, as defined in the metadata string.)
The effect starts off by checking to see if the segment that the effect is being applied to is a 2D Matrix, and if it is not, then it returns the static effect which displays no pattern:
```cpp
if (!strip.isMatrix || !SEGMENT.is2D())
return mode_static(); // not a 2D set-up
```
The next code block contains several constant variable definitions which essentially serve to extract the dimensions of the user's 2D matrix and allow WLED to interpret the matrix as a 1D coordinate system (WLED must do this for all 2D animations):
```cpp
const int cols = SEG_W;
const int rows = SEG_H;
const auto XY = [&](int x, int y) { return x + y * cols; };
```
* The first line assigns the number of columns (width) in the active segment to cols.
* SEG_W is a macro defined in WLED that expands to SEGMENT.width(). This value is the width of your 2D matrix segment, used to traverse the matrix correctly.
* Next, we assign the number of rows (height) in the segment to rows.
* SEG_H is a macro for SEGMENT.height(). Combined with cols, this allows pixel addressing in 2D (x, y) space.
* The third line declares a lambda function named `XY` to map (x, y) matrix coordinates into a 1D index in the LED array. This assumes row-major order (left to right, top to bottom).
* This lambda helps with mapping a local 1D array to a 2D one.
The next lines of code further the setup process by defining variables that allow the effect's settings to be configurable using the UI sliders (or alternatively, through API calls):
```cpp
const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80);
const unsigned refresh_ms = 1000 / refresh_hz;
const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100);
const uint8_t spark_rate = SEGMENT.intensity;
const uint8_t turbulence = SEGMENT.custom2;
```
* The first line maps the SEGMENT.speed (user-controllable parameter from 0255) to a value between 20 and 80 Hz.
* This determines how often the effect should refresh per second (Higher speed = more frames per second).
* Next we convert refresh rate from Hz to milliseconds. (Its easier to schedule animation updates in WLED using elapsed time in milliseconds.)
* This value is used to time when to update the effect.
* The third line utilizes the `custom1` control (0255 range, usually exposed via sliders) to define the diffusion rate, mapped to 0100.
* This controls how much "heat" spreads to neighboring pixels — more diffusion = smoother flame spread.
* Next we assign `SEGMENT.intensity` (user input 0255) to a variable named `spark_rate`.
* This controls how frequently new "spark" pixels appear at the bottom of the matrix.
* A higher value means more frequent ignition of flame points.
* The final line stores the user-defined `custom2` value to a variable called `turbulence`.
* This is used to introduce randomness in spark generation or flow — more turbulence means more chaotic behavior.
Next we will look at some lines of code that handle memory allocation and effect initialization:
```cpp
unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D
```
* This part calculates how much memory we need to represent per-pixel state.
* `cols * rows` or `(or SEGLEN)` returns the total number of pixels in the current segment.
* This fire effect models heat values per pixel (not just colors), so we need persistent storage — one uint8_t per pixel — for the entire effect.
> **_NOTE:_** Virtual lengths `vWidth()` and `vHeight()` will be evaluated differently based on your own custom effect, and based on what other settings are active. For example: If you have an LED strip of length = 60 and you enable grouping = 2, then the virtual length will be 30, so the FX will render 30 pixels instead of 60. This is also true for mirroring or adding gaps--it halves the size. For a 1D strip mapped to 2D, the virtual length depends on selected mode. Keep these things in mind during your custom effect's creation.
```cpp
if (!SEGENV.allocateData(dataSize))
return mode_static(); // allocation failed
```
* Upon the first call, this section allocates a persistent data buffer tied to the segment environment (`SEGENV.data`). All subsequent calls simply ensure that the data is still valid.
* The syntax `SEGENV.allocateData(n)` requests a buffer of size n bytes (1 byte per pixel here).
* If allocation fails (e.g., out of memory), it returns false, and the effect cant proceed.
* It calls previously defined `mode_static()` fallback effect, which just fills the segment with a static color. We need to do this because WLED needs a fail-safe behavior if a custom effect can't run properly due to memory constraints.
The next lines of code clear the LEDs and initialize timing:
```cpp
if (SEGENV.call == 0) {
SEGMENT.fill(BLACK);
SEGENV.step = 0;
}
```
* The first line checks whether this is the first time the effect is being run; `SEGENV.call` is a counter for how many times this effect function has been invoked since it started.
* If `SEGENV.call` equals 0 (which it does on the very first call, making it useful for initialization), then it clears the LED segment by filling it with black (turns off all LEDs).
* This gives a clean starting point for the fire animation.
* It also initializes `SEGENV.step`, a timing marker, to 0. This value is later used as a timestamp to control when the next animation frame should occur (based on elapsed time).
The next block of code is where the animation update logic starts to kick in:
```cpp
if ((strip.now - SEGENV.step) >= refresh_ms) {
uint8_t tmp_row[cols]; // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch.
SEGENV.step = strip.now;
// scroll up
for (unsigned y = 1; y < rows; y++)
for (unsigned x = 0; x < cols; x++) {
unsigned src = XY(x, y);
unsigned dst = XY(x, y - 1);
SEGENV.data[dst] = SEGENV.data[src];
}
```
* The first line checks if it's time to update the effect frame. `strip.now` is the current timestamp in milliseconds; `SEGENV.step` is the last update time (set during initialization or previous frame). `refresh_ms` is how long to wait between frames, computed earlier based on SEGMENT.speed.
* The conditional statement in the first line of code ensures the effect updates on a fixed interval — e.g., every 20 ms for 50 Hz.
* The second line of code declares a temporary row buffer for intermediate diffusion results that is one byte per column (horizontal position), so this buffer holds one row's worth of heat values.
* You'll see later that it writes results here before updating `SEGENV.data`.
* Note: this is allocated on the stack each frame. Keep such VLAs ≤ ~1 KiB; for larger sizes, prefer a buffer in `SEGENV.data`.
> **_IMPORTANT NOTE:_** Creating variablelength arrays (VLAs) is nonstandard C++, but this practice is used throughout WLED and works in practice. But be aware that VLAs live on the stack, which is limited. If the array scales with segment length (1D), it can overflow the stack and crash. Keep VLAs ≲ ~1 KiB; an array with 4000 LEDs is ~4 KiB and will likely crash. Its worse with `uint16_t`. Anything larger than ~1 KiB should go into `SEGENV.data`, which has a higher limit.
Now we get to the spark generation portion, where new bursts of heat appear at the bottom of the matrix:
```cpp
if (hw_random8() > turbulence) {
// create new sparks at bottom row
for (unsigned x = 0; x < cols; x++) {
uint8_t p = hw_random8();
if (p < spark_rate) {
unsigned dst = XY(x, rows - 1);
SEGENV.data[dst] = 255;
}
}
}
```
* The first line randomizes whether we even attempt to spawn sparks this frame.
* `hw_random8()` gives a random number between 0255 using a fast hardware RNG.
* `turbulence` is a user-controlled parameter (SEGMENT.custom2, set earlier).
* Higher turbulence means this block is less likely to run (because `hw_random8()` is less likely to exceed a high threshold).
* This adds randomness to when sparks appear — simulating natural flicker and chaotic fire.
* The next line loops over all columns in the bottom row (row `rows - 1`).
* Another random number, `p`, is used to probabilistically decide whether a spark appears at this (x, `rows-1`) position.
* Next is a conditional statement. The lower spark_rate is, the fewer sparks will appear.
* `spark_rate` comes from `SEGMENT.intensity` (0255).
* High intensity means more frequent ignition.
* `dst` calculates the destination index in the bottom row at column x.
* The final line here sets the heat at this pixel to maximum (255).
* This simulates a fresh burst of flame, which will diffuse and move upward over time in subsequent frames.
Next we reach the first part of the core of the fire simulation, which is diffusion (how heat spreads to neighboring pixels):
```cpp
// diffuse
for (unsigned y = 0; y < rows; y++) {
for (unsigned x = 0; x < cols; x++) {
unsigned v = SEGENV.data[XY(x, y)];
if (x > 0) {
v += SEGENV.data[XY(x - 1, y)];
}
if (x < (cols - 1)) {
v += SEGENV.data[XY(x + 1, y)];
}
tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion)));
}
```
* This block of code starts by looping over each row from top to bottom. (We will do diffusion for each pixel row.)
* Next we start an inner loop which iterates across each column in the current row.
* Starting with the current heat value of pixel (x, y) assigned `v`:
* if theres a pixel to the left, add its heat to the total.
* If theres a pixel to the right, add its heat as well.
* So essentially, what the two `if` statements accomplish is: `v = center + left + right`.
* The final line of code applies diffusion smoothing:
* The denominator controls how much the neighboring heat contributes. `300 + diffusion` means that with higher diffusion, you get more smoothing (since the sum is divided more).
* The `v * 100` scales things before dividing (preserving some dynamic range).
* `min(255, ...)` clamps the result to 8-bit range.
* This entire line of code stores the smoothed heat into the temporary row buffer.
After calculating tmp_row, we now handle rendering the pixels by updating the actual segment data and turning 'heat' into visible colors:
```cpp
for (unsigned x = 0; x < cols; x++) {
SEGENV.data[XY(x, y)] = tmp_row[x];
if (SEGMENT.check1) {
uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0);
SEGMENT.setPixelColorXY(x, y, color);
} else {
uint32_t base = SEGCOLOR(0);
SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x]));
}
}
}
```
* This next loop starts iterating over each row from top to bottom. (We're now doing this for color-rendering for each pixel row.)
* Next we update the main segment data with the smoothed value for this pixel.
* The if statement creates a conditional rendering path — the user can toggle this. If `check1` is enabled in the effect metadata, we use a color palette to display the flame.
* The next line converts the heat value (`tmp_row[x]`) into a `color` from the current palette with 255 brightness, and no wrapping in palette lookup.
* This creates rich gradient flames (e.g., yellow → red → black).
* Finally we set the rendered color for the pixel (x, y).
* This repeats for each pixel in each row.
* If palette use is disabled, we fallback to fading a base color.
* `SEGCOLOR(0)` gets the first user-selected color for the segment.
* The final line of code fades that base color according to the heat value (acts as brightness multiplier).
The final piece of this custom effect returns the frame time:
```cpp
}
return FRAMETIME;
}
```
* The first bracket closes the earlier `if ((strip.now - SEGENV.step) >= refresh_ms)` block.
* It ensures that the fire simulation (scrolling, sparking, diffusion, rendering) only runs when enough time has passed since the last update.
* returning the frame time tells WLED how soon this effect wants to be called again.
* `FRAMETIME` is a predefined macro in WLED, typically set to ~16ms, corresponding to ~60 FPS (frames per second).
* Even though the effect logic itself controls when to update based on refresh_ms, WLED will still call this function at roughly FRAMETIME intervals to check whether an update is needed.
* ⚠️ Important: Because the actual frame logic is gated by strip.now - SEGENV.step, returning FRAMETIME here doesnt cause excessive updates — it just keeps the engine responsive. **Also note that an Effect should ALWAYS return FRAMETIME. Not doing so can cause glitches.**
* The final bracket closes the `mode_diffusionfire()` function itself.
### The Metadata String
At the end of every effect is an important line of code called the **metadata string**.
It defines how the effect is to be interacted with in the UI:
```cpp
static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35";
```
This metadata string is passed into `strip.addEffect()` and parsed by WLED to determine how your effect appears and behaves in the UI.
The string follows the syntax of `<Effect Parameters>;<Colors>;<Palette>;<Flags>;<Defaults>`, where Effect Parameters are specified by a comma-separated list.
The values for Effect Parameters will always follow the convention in the table below:
| Parameter | Default tooltip label |
| :-------- | :-------------------- |
| sx | Effect Speed |
| ix | Effect Intensity |
| c1 | Custom 1 |
| c2 | Custom 2 |
| c3 | Custom 3 |
| o1 | Checkbox 1 |
| o2 | Checkbox 2 |
| o3 | Checkbox 3 |
Using this info, lets split the Metadata string above into logical sections:
| Syntax Element | Description |
| :---------------------------------------------- | :---------- |
| "Diffusion Fire@! | Name. (The @ symbol marks the end of the Effect Name, and the beginning of the Parameter String elements.) |
| !, | Use default UI entry; for the first space, this will automatically create a slider for Speed |
| Spark rate, Diffusion Speed, Turbulence, | UI sliders for Spark Rate, Diffusion Speed, and Turbulence. Defining slider 2 as "Spark Rate" overwrites the default value of Intensity. |
| (blank), | unused (empty field with not even a space) |
| Use palette; | This occupies the spot for the 6th effect parameter, which automatically makes this a checkbox argument `o1` called Use palette in the UI. When this is enabled, the effect uses `SEGMENT.color_from_palette(...)` (RGBW-aware, respects wrap), otherwise it fades from `SEGCOLOR(0)`. The first semicolon marks the end of the Effect Parameters and the beginning of the `Colors` parameter. |
| Color; | Custom color field `(SEGCOLOR(0))` |
| (blank); | Empty means the effect does not allow Palettes to be selected by the user. But used in conjunction with the checkbox argument, palette use can be turned on/off by the user. |
| 2; | Flag specifying that the effect requires a 2D matrix setup |
| pal=35" | Default Palette ID. this is the setting that the effect starts up with. |
More information on metadata strings can be found [here](https://kno.wled.ge/interfaces/json-api/#effect-metadata).
## Understanding 1D WLED Effects
Next, we will look at a 1D WLED effect called `Sinelon`. This one is an especially interesting example because it shows how a single effect function can be used to create several different selectable effects in the UI.
We will break this effect down step by step.
(This effect was originally one of the FastLED example effects; more information on FastLED can be found [here](https://fastled.io/).)
```cpp
static uint16_t sinelon_base(bool dual, bool rainbow=false) {
```
* The first line of code defines `sinelon base` as static helper function. This is how all effects are initially defined.
* Notice that it has some optional flags; these parameters will allow us to easily define the effect in different ways in the UI.
```cpp
if (SEGLEN <= 1) return mode_static();
```
* If segment length ≤ 1, theres nothing to animate. Just show static mode.
The line of code helps create the "Fade Out" Trail:
```cpp
SEGMENT.fade_out(SEGMENT.intensity);
```
* Gradually dims all LEDs each frame using SEGMENT.intensity as fade amount.
* Creates the trailing "comet" effect by leaving a fading path behind the moving dot.
Next, the effect computes some position information for the actively changing pixel, and the rest of the pixels as well:
```cpp
unsigned pos = beatsin16_t(SEGMENT.speed/10, 0, SEGLEN-1);
if (SEGENV.call == 0) SEGENV.aux0 = pos;
```
* Calculates a sine-based oscillation to move the dot smoothly back and forth.
* `beatsin16_t` is an improved version of FastLEDs beatsin16 function, generating smooth oscillations
* SEGMENT.speed / 10: affects oscillation speed. Higher = faster.
* 0: minimum position.
* SEGLEN-1: maximum position.
* On first call `(SEGENV.call == 0)`, stores initial position in `SEGENV.aux0`. (`SEGENV.aux0` is a temporary state variable to keep track of last position.)
The next lines of code help determine the colors to be used:
```cpp
uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0);
uint32_t color2 = SEGCOLOR(2);
```
* `color1`: main moving dot color, chosen from palette using the current position as index.
* `color2`: secondary color from user-configured color slot 2.
The next part takes into account the optional argument for if a Rainbow colored palette is in use:
```cpp
if (rainbow) {
color1 = SEGMENT.color_wheel((pos & 0x07) * 32);
}
```
* If `rainbow` is true, override color1 using a rainbow wheel, producing rainbow cycling colors.
* `(pos & 0x07) * 32` ensures the color changes gradually with position.
```cpp
SEGMENT.setPixelColor(pos, color1);
```
* Lights up the computed position with the selected color.
The next line takes into account another one of the optional arguments for the effect to potentially handle dual mirrored dots which create the animation:
```cpp
if (dual) {
if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0);
if (rainbow) color2 = color1; // share rainbow color
SEGMENT.setPixelColor(SEGLEN-1-pos, color2);
}
```
* If dual is true:
* Uses `color2` for mirrored dot on opposite side.
* If `color2` is not set (0), fallback to same palette color as `color1`.
* In `rainbow` mode, force both dots to share the rainbow color.
* Sets pixel at `SEGLEN-1-pos` to `color2`.
This final part of the effect function will fill in the 'trailing' pixels to complete the animation:
```cpp
if (SEGENV.aux0 < pos) {
for (unsigned i = SEGENV.aux0; i < pos ; i++) {
SEGMENT.setPixelColor(i, color1);
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
}
} else {
for (unsigned i = SEGENV.aux0; i > pos ; i--) {
SEGMENT.setPixelColor(i, color1);
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
}
}
SEGENV.aux0 = pos;
}
```
* The first line checks if current position has changed since last frame. (Prevents holes if the dot moves quickly and "skips" pixels.) If the position has changed, then it will implement the logic to update the rest of the pixels.
* Fills in all pixels between previous position (SEGENV.aux0) and new position (pos) to ensure smooth continuous trail.
* Works in both directions: Forward (if new pos > old pos), and Backward (if new pos < old pos).
* Updates `SEGENV.aux0` to current position at the end.
Finally, we return the `FRAMETIME`, as with all effect functions:
```cpp
return FRAMETIME;
}
```
* Returns `FRAMETIME` constant to set effect update rate (usually ~16 ms).
The last part of this effect has the Wrapper functions for different Sinelon modes.
Notice that there are three different modes that we can define from the single effect definition by leveraging the arguments in the function:
```cpp
uint16_t mode_sinelon(void) {
return sinelon_base(false);
}
// Calls sinelon_base with dual = false and rainbow = false
uint16_t mode_sinelon_dual(void) {
return sinelon_base(true);
}
// Calls sinelon_base with dual = true and rainbow = false
uint16_t mode_sinelon_rainbow(void) {
return sinelon_base(false, true);
}
// Calls sinelon_base with dual = false and rainbow = true
```
And then the last part defines the metadata strings for each effect to specify how it will be portrayed in the UI:
```cpp
static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!";
static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!";
static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!";
```
Refer to the section above for guidance on understanding metadata strings.
### The UserFxUsermod Class
The `UserFxUsermod` class registers the `mode_diffusionfire` effect with WLED. This section starts right after the effect function and metadata string, and is responsible for making the effect usable in the WLED interface:
```cpp
class UserFxUsermod : public Usermod {
private:
public:
void setup() override {
strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE);
////////////////////////////////////////
// add your effect function(s) here //
////////////////////////////////////////
// use id=255 for all custom user FX (the final id is assigned when adding the effect)
// strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT);
// strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2);
// strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3);
}
void loop() override {} // nothing to do in the loop
uint16_t getId() override { return USERMOD_ID_USER_FX; }
};
```
* The first line declares a new class called UserFxUsermod. It inherits from `Usermod`, which is the base class WLED uses for any pluggable user-defined modules.
* This makes UserFxUsermod a valid WLED extension that can hook into `setup()`, `loop()`, and other lifecycle events.
* The `void setup()` function runs once when WLED initializes the usermod.
* It's where you should register your effects, initialize hardware, or do any other setup logic.
* `override` ensures that this matches the Usermod base class definition.
* The `strip.addEffect` line is an important one that registers the custom effect so WLED knows about it.
* 255: Temporary ID WLED will assign a unique ID automatically. (**Create all custom effects with the 255 ID.**)
* `&mode_diffusionfire`: Pointer to the effect function.
* `_data_FX_MODE_DIFFUSIONFIRE`: Metadata string stored in PROGMEM, describing the effect name and UI fields (like sliders).
* After this, your custom effect shows up in the WLED effects list.
* The `loop()` function remains empty because this usermod doesnt need to do anything continuously. WLED still calls this every main loop, but nothing is done here.
* If your usermod had to respond to input or update state, you'd do it here.
* The last part returns a unique ID constant used to identify this usermod.
* USERMOD_ID_USER_FX is defined in [const.h](https://github.com/wled/WLED/blob/main/wled00/const.h). WLED uses this for tracking, debugging, or referencing usermods internally.
The final part of this file handles instantiation and initialization:
```cpp
static UserFxUsermod user_fx;
REGISTER_USERMOD(user_fx);
```
* The first line creates a single, global instance of your usermod class.
* The last line is a macro that tells WLED: This is a valid usermod load it during startup.”
* WLED adds it to the list of active usermods, calls `setup()` and `loop()`, and lets it interact with the system.
## Combining Multiple Effects in this Usermod
So now let's say that you wanted add the effects "Diffusion Fire" and "Sinelon" through this same Usermod file:
* Navigate to [the code for Sinelon](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/FX.cpp#L3110).
* Copy this code, and place it below the metadata string for Diffusion Fire. Be sure to get the metadata string as well--and to name it something different than what's already inside the core WLED code. (Refer to the metadata String section above for more information.)
* Register the effect using the `addEffect` function in the Usermod class.
* Compile the code!
## Compiling
Compiling WLED yourself is beyond the scope of this tutorial, but [the complete guide to compiling WLED can be found here](https://kno.wled.ge/advanced/compiling-wled/), on the official WLED documentation website.
## Change Log
### Version 1.0.0
* First version of the custom effect creation guide
## Contact Us
This custom effect tutorial guide is still in development.
If you have suggestions on what should be added, or if you've found any parts of this guide which seem incorrect, feel free to reach out [here](mailto:aregis1992@gmail.com) and help us improve this guide for future creators.

View File

@@ -27,7 +27,7 @@ static uint16_t mode_diffusionfire(void) {
const uint8_t spark_rate = SEGMENT.intensity;
const uint8_t turbulence = SEGMENT.custom2;
unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D
unsigned dataSize = SEGMENT.length(); // allocate persistent data for heat value for each pixel
if (!SEGENV.allocateData(dataSize))
return mode_static(); // allocation failed
@@ -37,7 +37,6 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
}
if ((strip.now - SEGENV.step) >= refresh_ms) {
// Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch.
uint8_t tmp_row[cols];
SEGENV.step = strip.now;
// scroll up
@@ -45,7 +44,7 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
for (unsigned x = 0; x < cols; x++) {
unsigned src = XY(x, y);
unsigned dst = XY(x, y - 1);
SEGENV.data[dst] = SEGENV.data[src];
SEGMENT.data[dst] = SEGMENT.data[src];
}
if (hw_random8() > turbulence) {
@@ -54,7 +53,7 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
uint8_t p = hw_random8();
if (p < spark_rate) {
unsigned dst = XY(x, rows - 1);
SEGENV.data[dst] = 255;
SEGMENT.data[dst] = 255;
}
}
}
@@ -62,24 +61,24 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
// diffuse
for (unsigned y = 0; y < rows; y++) {
for (unsigned x = 0; x < cols; x++) {
unsigned v = SEGENV.data[XY(x, y)];
unsigned v = SEGMENT.data[XY(x, y)];
if (x > 0) {
v += SEGENV.data[XY(x - 1, y)];
v += SEGMENT.data[XY(x - 1, y)];
}
if (x < (cols - 1)) {
v += SEGENV.data[XY(x + 1, y)];
v += SEGMENT.data[XY(x + 1, y)];
}
tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion)));
}
for (unsigned x = 0; x < cols; x++) {
SEGENV.data[XY(x, y)] = tmp_row[x];
SEGMENT.data[XY(x, y)] = tmp_row[x];
if (SEGMENT.check1) {
uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0);
uint32_t color = ColorFromPalette(SEGPALETTE, tmp_row[x], 255, LINEARBLEND_NOWRAP);
SEGMENT.setPixelColorXY(x, y, color);
} else {
uint32_t base = SEGCOLOR(0);
SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x]));
uint32_t color = SEGCOLOR(0);
SEGMENT.setPixelColorXY(x, y, color_fade(color, tmp_row[x]));
}
}
}

View File

@@ -135,8 +135,7 @@ uint16_t mode_copy_segment(void) {
SEGMENT.fadeToBlackBy(5); // fade out
return FRAMETIME;
}
Segment& sourcesegment = strip.getSegment(sourceid);
Segment sourcesegment = strip.getSegment(sourceid);
if (sourcesegment.isActive()) {
uint32_t sourcecolor;
uint32_t destcolor;
@@ -678,7 +677,7 @@ uint16_t mode_twinkle(void) {
SEGENV.step = it;
}
uint16_t PRNG16 = SEGENV.aux1;
unsigned PRNG16 = SEGENV.aux1;
for (unsigned i = 0; i < SEGENV.aux0; i++)
{
@@ -1715,8 +1714,8 @@ static const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = "Tri Wipe@!;1,2,3;!";
* Modified by Aircoookie
*/
uint16_t mode_tricolor_fade(void) {
uint16_t counter = strip.now * ((SEGMENT.speed >> 3) +1);
uint32_t prog = (counter * 768) >> 16;
unsigned counter = strip.now * ((SEGMENT.speed >> 3) +1);
uint16_t prog = (counter * 768) >> 16;
uint32_t color1 = 0, color2 = 0;
unsigned stage = 0;

View File

@@ -625,9 +625,6 @@ class Segment {
DEBUGFX_PRINTLN();
#endif
clearName();
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
deallocateData();
p_free(pixels);
}

View File

@@ -448,9 +448,6 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
// apply change immediately
if (i2 <= i1) { //disable segment
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
deallocateData();
p_free(pixels);
pixels = nullptr;
@@ -469,9 +466,6 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
#endif
// safety check
if (start >= stop || startY >= stopY) {
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
deallocateData();
p_free(pixels);
pixels = nullptr;
@@ -485,9 +479,6 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
pixels = static_cast<uint32_t*>(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS));
if (!pixels) {
DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
deallocateData();
errorFlag = ERR_NORAM_PX;
stop = 0;

View File

@@ -72,10 +72,11 @@ uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) {
// 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
addRemains = r && (r<<5) > maxc ? 0x00010000 : 0; // note: setting color preservation threshold too high results in flickering and
addRemains |= g && (g<<5) > maxc ? 0x00000100 : 0; // jumping colors in low brightness gradients. Multiplying the color preserves
addRemains |= b && (b<<5) > maxc ? 0x00000001 : 0; // better accuracy than dividing the maxc. Shifting by 5 is a good compromise
addRemains |= w ? 0x01000000 : 0; // i.e. remove color channel if <13% of max
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) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue

View File

@@ -439,6 +439,23 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented)
#define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented)
// Additional error types for better user feedback
#define ERR_NORAM_BUS 33 // Not enough RAM for bus allocation
#define ERR_NORAM_SEG 34 // Not enough RAM for segment allocation
#define ERR_NORAM_TRANS 35 // Not enough RAM for transition effects
#define ERR_PIN_CONFLICT 36 // Pin assignment conflict detected
#define ERR_PIN_INVALID 37 // Invalid pin number for this platform
#define ERR_CONFIG_LOAD 38 // Configuration loading failed
#define ERR_CONFIG_SAVE 39 // Configuration saving failed
// Warning types (starting at 100 as requested)
#define WARN_LOW_MEMORY 100 // Low memory warning
#define WARN_HIGH_TEMP 101 // Temperature approaching limits
#define WARN_LOW_VOLTAGE 102 // Voltage below optimal range
#define WARN_HIGH_CURRENT 103 // Current approaching limits
#define WARN_WIFI_WEAK 104 // Weak WiFi signal
#define WARN_FS_SPACE 105 // Filesystem space running low
// Timer mode types
#define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness
#define NL_MODE_FADE 1 //Fade to target brightness gradually

View File

@@ -293,7 +293,7 @@
rmTrash(e);
e.stopPropagation();
const src = e.target || e.srcElement;
let cp = gId(src.id.replace("cPM","cPick"));
let cp = gId(src.id.replace("M","")); // marker → picker
cp.click();
}

View File

@@ -325,6 +325,13 @@
<div id="imgw">
<img class="wi" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAFCAYAAAC5Fuf5AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABbSURBVChTlY9bDoAwDMNW7n9nwCipytQN4Z8tbrTHmDmF4oPzyldwRqp1SSdnV/NuZuzqerAByxXznBw3igkeFEfXyUuhK/yFM0CxJfyqXZEOc6/Sr9/bf7uIC5Nwd7orMvAPAAAAAElFTkSuQmCC" />
</div>
<div id="errorLogArea" style="margin: 10px auto; display: none;">
<h3 style="margin: 0 0 8px 0; font-size: 16px; text-align: center;">Error Log</h3>
<div id="errorLogContent" style="padding: 8px; background: var(--c-3); border-radius: 20px; font-size: 14px; max-height: 120px; overflow-y: auto; text-align: left;"></div>
<div style="margin-top: 8px; text-align: center;">
<button class="btn ibtn" onclick="clearErrorLog()">Clear Log</button>
</div>
</div>
<div id="kv">Loading...</div><br>
<div>
<button class="btn ibtn" onclick="requestJson()">Refresh</button>

View File

@@ -26,6 +26,8 @@ var pmt = 1, pmtLS = 0, pmtLast = 0;
var lastinfo = {};
var isM = false, mw = 0, mh=0;
var ws, wsRpt=0;
var errorLog = []; // Store last 5 errors/warnings
var hasUnreadErrors = false;
var cfg = {
theme:{base:"dark", bg:{url:"", rnd: false, rndGrayscale: false, rndBlur: false}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}},
comp :{colors:{picker: true, rgb: false, quick: true, hex: false},
@@ -384,6 +386,152 @@ function inforow(key, val, unit = "")
return `<tr><td class="keytd">${key}</td><td class="valtd">${val}${unit}</td></tr>`;
}
function getErrorMessage(errorCode) {
switch (errorCode) {
case 1: return "Operation denied by current settings";
case 2: return "Cannot process request while client is active";
case 3: return "JSON buffer is locked, try again in a moment";
case 4: return "Feature not implemented on this version";
case 7: return "Insufficient RAM to allocate pixel buffer";
case 8: return "Not enough RAM available for effect processing";
case 9: return "JSON parsing failed - data may be too large";
case 10: return "Could not initialize filesystem - check partition";
case 11: return "Not enough space to save preset to filesystem";
case 12: return "Requested preset does not exist";
case 13: return "IR configuration file 'ir.json' not found";
case 14: return "Remote configuration file 'remote.json' not found";
case 19: return "An unspecified filesystem error occurred";
case 30: return "Temperature sensor reading above safe threshold";
case 31: return "Current sensor reading above safe threshold";
case 32: return "Voltage sensor reading below safe threshold";
case 33: return "Insufficient RAM to allocate LED bus";
case 34: return "Insufficient RAM to allocate segment data";
case 35: return "Insufficient RAM for transition effects";
case 36: return "Pin assignment conflict detected";
case 37: return "Invalid pin number for this platform";
case 38: return "Failed to load configuration from filesystem";
case 39: return "Failed to save configuration to filesystem";
case 100: return "Memory usage is approaching limits";
case 101: return "Temperature is approaching safe limits";
case 102: return "Voltage is below optimal operating range";
case 103: return "Current draw is approaching safe limits";
case 104: return "WiFi signal strength is poor";
case 105: return "Filesystem space is running low";
default: return `Unknown error code ${errorCode}`;
}
}
function addToErrorLog(errorCode, timestamp = null) {
if (!timestamp) timestamp = Date.now();
const errorEntry = {
code: errorCode,
message: getErrorMessage(errorCode),
timestamp: timestamp,
isWarning: errorCode >= 100
};
// Add to beginning of array
errorLog.unshift(errorEntry);
// Keep only last 5 entries
if (errorLog.length > 5) {
errorLog = errorLog.slice(0, 5);
}
hasUnreadErrors = true;
updateInfoButtonIcon();
}
function updateInfoButtonIcon() {
const infoBtn = gId('buttonI');
const icon = infoBtn.querySelector('i');
if (hasUnreadErrors) {
// Change to red exclamation mark icon
icon.innerHTML = '&#xe18a;'; // Use add/warning icon
icon.style.color = 'var(--c-r)';
} else {
// Reset to normal info icon
icon.innerHTML = '&#xe066;'; // Info icon
icon.style.color = '';
}
}
function handleServerErrorLog(serverErrors, serverTime) {
// Clear client-side log and replace with server data
errorLog = [];
hasUnreadErrors = false;
for (let i = 0; i < serverErrors.length; i++) {
const serverEntry = serverErrors[i];
// Calculate absolute timestamp using server time and error timestamp
const absoluteTime = Date.now() - (serverTime - serverEntry.t);
const errorEntry = {
code: serverEntry.c,
// Use custom message if provided, otherwise use error code lookup
message: serverEntry.m ? serverEntry.m : getErrorMessage(serverEntry.c),
timestamp: absoluteTime,
// If custom message provided, determine warning/error based on code range
isWarning: serverEntry.c >= 100,
tag1: serverEntry.t1 || 0,
tag2: serverEntry.t2 || 0,
tag3: serverEntry.t3 || 0
};
errorLog.push(errorEntry);
}
if (errorLog.length > 0) {
hasUnreadErrors = true;
}
updateInfoButtonIcon();
}
function clearErrorLog() {
// Send clear command to server
fetch(getURL('/json/state'), {
method: 'post',
body: JSON.stringify({clrErrLog: true}),
headers: {
'Content-Type': 'application/json'
}
})
.then(res => {
// Clear local state
errorLog = [];
hasUnreadErrors = false;
updateInfoButtonIcon();
const errorArea = gId('errorLogArea');
if (errorArea) {
errorArea.style.display = 'none';
}
})
.catch((error) => {
console.log('Error clearing log:', error);
});
}
function generateErrorLogHtml() {
if (errorLog.length === 0) return '';
let html = '';
for (let i = 0; i < errorLog.length; i++) {
const entry = errorLog[i];
// Use 24h format without seconds
const timeStr = new Date(entry.timestamp).toLocaleTimeString([], {hour12: false, hour: '2-digit', minute: '2-digit'});
const prefix = entry.isWarning ? 'Warning' : 'Error';
const color = entry.isWarning ? 'var(--c-y)' : 'var(--c-r)';
html += `<div style="margin: 2px 0; padding: 3px; word-wrap: break-word; color: ${color};">
${timeStr} ${prefix} ${entry.code}: ${entry.message}
</div>`;
}
return html;
}
function getLowestUnusedP()
{
var l = 1;
@@ -746,7 +894,17 @@ ${inforow("Flash size",i.flash," MB")}
${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")}
${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}
</table>`;
gId('kv').innerHTML = cn;
// Update error log area visibility and content
const errorArea = gId('errorLogArea');
if (errorLog.length > 0) {
errorArea.style.display = 'block';
gId('errorLogContent').innerHTML = generateErrorLogHtml();
} else {
errorArea.style.display = 'none';
}
// update all sliders in Info
d.querySelectorAll('#kv .sliderdisplay').forEach((sd,i) => {
let s = sd.previousElementSibling;
@@ -1518,40 +1676,13 @@ function readState(s,command=false)
gId('checkO3').checked = !(!i.o3);
if (s.error && s.error != 0) {
var errstr = "";
switch (s.error) {
case 1:
errstr = "Denied!";
break;
case 3:
errstr = "Buffer locked!";
break;
case 7:
errstr = "No RAM for buffer!";
break;
case 8:
errstr = "Effect RAM depleted!";
break;
case 9:
errstr = "JSON parsing error!";
break;
case 10:
errstr = "Could not mount filesystem!";
break;
case 11:
errstr = "Not enough space to save preset!";
break;
case 12:
errstr = "Preset not found.";
break;
case 13:
errstr = "Missing ir.json.";
break;
case 19:
errstr = "A filesystem error has occured.";
break;
// Add to error log for detailed tracking
addToErrorLog(s.error);
}
showToast('Error ' + s.error + ": " + errstr, true);
// Handle server-side error log
if (s.errorLog && s.errorLogTime) {
handleServerErrorLog(s.errorLog, s.errorLogTime);
}
selectedPal = i.pal;
@@ -1846,7 +1977,12 @@ function toggleInfo()
if (isNodes) toggleNodes();
if (isLv && isM) toggleLiveview();
isInfo = !isInfo;
if (isInfo) requestJson();
if (isInfo) {
requestJson();
// Mark errors as read when info panel is opened
hasUnreadErrors = false;
updateInfoButtonIcon();
}
gId('info').style.transform = (isInfo) ? "translateY(0px)":"translateY(100%)";
gId('buttonI').className = (isInfo) ? "active":"";
}

View File

@@ -48,7 +48,7 @@
To enable OTA, for security reasons you need to also enter the correct password!<br>
The password should be changed when OTA is enabled.<br>
<b>Disable OTA when not in use, otherwise an attacker can reflash device software!</b><br>
<i>Settings on this page are only changeable if OTA lock is disabled!</i><br>
<i>Settings on this page are only changable if OTA lock is disabled!</i><br>
Deny access to WiFi settings if locked: <input type="checkbox" name="OW"><br><br>
Factory reset: <input type="checkbox" name="RS"><br>
All settings and presets will be erased.<br><br>

View File

@@ -555,4 +555,10 @@ void sendDataWs(AsyncWebSocketClient * client = nullptr);
void XML_response(Print& dest);
void getSettingsJS(byte subPage, Print& dest);
//util.cpp - error logging
void addToErrorLog(byte errorCode, byte tag1 = 0, byte tag2 = 0, byte tag3 = 0, const char* customMessage = nullptr);
void clearErrorLog();
byte getErrorLogCount();
const struct ErrorLogEntry& getErrorLogEntry(byte index);
#endif

View File

@@ -64,22 +64,6 @@ namespace {
}
}
/**
* Deserialize a segment description from a JSON object and apply it to the specified segment slot.
*
* Parses and applies geometry, naming, grouping/spacing/offset, 2D bounds, mode, palette, colors
* (supports kelvin, hex, "r" random, object or array formats), per-LED assignments, options (on/frz/sel/rev/mi),
* speed/intensity, custom channels, checks, blend mode, and LOXONE mappings. The function may append a new
* segment, delete a segment, perform a repeat expansion to create multiple segments, and mark global state
* as changed when segment parameters differ.
*
* @param elem JSON object describing the segment (API format).
* @param it Default segment index to use when `elem["id"]` is not provided.
* @param presetId Optional preset identifier; when nonzero, preset-related side effects (e.g., playlist unloading)
* are suppressed or handled differently.
* @return true if the JSON was valid for the target id and the segment was applied (or created); false if the
* target id is out of range or the descriptor is invalid (e.g., attempting to create an empty segment).
*/
static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
{
byte id = elem["id"] | it;
@@ -221,7 +205,7 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
// JSON "col" array can contain the following values for each of segment's colors (primary, background, custom):
// "col":[int|string|object|array, int|string|object|array, int|string|object|array]
// int = Kelvin temperature or 0 for black
// string = hex representation of [WW]RRGGBB or "r" for random color
// string = hex representation of [WW]RRGGBB
// object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {})
// array = direct channel values [r,g,b,w] (w element being optional)
int rgbw[] = {0,0,0,0};
@@ -245,9 +229,6 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
if (kelvin == 0) seg.setColor(i, 0);
if (kelvin > 0) colorKtoRGB(kelvin, brgbw);
colValid = true;
} else if (hexCol[0] == 'r' && hexCol[1] == '\0') { // Random colors via JSON API in Segment object like col=["r","r","r"] · Issue #4996
setRandomColor(brgbw);
colValid = true;
} else { //HEX string, e.g. "FFAA00"
colValid = colorFromHexString(brgbw, hexCol);
}
@@ -387,6 +368,11 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
netDebugEnabled = root[F("debug")] | netDebugEnabled;
#endif
// Handle clear error log command
if (root[F("clrErrLog")]) {
clearErrorLog();
}
bool onBefore = bri;
getVal(root["bri"], bri);
if (bri != briOld) stateChanged = true;
@@ -658,7 +644,31 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
}
if (!forPreset) {
if (errorFlag) {root[F("error")] = errorFlag; errorFlag = ERR_NONE;} //prevent error message to persist on screen
if (errorFlag) {
root[F("error")] = errorFlag;
addToErrorLog(errorFlag); // Add to error log
errorFlag = ERR_NONE; // Reset error flag
}
// Add error log to JSON response
if (getErrorLogCount() > 0) {
JsonArray errors = root.createNestedArray(F("errorLog"));
for (byte i = 0; i < getErrorLogCount(); i++) {
const ErrorLogEntry& entry = getErrorLogEntry(i);
JsonObject err = errors.createNestedObject();
err[F("t")] = entry.timestamp;
err[F("c")] = entry.errorCode;
err[F("t1")] = entry.tag1;
err[F("t2")] = entry.tag2;
err[F("t3")] = entry.tag3;
// Add custom message if present
if (entry.customMessage != nullptr) {
err[F("m")] = entry.customMessage;
}
}
root[F("errorLogTime")] = millis(); // Current time for client calculations
}
root["ps"] = (currentPreset > 0) ? currentPreset : -1;
root[F("pl")] = currentPlaylist;

View File

@@ -897,7 +897,6 @@ static bool detectBootLoop() {
if (bl_crashcounter >= BOOTLOOP_THRESHOLD) {
DEBUG_PRINTLN(F("!BOOTLOOP DETECTED!"));
bl_crashcounter = 0;
if(bl_actiontracker > BOOTLOOP_ACTION_DUMP) bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // reset action tracker if out of bounds
result = true;
}
} else {
@@ -1126,3 +1125,72 @@ uint8_t perlin8(uint16_t x, uint16_t y) {
uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) {
return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 2015) >> 10) + 33168) >> 8; //scale to 16 bit, offset, then scale to 8bit
}
// Error logging system
struct ErrorLogEntry {
unsigned long timestamp; // millis() when error occurred
byte errorCode; // error number (8bit)
byte tag1; // future use tag 1
byte tag2; // future use tag 2
byte tag3; // future use tag 3
char* customMessage; // optional custom message string (nullptr if not used)
};
#define ERROR_LOG_SIZE 5
static ErrorLogEntry errorLog[ERROR_LOG_SIZE];
static byte errorLogIndex = 0;
static byte errorLogCount = 0;
// Error logging functions
void addToErrorLog(byte errorCode, byte tag1, byte tag2, byte tag3, const char* customMessage) {
// Free existing custom message if present
if (errorLog[errorLogIndex].customMessage != nullptr) {
delete[] errorLog[errorLogIndex].customMessage;
errorLog[errorLogIndex].customMessage = nullptr;
}
errorLog[errorLogIndex].timestamp = millis();
errorLog[errorLogIndex].errorCode = errorCode;
errorLog[errorLogIndex].tag1 = tag1;
errorLog[errorLogIndex].tag2 = tag2;
errorLog[errorLogIndex].tag3 = tag3;
// Copy custom message if provided
if (customMessage != nullptr) {
size_t len = strlen(customMessage);
errorLog[errorLogIndex].customMessage = new char[len + 1];
if (errorLog[errorLogIndex].customMessage != nullptr) {
strcpy(errorLog[errorLogIndex].customMessage, customMessage);
}
} else {
errorLog[errorLogIndex].customMessage = nullptr;
}
errorLogIndex = (errorLogIndex + 1) % ERROR_LOG_SIZE;
if (errorLogCount < ERROR_LOG_SIZE) {
errorLogCount++;
}
}
void clearErrorLog() {
// Free all custom message strings
for (byte i = 0; i < ERROR_LOG_SIZE; i++) {
if (errorLog[i].customMessage != nullptr) {
delete[] errorLog[i].customMessage;
errorLog[i].customMessage = nullptr;
}
}
errorLogIndex = 0;
errorLogCount = 0;
}
// Helper functions to read error log
byte getErrorLogCount() {
return errorLogCount;
}
const ErrorLogEntry& getErrorLogEntry(byte index) {
byte actualIndex = (errorLogIndex + ERROR_LOG_SIZE - errorLogCount + index) % ERROR_LOG_SIZE;
return errorLog[actualIndex];
}