diff --git a/tools/cdata.js b/tools/cdata.js index e02047e8b3..b5583a4a1c 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -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__length = ; + * - const uint8_t 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({ @@ -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); @@ -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", [ diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 2f8d5515fd..fe78fe2055 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -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! @@ -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); @@ -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) { @@ -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; @@ -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++; @@ -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) diff --git a/wled00/colors.cpp b/wled00/colors.cpp index bf2b69d73a..691a29bf90 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -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); diff --git a/wled00/colors.h b/wled00/colors.h index 376959fd65..158cfea650 100644 --- a/wled00/colors.h +++ b/wled00/colors.h @@ -123,7 +123,21 @@ CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(); void loadCustomPalettes(); extern std::vector 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); @@ -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 + diff --git a/wled00/data/index.js b/wled00/data/index.js index fe154783d0..b4ba74eee4 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -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; @@ -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)) { @@ -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()});`; } @@ -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} 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; diff --git a/wled00/json.cpp b/wled00/json.cpp index e8ebaaba29..82a13fddaf 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -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 @@ -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; @@ -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 255 - customPalettes.size()), a formatted "~ Custom N ~" + * name is written. Otherwise, the function parses a PROGMEM JSON-like string pointed + * to by src to locate the mode's quoted name (handles commas and quoted fields) and + * stops if an SR-extension marker '@' is encountered for that mode. + * + * The function always NUL-terminates dest and will truncate the name to fit maxLen. + * + * @param mode Index of the mode or palette to extract. + * @param src PROGMEM string source to parse, or JSON_mode_names / JSON_palette_names / nullptr. + * @param dest Caller-provided buffer to receive the NUL-terminated name (must be large enough). + * @param maxLen Maximum number of bytes to write into dest (including the terminating NUL). + * @return uint8_t Length of the resulting string written into dest (excluding the terminating NUL). + */ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen) { if (src == JSON_mode_names || src == nullptr) { @@ -230,7 +247,7 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe } else return 0; } - if (src == JSON_palette_names && mode > (GRADIENT_PALETTE_COUNT + 13)) { + if (src == JSON_palette_names && mode > 255-customPalettes.size()) { snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode); dest[maxLen-1] = '\0'; return strlen(dest); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 12e2862953..b9ff06ceb4 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -244,6 +244,24 @@ static bool captivePortal(AsyncWebServerRequest *request) return false; } +/** + * @brief Initialize and configure the HTTP server routes and handlers. + * + * Registers CORS/default headers and all web endpoints used by the device web UI and API, + * including static content routes, settings UI, JSON API (/json), file upload (/upload), + * OTA update endpoints (/update), optional pages (DMX, PixArt, PxMagic, CPAL, live views), + * WebSocket attachment, captive portal handling and a NotFound handler that routes API calls + * or serves a 404 page. Also installs the filesystem editor route (or an Access Denied + * stub) via createEditHandler and attaches an AsyncJsonWebHandler for JSON POSTs. + * + * Side effects: + * - Adds default HTTP headers (CORS). + * - Registers many server routes and their callbacks with global state handlers. + * - May set flags such as doReboot and configNeedsWrite from request handlers. + * - Enforces PIN/OTA lock and subnet restrictions inside sensitive endpoints (OTA, settings, cfg). + * + * This function does not return a value. + */ void initServer() { //CORS compatiblity @@ -470,29 +488,31 @@ void initServer() server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { if (captivePortal(request)) return; if (!showWelcomePage || request->hasArg(F("sliders"))) { - handleStaticContent(request, F("/index.htm"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_L); + handleStaticContent(request, F("/index.htm"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_length); } else { serveSettings(request); } }); -#ifdef WLED_ENABLE_PIXART +#ifndef WLED_DISABLE_2D + #ifdef WLED_ENABLE_PIXART static const char _pixart_htm[] PROGMEM = "/pixart.htm"; server.on(_pixart_htm, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_L); + handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_length); }); -#endif + #endif -#ifndef WLED_DISABLE_PXMAGIC + #ifndef WLED_DISABLE_PXMAGIC static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm"; server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_L); + handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_length); }); + #endif #endif static const char _cpal_htm[] PROGMEM = "/cpal.htm"; server.on(_cpal_htm, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_L); + handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_length); }); #ifdef WLED_ENABLE_WEBSOCKETS