Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions tools/cdata.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ async function minify(str, type = "plain") {
throw new Error("Unknown filter: " + type);
}

/**
* Inline-depends, minifies, gzip-compresses an HTML source and writes a C header array.
*
* Reads the HTML at sourceFile, inlines referenced resources, replaces repo/version placeholders,
* minifies the HTML, compresses it with gzip, converts the compressed bytes to a C-style hex array,
* and writes a header file to resultFile that defines:
* - const uint16_t PAGE_<page>_length = <length>;
* - const uint8_t PAGE_<page>[] PROGMEM = { ... };
*
* @param {string} sourceFile - Path to the source HTML file to inline and compress.
* @param {string} resultFile - Path where the generated C header file will be written.
* @param {string} page - Identifier used to name the generated symbols (e.g., "index", "pixart").
* @throws {Error} If inlining the HTML fails (propagates the inline error).
*/
async function writeHtmlGzipped(sourceFile, resultFile, page) {
console.info("Reading " + sourceFile);
inline.html({
Expand All @@ -143,7 +157,7 @@ async function writeHtmlGzipped(sourceFile, resultFile, page) {
console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes");
const array = hexdump(result);
let src = singleHeader;
src += `const uint16_t PAGE_${page}_L = ${result.length};\n`;
src += `const uint16_t PAGE_${page}_length = ${result.length};\n`;
src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`;
console.info("Writing " + resultFile);
fs.writeFileSync(resultFile, src);
Expand Down Expand Up @@ -244,9 +258,22 @@ if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.ar

writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
//writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');

writeChunks(
"wled00/data/cpal",
[
{
file: "cpal.htm",
name: "PAGE_cpal",
method: "gzip",
filter: "html-minify"
}
],
"wled00/html_cpal.h"
);

writeChunks(
"wled00/data",
[
Expand Down
84 changes: 69 additions & 15 deletions wled00/FX_fcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
*/
#include "wled.h"
#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h?
#include "palettes.h"

/*
Custom per-LED mapping has moved!
Expand Down Expand Up @@ -200,12 +199,15 @@ void Segment::deallocateData() {
}

/**
* If reset of this segment was requested, clears runtime
* settings of this segment.
* Must not be called while an effect mode function is running
* because it could access the data buffer and this method
* may free that data buffer.
*/
* @brief Clear pending runtime state when a segment is marked for reset.
*
* If this segment's reset flag is set and the segment is active, zeroes the
* segment's runtime data buffer (to avoid heap fragmentation), clears the
* pixel buffer, resets timing/step/call/aux counters, clears the reset flag,
* and, when GIF playback support is enabled, ends any ongoing image playback.
*
* Note: this routine is safe to call only when no effect mode is currently
* running for the segment — effect code may access the data/pixel buffers. */
void Segment::resetIfRequired() {
if (!reset || !isActive()) return;
//DEBUG_PRINTF_P(PSTR("-- Segment reset: %p\n"), this);
Expand All @@ -218,9 +220,31 @@ void Segment::resetIfRequired() {
#endif
}

/**
* @brief Selects and loads a palette into the supplied CRGBPalette16.
*
* Loads a palette identified by the numeric index `pal` into `targetPalette`.
* Supported palette sources (by index):
* - 0: the effect/default palette (resolved from _default_palette)
* - 1: runtime-random palette
* - 2–5: palettes derived from this segment's color slots (primary/secondary/tertiary combinations)
* - fastLED and built-in gradient palettes: mapped from the next contiguous index range
* - custom palettes: addressed from the high end (255, 254, ...) and stored in customPalettes
*
* If `pal` is outside the valid range for built-in/gradient/fastled indices it will be treated as 0
* (the default palette). When a custom palette index is selected it is loaded from customPalettes.
*
* @param targetPalette Palette object to populate (returned by reference).
* @param pal Numeric palette index selecting the source and layout (see summary above).
* @return CRGBPalette16& Reference to the populated `targetPalette`.
*/
CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0;
if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0;
// there is one randomy generated palette (1) followed by 4 palettes created from segment colors (2-5)
// those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71)
// then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette)
// palette 0 is a varying palette depending on effect and may be replaced by segment's color if so
// instructed in color_from_palette()
if (pal > FIXED_PALETTE_COUNT && pal < 255-customPalettes.size()+1) pal = 0; // out of bounds palette
//default palette. Differs depending on effect
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode()
switch (pal) {
Expand Down Expand Up @@ -256,13 +280,13 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
}
break;}
default: //progmem palettes
if (pal>245) {
if (pal > 255 - customPalettes.size()) {
targetPalette = customPalettes[255-pal]; // we checked bounds above
} else if (pal < 13) { // palette 6 - 12, fastled palettes
targetPalette = *fastledPalettes[pal-6];
} else if (pal < DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT+1) { // palette 6 - 12, fastled palettes
targetPalette = *fastledPalettes[pal-DYNAMIC_PALETTE_COUNT-1];
} else {
byte tcp[72];
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72);
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-(DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT)-1])), 72);
targetPalette.loadDynamicGradientPalette(tcp);
}
break;
Expand Down Expand Up @@ -529,6 +553,24 @@ Segment &Segment::setOption(uint8_t n, bool val) {
return *this;
}

/**
* @brief Set the effect (mode) for this segment.
*
* Sets the segment's mode to the first non-reserved effect at or after the
* provided index, optionally loading the effect's default parameters. If the
* new mode differs from the current one, a transition is started (a segment
* copy is created), the mode-specific defaults and palette are applied when
* requested, and the segment is marked for reset and state broadcast.
*
* @param fx Index of the desired effect/mode. If this index points to a
* reserved mode the next non-reserved mode is used. If the index is
* out of range the solid mode (index 0) is selected.
* @param loadDefaults When true, extract and apply the effect's default
* parameters (speed, intensity, custom values, mapping
* flags, sound simulation, mirror/reverse flags, etc.)
* and set the palette default when present.
* @return Segment& Reference to this segment (allows chaining).
*/
Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
// skip reserved
while (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4) == 0) fx++;
Expand Down Expand Up @@ -565,9 +607,21 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
return *this;
}

/**
* @brief Set the segment's palette by index.
*
* Validates the supplied palette index and, if it differs from the current
* palette, begins a palette transition and marks the segment's state as
* changed so the new palette is propagated to clients/hardware.
*
* If the provided index is outside the range of built-in or custom palettes,
* it is normalized to 0.
*
* @param pal Palette index (may be adjusted to a valid value).
* @return Segment& Reference to this segment (for chaining).
*/
Segment &Segment::setPalette(uint8_t pal) {
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes
if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0; // custom palettes
if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette
if (pal != palette) {
//DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal);
startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment)
Expand Down
20 changes: 19 additions & 1 deletion wled00/colors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,29 @@ CRGBPalette16 generateRandomPalette() // generate fully random palette
CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)));
}

/**
* @brief Loads user-defined color palettes from filesystem into runtime storage.
*
* Scans for files named "/palette0.json", "/palette1.json", ... up to
* WLED_MAX_CUSTOM_PALETTES and builds dynamic gradient palettes from any valid
* JSON found. Existing in-memory custom palettes are cleared before loading.
*
* Supported JSON formats for the "palette" array:
* - Pairs of [index, hexString] (e.g. [0, "FF0000", 128, "00FF00", ...]) where
* each pair is an index (0–255) followed by an RRGGBB or RRGGBBWW hex color.
* - Quads of [index, R, G, B] (e.g. [0, 255, 0, 0, 128, 0, 255, 0, ...]) where
* each group of four values is an index (0–255) followed by red/green/blue bytes.
*
* For each palette file the function converts the supplied entries into a
* temporary gradient table (supporting up to 18 color stops) and appends the
* resulting CRGBPalette16 to customPalettes. The loader stops at the first
* missing palette file.
*/
void loadCustomPalettes() {
byte tcp[72]; //support gradient palettes with up to 18 entries
CRGBPalette16 targetPalette;
customPalettes.clear(); // start fresh
for (int index = 0; index<10; index++) {
for (int index = 0; index < WLED_MAX_CUSTOM_PALETTES; index++) {
char fileName[32];
sprintf_P(fileName, PSTR("/palette%d.json"), index);

Expand Down
20 changes: 19 additions & 1 deletion wled00/colors.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,21 @@ CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette);
CRGBPalette16 generateRandomPalette();
void loadCustomPalettes();
extern std::vector<CRGBPalette16> customPalettes;
inline size_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); }
/**
* Get the total number of available palettes (built-in fixed palettes plus user-defined custom palettes).
*
* @return Total palette count (FIXED_PALETTE_COUNT + customPalettes.size()).
*/
inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); }
/**
* Pack an RGBW byte array into a 32-bit color value.
*
* The input must point to at least four bytes in order: R, G, B, W.
* Returns a uint32_t with layout 0xWWRRGGBB (white in the highest byte).
*
* @param rgbw Pointer to 4 bytes: {R, G, B, W}.
* @return 32-bit packed color in 0xWWRRGGBB format.
*/
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);
Expand All @@ -141,4 +155,8 @@ void setRandomColor(byte* rgb);

[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false);

// palettes
extern const TProgmemRGBPalette16* const fastledPalettes[];
extern const uint8_t* const gGradientPalettes[];
#endif

51 changes: 40 additions & 11 deletions wled00/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,23 @@ function redrawPalPrev()
});
}

/**
* Generate a CSS background rule showing a horizontal gradient preview for a palette.
*
* Uses the global `palettesData` array for palette entries. Each palette entry may be:
* - an array [posByte, r, g, b] where `posByte` is 0..255 mapped to 0..100%,
* - the literal 'r' to insert a random RGB color, or
* - a reference value whose second character is treated as a 1-based index into the DOM color-slot list (element with id "csl") and reads its `data-r`, `data-g`, `data-b` attributes.
*
* Special cases:
* - If the palette contains a single color, the function duplicates it to produce a two-color gradient.
* - If `palettesData` is not defined the function returns undefined.
* - If the requested palette id is not found the function returns the string `'display: none'`.
*
* @param {number|string} id - Palette identifier (index or key) into `palettesData`.
* @return {string|undefined} CSS declaration for a left-to-right linear-gradient (e.g. `"background: linear-gradient(to right, ...);"`),
* `'display: none'` when the palette is missing, or `undefined` if `palettesData` is not available.
*/
function genPalPrevCss(id)
{
if (!palettesData) return;
Expand All @@ -1042,8 +1059,7 @@ function genPalPrevCss(id)
}

var gradient = [];
for (let j = 0; j < paletteData.length; j++) {
const e = paletteData[j];
paletteData.forEach((e,j) => {
let r, g, b;
let index = false;
if (Array.isArray(e)) {
Expand All @@ -1065,9 +1081,8 @@ function genPalPrevCss(id)
if (index === false) {
index = Math.round(j / paletteData.length * 100);
}

gradient.push(`rgb(${r},${g},${b}) ${index}%`);
}
});

return `background: linear-gradient(to right,${gradient.join()});`;
}
Expand Down Expand Up @@ -3086,15 +3101,29 @@ let iSlide = 0, x0 = null, scrollS = 0, locked = false;

function unify(e) { return e.changedTouches ? e.changedTouches[0] : e; }

/**
* Return true if any class name in the provided list starts with "Iro".
*
* @param {Iterable<string>} classList - An iterable of class name strings (e.g., Element.classList or an array).
* @returns {boolean} True when at least one class name begins with "Iro", otherwise false.
*/
function hasIroClass(classList)
{
for (var i = 0; i < classList.length; i++) {
var element = classList[i];
if (element.startsWith('Iro')) return true;
}
return false;
}
//required by rangetouch.js
let found = false;
classList.forEach((e)=>{ if (e.startsWith('Iro')) found = true; });
return found;
}
/**
* Handle touch/drag start to lock page scrolling and initiate horizontal slide gestures.
*
* If the app is in PC mode or simplified UI, or the event target (or its parent) is marked
* to skip sliding (has class `noslide` or contains iro-related classes), the function returns
* without side effects. Otherwise it records the initial pointer X position and current
* scrollTop into globals used by the gesture handler, sets the global `locked` flag, and
* toggles the container's `smooth` class accordingly.
*
* @param {Event} e - Pointer/touch event from rangetouch (the originating target is inspected).
*/
function lock(e)
{
if (pcMode || simplifiedUI) return;
Expand Down
19 changes: 16 additions & 3 deletions wled00/json.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#include "wled.h"

#include "palettes.h"

#define JSON_PATH_STATE 1
#define JSON_PATH_INFO 2
#define JSON_PATH_STATE_INFO 3
Expand Down Expand Up @@ -689,6 +687,20 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
}
}

/**
* @brief Populate a JSON object with device and runtime information.
*
* Fills the provided JsonObject with system, hardware, network, and LED subsystem
* metadata used by the JSON API (version, build, LED counts and capabilities,
* palettes and modes counts, WiFi and filesystem stats, uptime, time, and usermod
* additions).
*
* The function writes several nested objects and arrays (for example "leds",
* "wifi", "fs", "maps") and a set of top-level fields consumed by clients.
*
* @param root JsonObject to populate. Must be a valid writable JSON object;
* the function will create nested objects/arrays inside it.
*/
void serializeInfo(JsonObject root)
{
root[F("ver")] = versionString;
Expand Down Expand Up @@ -770,7 +782,8 @@ void serializeInfo(JsonObject root)

root[F("fxcount")] = strip.getModeCount();
root[F("palcount")] = getPaletteCount();
root[F("cpalcount")] = customPalettes.size(); //number of custom palettes
root[F("cpalcount")] = customPalettes.size(); // number of custom palettes
root[F("cpalmax")] = WLED_MAX_CUSTOM_PALETTES; // maximum number of custom palettes

JsonArray ledmaps = root.createNestedArray(F("maps"));
for (size_t i=0; i<WLED_MAX_LEDMAPS; i++) {
Expand Down
Loading
Loading