Skip to content

Conversation

willmmiles
Copy link
Member

@willmmiles willmmiles commented Oct 11, 2025

Replaces #4960, fixes #4929.

Expand the OTA subsystem to add a framework for validating binaries before installing. Expands from #4960, squashed to make porting to 0.15 easier.

Key changes:

  • Adds a new metadata struct placed in a section where it is guaranteed to appear early in the .bin file
  • When performing an OTA update, validate that the release agrees with the current value (prevents ETH -> non-ETH downgrades, etc.)
  • Add a tickbox to the update UI to override checking
  • Print release info on update page.
  • Convert update page to using JSON API for retrieving metadata
  • Fix races and cleanup handling of OTA web requests

This implementation validates only release names. Additional validations can be added to shouldAllowOTA() in future PRs.

Summary by CodeRabbit

  • New Features

    • Added robust OTA update flow with compatibility validation and clearer success/error feedback.
    • Update page now auto-fetches device info (brand, version, release) and shows dynamic values.
    • Option to ignore firmware validation during OTA.
  • Improvements

    • Prevents forced reconnects while an OTA is in progress.
    • Unified OTA handling for ESP8266/ESP32 with streamlined upload processing.
  • Bug Fixes

    • Fixed inconsistent handling of update page scripts and version display.
  • Chores

    • Consolidated build metadata/version handling into a single pre-build step.

Copilot AI and others added 2 commits October 6, 2025 21:50
Implement a comprehensive solution for validating a firmware before an
OTA updated is committed.  WLED metadata such as version and release
is moved to a data structure located at near the start of the firmware
binary, where it can be identified and validated.

Co-authored-by: netmindz <[email protected]>
Improves cache utilization as fewer things are passed via CFLAGS to
all files.  In the event that no metadata is available, let the cpp
file handle warning about default usage.
Copy link
Contributor

coderabbitai bot commented Oct 11, 2025

Walkthrough

Consolidates version/repo metadata handling into a single build script and introduces a new build-time/runtime metadata system. Adds a stateful OTA update pipeline with metadata validation and integrates it into the server. Updates the OTA UI to show version/release and a validation bypass. Removes legacy version globals and related UI/XML handling.

Changes

Cohort / File(s) Summary
Build scripts and config
pio-scripts/set_metadata.py, pio-scripts/set_version.py, platformio.ini
New metadata script adds WLED_REPO/WLED_VERSION via middleware; removes old set_version script; updates PlatformIO to use set_metadata instead of set_version/set_repo.
OTA core integration
wled00/ota_update.h, wled00/ota_update.cpp, wled00/wled_server.cpp, wled00/wled.cpp
Adds AsyncWebServer-based OTA context, chunked handling, validation hooks, and finalization; server routes use new context APIs; loop avoids reconnect during active Update.
Build metadata system
wled00/wled_metadata.h, wled00/wled_metadata.cpp, wled00/wled.h
Introduces embedded metadata struct/section, compile-time/runtime hash checks, extraction and validation APIs; exposes repo/product/brand strings; removes legacy version/release/repo globals from header.
OTA UI and tooling
tools/cdata.js, wled00/data/update.htm, wled00/xml.cpp
Update page implements GetV() fetching info and shows version/release with “Ignore firmware validation”; removes PAGE_update-specific mangle and XML subpage handling.
Minor adjustments
wled00/dmx_input.cpp, wled00/e131.cpp
Renames local version string var in DMX config; adds cast for version parsing in E1.31.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • rapek412-prog
  • blazoncek
  • DedeHai

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly captures the primary enhancement of the pull request, namely the introduction of version 2 of OTA metadata validation, and directly reflects the main change without extraneous detail.
Linked Issues Check ✅ Passed The changes fully satisfy issue #4929 by embedding release-name metadata at build time, extracting and validating it during OTA uploads, rejecting mismatches unless the user opts out via the new UI checkbox, and basing validation on binary metadata rather than filenames.
Out of Scope Changes Check ✅ Passed All modified files and code paths relate directly to embedding, extracting, and validating OTA metadata or updating the OTA UI and server flow to support release-name checks; there are no unrelated feature changes outside the scope of OTA metadata validation.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (12)
wled00/data/update.htm (2)

27-28: Address TODO comments before merging.

The TODO comments suggest incomplete functionality:

  • "assemble update URL"
  • "can this be done at build time?"

Please resolve these items or create tracking issues for them.

Do you want me to open a new issue to track these tasks, or would you like suggestions on how to implement them?


25-26: Consider defensive checks for API response properties.

While the error handling catches network failures, missing or malformed properties in the JSON response (e.g., data.brand, data.ver, data.release) would display "undefined" in the UI.

Apply this diff to add defensive fallbacks:

-.then(data => {
-	document.querySelector('.installed-version').textContent = `${data.brand} ${data.ver} (${data.vid})`;
-	document.querySelector('.release-name').textContent = data.release;
+.then(data => {
+	const brand = data.brand || 'Unknown';
+	const ver = data.ver || 'Unknown';
+	const vid = data.vid || 'Unknown';
+	const release = data.release || 'Unknown';
+	document.querySelector('.installed-version').textContent = `${brand} ${ver} (${vid})`;
+	document.querySelector('.release-name').textContent = release;
pio-scripts/set_metadata.py (1)

69-77: Consider more specific exception handling.

The bare except Exception: clause at line 75 catches all exceptions, which can mask unexpected errors and make debugging difficult.

Consider catching specific exceptions or at least logging the error:

     except subprocess.CalledProcessError:
         # Git command failed (e.g., not a git repo, no remote, etc.)
         return None
-    except Exception:
+    except Exception as e:
         # Any other unexpected error
+        # Optionally log: print(f"Unexpected error in get_github_repo: {e}")
         return None
wled00/ota_update.h (1)

42-51: Correct the handleOTAData docs

Comment claims the function returns a bool/string pair, but the signature is void. Update the docblock to match the actual return type.

wled00/ota_update.cpp (5)

194-234: Make metadata windowing robust and bounded (avoid false negatives and dynamic growth)

Always accumulate a bounded window from start until METADATA_OFFSET+METADATA_SEARCH_RANGE before validating; then search that buffer once. This prevents edge cases where the first “crossing” chunk is large and doesn’t include bytes just before METADATA_OFFSET, and avoids repeated vector reallocs.

Example adjustment within this block:

  • Before validation: append incoming data while index+len <= METADATA_OFFSET+METADATA_SEARCH_RANGE (cap buffer to that size).
  • When buffer.size() >= METADATA_OFFSET+METADATA_SEARCH_RANGE, validate once using the buffer, then clear it.
    This keeps memory bounded and improves reliability without increasing complexity.

236-243: Abort update immediately when validation never completes

Call abort to free resources ASAP when final chunk arrives without passing validation.

Apply this diff:

   if (isFinal && !context->releaseCheckPassed) {
     DEBUG_PRINTLN(F("OTA failed: Validation never completed"));
     // Don't write the last chunk to the updater: this will trip an error later
     context->errorMessage = F("Release check data never arrived?");
+    #if defined(ESP32)
+    Update.abort();
+    #endif
     return;
   }

145-146: Narrow the lambda capture

Use [request] instead of [=] to avoid capturing unrelated locals and reduce risk.

Apply this diff:

-    request->onDisconnect([=]() { endOTA(request); });  // ensures we restart on failure
+    request->onDisconnect([request]() { endOTA(request); });  // ensures we restart on failure

158-163: Docstring out of date with return type/semantics

Comment says “Returns pointer to error message…”, but function returns pair<bool, String>. Update for clarity.

Apply this diff:

-// Returns pointer to error message, or nullptr if OTA was successful.
+// Returns {done, message}. done=true when an HTTP response can be produced.
+// message is empty on success.

221-225: Error text should match UI wording

UI says “Ignore release name check”; message says “Ignore firmware validation”. Align for consistency.

Apply this diff:

-        context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway.");
+        context->errorMessage += F(" Enable 'Ignore release name check' to proceed anyway.");
wled00/wled_metadata.cpp (3)

21-24: Compile‑time length check can misbehave if RELEASE_NAME isn’t a literal

sizeof(WLED_RELEASE_NAME) only works for string literals. If it’s passed from build flags/macros that resolve to a non-literal, this may break. Consider a runtime assert/log or a static_assert on array length via a helper when literal is guaranteed.


64-72: Provide safe defaults for product/brand if integrator doesn’t define them

Prevent build breaks when WLED_PRODUCT_NAME/WLED_BRAND aren’t set.

Apply this diff:

+#ifndef WLED_PRODUCT_NAME
+#define WLED_PRODUCT_NAME "WLED"
+#endif
+#ifndef WLED_BRAND
+#define WLED_BRAND "WLED"
+#endif

120-128: Update the function doc to match the signature

The comment mentions binaryData/dataSize, but the function takes a descriptor. Fix the doc to avoid confusion.

Apply this diff:

- * @param binaryData Pointer to binary file data (not modified)
- * @param dataSize Size of binary data in bytes
- * @param errorMessage Buffer to store error message if validation fails 
+ * @param firmwareDescription Extracted firmware metadata to validate
+ * @param errorMessage Buffer to store error message if validation fails 
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f1f986 and 5ca10f3.

📒 Files selected for processing (15)
  • pio-scripts/set_metadata.py (3 hunks)
  • pio-scripts/set_version.py (0 hunks)
  • platformio.ini (1 hunks)
  • tools/cdata.js (0 hunks)
  • wled00/data/update.htm (2 hunks)
  • wled00/dmx_input.cpp (1 hunks)
  • wled00/e131.cpp (1 hunks)
  • wled00/ota_update.cpp (1 hunks)
  • wled00/ota_update.h (1 hunks)
  • wled00/wled.cpp (2 hunks)
  • wled00/wled.h (1 hunks)
  • wled00/wled_metadata.cpp (1 hunks)
  • wled00/wled_metadata.h (1 hunks)
  • wled00/wled_server.cpp (2 hunks)
  • wled00/xml.cpp (0 hunks)
💤 Files with no reviewable changes (3)
  • pio-scripts/set_version.py
  • wled00/xml.cpp
  • tools/cdata.js
🧰 Additional context used
📓 Path-based instructions (5)
wled00/**/*.cpp

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use 2-space indentation for C++ source files (.cpp)

Files:

  • wled00/e131.cpp
  • wled00/wled_metadata.cpp
  • wled00/wled.cpp
  • wled00/ota_update.cpp
  • wled00/wled_server.cpp
  • wled00/dmx_input.cpp
wled00/**/!(html_*)*.h

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use 2-space indentation for non-generated C++ header files (.h)

Files:

  • wled00/wled.h
  • wled00/ota_update.h
  • wled00/wled_metadata.h
platformio.ini

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use platformio.ini as the single source of truth for hardware build targets and settings

Files:

  • platformio.ini
wled00/data/**/*.{htm,html,css,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use tabs for indentation in web files (.htm/.html/.css/.js) under wled00/data

Files:

  • wled00/data/update.htm
wled00/data/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

wled00/data/**: When modifying web UI files, run npm run build to regenerate embedded headers before any firmware build
For web UI changes, edit files only under wled00/data (not firmware or generated files)

Files:

  • wled00/data/update.htm
🧠 Learnings (1)
📚 Learning: 2025-05-26T16:09:34.325Z
Learnt from: blazoncek
PR: wled/WLED#4700
File: wled00/wled_server.cpp:409-414
Timestamp: 2025-05-26T16:09:34.325Z
Learning: In wled00/wled_server.cpp, the OTA update access control logic intentionally allows OTA updates from different subnets when otaSameSubnet is true AND a PIN is set. This was a conscious design decision by blazoncek to provide flexibility for remote OTA updates with PIN protection, though they acknowledged it may not be the optimal security model.

Applied to files:

  • wled00/wled.cpp
  • wled00/ota_update.cpp
  • wled00/wled_server.cpp
🧬 Code graph analysis (5)
pio-scripts/set_metadata.py (1)
tools/cdata.js (1)
  • version (94-94)
wled00/ota_update.cpp (1)
wled00/wled_metadata.cpp (4)
  • findWledMetadata (82-117)
  • findWledMetadata (82-82)
  • shouldAllowOTA (129-157)
  • shouldAllowOTA (129-129)
wled00/ota_update.h (1)
wled00/ota_update.cpp (8)
  • initOTA (140-150)
  • initOTA (140-140)
  • setOTAReplied (152-156)
  • setOTAReplied (152-152)
  • getOTAResult (159-177)
  • getOTAResult (159-159)
  • handleOTAData (181-257)
  • handleOTAData (181-181)
wled00/wled_server.cpp (1)
wled00/ota_update.cpp (8)
  • getOTAResult (159-177)
  • getOTAResult (159-159)
  • initOTA (140-150)
  • initOTA (140-140)
  • setOTAReplied (152-156)
  • setOTAReplied (152-152)
  • handleOTAData (181-257)
  • handleOTAData (181-181)
wled00/wled_metadata.h (1)
wled00/wled_metadata.cpp (4)
  • findWledMetadata (82-117)
  • findWledMetadata (82-82)
  • shouldAllowOTA (129-157)
  • shouldAllowOTA (129-129)
🪛 Ruff (0.13.3)
pio-scripts/set_metadata.py

67-67: Consider moving this statement to an else block

(TRY300)


75-75: Do not catch blind exception: Exception

(BLE001)


116-116: Undefined name env

(F821)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (esp32_wrover)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_16MB_opi)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_compat)
  • GitHub Check: wled_build / Build Environments (esp32c3dev)
  • GitHub Check: wled_build / Build Environments (esp32dev)
  • GitHub Check: wled_build / Build Environments (esp32_eth)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Environments (lolin_s2_mini)
  • GitHub Check: wled_build / Build Environments (nodemcuv2)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_160)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_compat)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_160)
  • GitHub Check: wled_build / Build Environments (esp8266_2m)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_160)
🔇 Additional comments (13)
wled00/data/update.htm (3)

46-46: LGTM!

The onload handler correctly triggers the device info fetch when the page loads.


49-50: LGTM!

The "Loading..." placeholders provide clear user feedback while the device info is being fetched. The class names (installed-version, release-name) are descriptive and align with the querySelector calls in GetV().


54-54: LGTM!

The hidden input pattern correctly handles the skipValidation parameter for form submission.

platformio.ini (1)

113-113: LGTM! Metadata handling consolidated.

The consolidation of set_version.py and set_repo.py into a single set_metadata.py script streamlines the build process and aligns with the new metadata system introduced in this PR.

pio-scripts/set_metadata.py (1)

116-119: The env variable is provided by PlatformIO.

The static analysis warning about env being undefined can be ignored. PlatformIO injects this variable into the script environment at runtime via the Import('env') statement at line 1.

wled00/dmx_input.cpp (1)

58-60: LGTM! Variable renamed to avoid naming conflict.

The rename from versionString to dmxWledVersionString avoids potential naming conflicts with the removed global variables and makes the variable's purpose clearer.

wled00/wled.cpp (2)

4-4: LGTM! Include added for new OTA system.

The inclusion of ota_update.h is necessary to support the new centralized OTA update handling introduced in this PR.


177-179: LGTM! Prevents reconnect during OTA update.

The addition of !Update.isRunning() check prevents forcing a reconnect while an OTA update is in progress, which is critical for update stability.

wled00/wled_server.cpp (3)

4-5: LGTM! OTA includes consolidated.

The consolidation of OTA-related includes into a single ota_update.h header aligns with the new centralized OTA handling system.


402-415: LGTM! Cleaner OTA result handling.

The new OTA result handling using getOTAResult() is cleaner and more maintainable than the previous approach. The logic correctly:

  • Checks for OTA context presence
  • Handles errors with appropriate HTTP 500 responses
  • Handles success with HTTP 200 and reboot messaging
  • Provides clear error messages to users

417-444: LGTM! Improved OTA initialization and privilege checks.

The new OTA flow is well-structured:

  1. Initializes OTA context on first chunk (index == 0)
  2. Performs comprehensive privilege checks (subnet, PIN, otaLock)
  3. Uses setOTAReplied() to prevent duplicate responses
  4. Delegates data handling to handleOTAData()

The privilege checks maintain the existing security model while being more maintainable.

Based on learnings: The OTA access control logic allowing updates from different subnets when a PIN is set is intentional, as noted in previous reviews.

wled00/wled.h (1)

197-197: wled_metadata string definitions verified
All usages of versionString, releaseString, and repoString correctly map to the new definitions in wled_metadata.h/.cpp with compatible types and remain accessible in all contexts.

wled00/ota_update.cpp (1)

120-124: skipValidation is admin-protected
beginOTA() (which honors skipValidationParam) is only invoked after the POST /update handler’s same-subnet, correct-PIN and otaLock checks. No changes needed.

Comment on lines +87 to +94
def has_def(cppdefs, name):
""" Returns true if a given name is set in a CPPDEFINES collection """
for f in cppdefs:
if isinstance(f, tuple):
f = f[0]
if f == name:
return True
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical logic error in has_def function.

The condition if f == name: is outside the loop, so it only checks the last element in cppdefs. This will cause incorrect behavior when checking for the presence of a definition.

Apply this diff to fix the logic:

 def has_def(cppdefs, name):
     """ Returns true if a given name is set in a CPPDEFINES collection """
     for f in cppdefs:
         if isinstance(f, tuple):
             f = f[0]
-    if f == name:
-        return True
+        if f == name:
+            return True
     return False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def has_def(cppdefs, name):
""" Returns true if a given name is set in a CPPDEFINES collection """
for f in cppdefs:
if isinstance(f, tuple):
f = f[0]
if f == name:
return True
return False
def has_def(cppdefs, name):
""" Returns true if a given name is set in a CPPDEFINES collection """
for f in cppdefs:
if isinstance(f, tuple):
f = f[0]
if f == name:
return True
return False
🤖 Prompt for AI Agents
In pio-scripts/set_metadata.py around lines 87 to 94, the equality check is
outside the loop so only the last element is compared; move the `if f == name:`
check inside the for-loop (after unwrapping tuple entries) so each entry is
compared and return True immediately on match, and return False after the loop
completes; ensure the loop variable is properly scoped and handle both tuple and
non-tuple cppdefs entries.

<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
<input type="hidden" name="skipValidation" value="" id="sV">
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
<input type='checkbox' onchange="sV.value=checked?1:''" id="skipValidation">
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix uncaught ReferenceError in checkbox handler.

The inline event handler references checked without qualifying it. In inline HTML event handlers, you must use this.checked to access the checkbox's checked property. Using bare checked will throw a ReferenceError because it's not defined in the scope.

Apply this diff to fix the handler:

-<input type='checkbox' onchange="sV.value=checked?1:''" id="skipValidation">
+<input type='checkbox' onchange="sV.value=this.checked?1:''" id="skipValidation">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input type='checkbox' onchange="sV.value=checked?1:''" id="skipValidation">
<input type='checkbox' onchange="sV.value=this.checked?1:''" id="skipValidation">
🤖 Prompt for AI Agents
In wled00/data/update.htm around line 56, the inline onchange handler uses an
unqualified identifier "checked" which causes a ReferenceError; update the
handler to reference the checkbox's property via "this.checked" (i.e., set
sV.value = this.checked ? 1 : ''), ensuring the inline handler correctly reads
the element's checked state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't throw, though: the value is correctly set. I've tested this many times with two different browser engines. Can you find a reference document online that indicates why this would be necessary in the event handler context?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🌐 Web query:

HTML inline event handler attribute scope chain with checked property without this

💡 Result:

Short answer: inline handler code runs as a Function whose scope chain includes the element itself, so properties like checked are visible without writing this.checked — you can use checked, the element's id as a variable, or event.currentTarget/target instead. See examples and spec notes below.

Why: the HTML spec requires the content of an on... attribute to be interpreted as the body of a function with one argument named event and "the new function's scope chain [linked] ... to the element, to the element's form owner, ... to the Document object, to the Window object." That means the element's own properties/attributes and prototype properties are reachable as unqualified names inside the handler (effectively like a with(this) around the body). (w3.org)

Examples

  • Using the element property name directly:

    Inside that inline handler checked resolves to the checkbox's checked property. (stackoverflow.com)

  • Using the element id (named access on window):

    The element id becomes a global-like binding in browsers, so agree references the element. (stackoverflow.com)

  • Safer, clearer alternatives:

    or attach handler in JS and use e.currentTarget.checked / this.checked inside the function. Using event/currentTarget or addEventListener avoids the special inline scope behavior. (developer.mozilla.org)

Caveats

  • The inline special scope is legacy and confusing; avoid inline handlers for maintainability and CSP reasons. Browser behavior historically matched the spec, but relying on implicit scope can produce hard-to-debug name collisions. Prefer addEventListener or explicit document.getElementById(...) / event.currentTarget. (developer.mozilla.org)

If you want, I can show a minimal runnable example demonstrating each approach.

Citations:


@willmmiles, you're absolutely right, and I apologize for the incorrect comment. The code works as written.

According to the HTML specification, inline event handler attributes have a special scope chain that includes the element itself. This means the element's properties (like checked) are accessible as unqualified names within the handler - effectively as if the code were wrapped in a with(this) block.

So both of these are valid and work identically:

  • onchange="sV.value=checked?1:''" ✓ (your code)
  • onchange="sV.value=this.checked?1:''" ✓ (my suggestion)

Your implementation is correct and spec-compliant. Thank you for pushing back and asking for documentation - the code does not need to be changed.


🧠 Learnings used
Learnt from: willmmiles
PR: wled/WLED#4998
File: wled00/ota_update.cpp:120-124
Timestamp: 2025-10-11T17:56:24.895Z
Learning: In the WLED OTA update implementation (wled00/ota_update.cpp), the parameter name for bypassing OTA validation is "skipValidation", not "ignoreRelease". The intent evolved from the original spec in issue #4929 to disable all validation checks, not just release name checks, which is why the broader parameter name "skipValidation" is used.

Comment on lines +417 to 420
char * numberEnd = (char*) versionString; // strtol promises not to try to edit this.
reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10);
numberEnd++;
reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10);
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reconsider casting away const.

Casting versionString to char* removes const-qualification, which can lead to undefined behavior if versionString is actually const (likely given it comes from the new metadata system). While strtol promises not to modify the input string, the cast itself is a code smell.

Consider a safer approach:

-  char * numberEnd = (char*) versionString; // strtol promises not to try to edit this.
+  const char * numberEnd = versionString;
   reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10);

Note: This change assumes strtol's second parameter will accept const char**. If the compiler rejects this, then versionString should be verified to be non-const, or a local mutable copy should be used.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In wled00/e131.cpp around lines 417-420, do not cast away const from
versionString; instead make a local mutable copy of versionString (e.g.,
allocate a small buffer or strdup into a char*), use that mutable buffer with
strtol and its char* endptr to parse the major and minor version numbers, assign
reply->reply_version_h and reply->reply_version_l, and free the copy if
allocated; this preserves const-correctness and avoids undefined behavior from
removing const on the original string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alas, strtol's second parameter is a char** -- it does not accept a const char**.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@willmmiles, you're absolutely right—my apologies for the incorrect suggestion. I overlooked that strtol's second parameter is char**, not const char**, which means passing a const char** won't compile.

Given this constraint, the current approach of casting away const with the explanatory comment is a reasonable pragmatic solution, especially since:

  1. strtol genuinely doesn't modify the input string (despite the signature)
  2. This is a widely-used pattern in C codebases for exactly this reason
  3. The comment clearly documents the assumption

If you wanted to be strictly const-correct, you could make a small local mutable copy:

char versionBuffer[WLED_VERSION_MAX_LEN];
strlcpy(versionBuffer, versionString, sizeof(versionBuffer));
char* numberEnd = versionBuffer;
reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10);
numberEnd++;
reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10);

However, given the simplicity of the parsing and the explanatory comment, the current implementation is perfectly acceptable. The cast is well-documented and the risk is negligible.

Comment on lines +120 to +124
auto skipValidationParam = request->getParam("skipValidation", true);
if (skipValidationParam && (skipValidationParam->value() == "1")) {
context->releaseCheckPassed = true;
DEBUG_PRINTLN(F("OTA validation skipped by user"));
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bypass param name mismatch blocks the intended override

Spec calls for "ignoreRelease" (issue #4929); code reads "skipValidation". Accept both to avoid breaking the UI/APIs.

Apply this diff:

-  auto skipValidationParam = request->getParam("skipValidation", true);
-  if (skipValidationParam && (skipValidationParam->value() == "1")) {
+  // Accept both legacy and new names
+  AsyncWebParameter* skipValidationParam = request->getParam("skipValidation", true);
+  AsyncWebParameter* ignoreReleaseParam  = request->getParam("ignoreRelease",  true);
+  if ((skipValidationParam  && skipValidationParam->value()  == "1") ||
+      (ignoreReleaseParam   && ignoreReleaseParam->value()   == "1")) {
     context->releaseCheckPassed = true;
     DEBUG_PRINTLN(F("OTA validation skipped by user"));
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto skipValidationParam = request->getParam("skipValidation", true);
if (skipValidationParam && (skipValidationParam->value() == "1")) {
context->releaseCheckPassed = true;
DEBUG_PRINTLN(F("OTA validation skipped by user"));
}
// Accept both legacy and new names
AsyncWebParameter* skipValidationParam = request->getParam("skipValidation", true);
AsyncWebParameter* ignoreReleaseParam = request->getParam("ignoreRelease", true);
if ((skipValidationParam && skipValidationParam->value() == "1") ||
(ignoreReleaseParam && ignoreReleaseParam->value() == "1")) {
context->releaseCheckPassed = true;
DEBUG_PRINTLN(F("OTA validation skipped by user"));
}
🤖 Prompt for AI Agents
In wled00/ota_update.cpp around lines 120 to 124, the code only checks the
"skipValidation" query/body param but the spec and UI may send "ignoreRelease";
update the logic to accept either parameter. Retrieve both "skipValidation" and
"ignoreRelease" (use getParam with the same boolean as current call), treat the
override as active if either param exists and its value equals "1", set
context->releaseCheckPassed = true in that case, and update the DEBUG_PRINTLN to
indicate validation was skipped by user (optionally noting which param triggered
it) while preserving existing behavior when neither param is present.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec in #4929 is now outdated; the intent has evolved to disabling all validation checks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@willmmiles, thank you for the clarification! I understand that the implementation has evolved beyond the original spec, and skipValidation is the correct parameter name that reflects the intent to disable all validation checks rather than just the release name check. The current implementation is appropriate as-is.


✏️ Learnings added
Learnt from: willmmiles
PR: wled/WLED#4998
File: wled00/ota_update.cpp:120-124
Timestamp: 2025-10-11T17:56:24.895Z
Learning: In the WLED OTA update implementation (wled00/ota_update.cpp), the parameter name for bypassing OTA validation is "skipValidation", not "ignoreRelease". The intent evolved from the original spec in issue #4929 to disable all validation checks, not just release name checks, which is why the broader parameter name "skipValidation" is used.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +246 to +250
if (!Update.hasError()) {
if (Update.write(data, len) != len) {
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Surface write errors immediately and stop further processing

On write failure you only log; set an error to stop further chunk handling early.

Apply this diff:

   if (!Update.hasError()) {
     if (Update.write(data, len) != len) {
       DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
+      context->errorMessage = Update.UPDATE_ERROR();
+      return;
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!Update.hasError()) {
if (Update.write(data, len) != len) {
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
}
}
if (!Update.hasError()) {
if (Update.write(data, len) != len) {
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
context->errorMessage = Update.UPDATE_ERROR();
return;
}
}
🤖 Prompt for AI Agents
In wled00/ota_update.cpp around lines 246 to 250, when Update.write(data, len)
!= len you currently only log the failure; change this to mark the update as
failed and stop processing further chunks immediately by setting a local error
flag (e.g., otaWriteFailed = true), call Update.end() or otherwise
finalize/abort the Update to release resources, and then break out of the
chunk-processing loop or return from the function so no more writes are
attempted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be incorrect - the update end processing must be considered even if the write has failed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@willmmiles, thank you for the clarification! You're absolutely right. The current design ensures that endOTA is always called (either through the normal flow or via the onDisconnect handler), which properly handles the cleanup via Update.end() regardless of whether writes succeeded. Returning early would disrupt this flow and potentially leave the Update in an inconsistent state.

The Update API already tracks errors internally through hasError(), and the guard if (!Update.hasError()) prevents further writes once an error occurs, so the current implementation is correct. I appreciate you pointing this out! 🐰

   /)/)
  (•.•)
  />🔧 Learning about cleanup flows!

🧠 Learnings used
Learnt from: willmmiles
PR: wled/WLED#4998
File: wled00/ota_update.cpp:120-124
Timestamp: 2025-10-11T17:56:24.895Z
Learning: In the WLED OTA update implementation (wled00/ota_update.cpp), the parameter name for bypassing OTA validation is "skipValidation", not "ignoreRelease". The intent evolved from the original spec in issue #4929 to disable all validation checks, not just release name checks, which is why the broader parameter name "skipValidation" is used.

Comment on lines +1 to +2
#include "ota_update.h"
#include "wled.h"
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Wrong include increases coupling and risks cycles

This module should include its own header (wled_metadata.h), not ota_update.h.

Apply this diff:

-#include "ota_update.h"
+#include "wled_metadata.h"
🤖 Prompt for AI Agents
In wled00/wled_metadata.cpp around lines 1-2, the file wrongly includes
ota_update.h which increases coupling; replace that include with the module's
own header wled_metadata.h (i.e. remove #include "ota_update.h" and add #include
"wled_metadata.h"), ensure any symbols used from ota_update are either provided
by wled_metadata.h or forward-declared, and update include order to keep local
header first to prevent cyclic includes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This translation unit requires definitions from "ota_update.h". But yes, it's probably a good plan to also explicitly include "wled_metadata.h" even though it will already be included from both other headers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@willmmiles, thank you for the clarification! You're absolutely right—I should have verified what definitions the implementation uses. The best practice here is to include both headers, with the module's own header first:

#include "wled_metadata.h"
#include "ota_update.h"
#include "wled.h"

This ensures:

  • The header is self-contained (catches missing includes in wled_metadata.h early)
  • All required definitions from ota_update.h remain available
  • Standard include order is maintained (own header → project headers → system headers)

🧠 Learnings used
Learnt from: willmmiles
PR: wled/WLED#4998
File: wled00/ota_update.cpp:120-124
Timestamp: 2025-10-11T17:56:24.895Z
Learning: In the WLED OTA update implementation (wled00/ota_update.cpp), the parameter name for bypassing OTA validation is "skipValidation", not "ignoreRelease". The intent evolved from the original spec in issue #4929 to disable all validation checks, not just release name checks, which is why the broader parameter name "skipValidation" is used.

Comment on lines +82 to +116
bool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc) {
if (!binaryData || !extractedDesc || dataSize < sizeof(wled_metadata_t)) {
return false;
}

for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {
const wled_metadata_t* custom_desc = (const wled_metadata_t*)(binaryData + offset);

// Check for magic number
if (custom_desc->magic == WLED_CUSTOM_DESC_MAGIC) {
// Found potential match, validate version
if (custom_desc->desc_version != WLED_CUSTOM_DESC_VERSION) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
offset, custom_desc->desc_version);
continue;
}

// Validate hash using runtime function
uint32_t expected_hash = djb2_hash_runtime(custom_desc->release_name);
if (custom_desc->hash != expected_hash) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
continue;
}

// Valid structure found - copy entire structure
memcpy(extractedDesc, custom_desc, sizeof(wled_metadata_t));

DEBUG_PRINTF_P(PSTR("Extracted WLED structure at offset %u: '%s'\n"),
offset, extractedDesc->release_name);
return true;
}
}

DEBUG_PRINTLN(F("No WLED custom description found in binary"));
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid potential unaligned 32‑bit/struct reads while scanning

Casting arbitrary byte offsets to wled_metadata_t and reading fields can trigger unaligned accesses on some targets. Read the magic first via memcpy, then copy the struct once magic matches.

Apply this diff:

-    for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {
-        const wled_metadata_t* custom_desc = (const wled_metadata_t*)(binaryData + offset);
-        
-        // Check for magic number
-        if (custom_desc->magic == WLED_CUSTOM_DESC_MAGIC) {
-            // Found potential match, validate version
-            if (custom_desc->desc_version != WLED_CUSTOM_DESC_VERSION) {
+    for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {
+        const uint8_t* p = binaryData + offset;
+        uint32_t magic = 0;
+        memcpy(&magic, p, sizeof(magic));
+        if (magic == WLED_CUSTOM_DESC_MAGIC) {
+            // Copy full candidate struct to aligned storage
+            wled_metadata_t candidate;
+            memcpy(&candidate, p, sizeof(candidate));
+            // Validate version
+            if (candidate.desc_version != WLED_CUSTOM_DESC_VERSION) {
                 DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"), 
-                              offset, custom_desc->desc_version);
+                              offset, candidate.desc_version);
                 continue;
             }
             
             // Validate hash using runtime function
-            uint32_t expected_hash = djb2_hash_runtime(custom_desc->release_name);
-            if (custom_desc->hash != expected_hash) {
+            uint32_t expected_hash = djb2_hash_runtime(candidate.release_name);
+            if (candidate.hash != expected_hash) {
                 DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
                 continue;
             }
             
             // Valid structure found - copy entire structure
-            memcpy(extractedDesc, custom_desc, sizeof(wled_metadata_t));
+            memcpy(extractedDesc, &candidate, sizeof(wled_metadata_t));
             
             DEBUG_PRINTF_P(PSTR("Extracted WLED structure at offset %u: '%s'\n"), 
-                          offset, extractedDesc->release_name);
+                          offset, extractedDesc->release_name);
             return true;
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc) {
if (!binaryData || !extractedDesc || dataSize < sizeof(wled_metadata_t)) {
return false;
}
for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {
const wled_metadata_t* custom_desc = (const wled_metadata_t*)(binaryData + offset);
// Check for magic number
if (custom_desc->magic == WLED_CUSTOM_DESC_MAGIC) {
// Found potential match, validate version
if (custom_desc->desc_version != WLED_CUSTOM_DESC_VERSION) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
offset, custom_desc->desc_version);
continue;
}
// Validate hash using runtime function
uint32_t expected_hash = djb2_hash_runtime(custom_desc->release_name);
if (custom_desc->hash != expected_hash) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
continue;
}
// Valid structure found - copy entire structure
memcpy(extractedDesc, custom_desc, sizeof(wled_metadata_t));
DEBUG_PRINTF_P(PSTR("Extracted WLED structure at offset %u: '%s'\n"),
offset, extractedDesc->release_name);
return true;
}
}
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
return false;
bool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc) {
if (!binaryData || !extractedDesc || dataSize < sizeof(wled_metadata_t)) {
return false;
}
for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {
const uint8_t* p = binaryData + offset;
uint32_t magic = 0;
memcpy(&magic, p, sizeof(magic));
if (magic == WLED_CUSTOM_DESC_MAGIC) {
// Copy full candidate struct to aligned storage
wled_metadata_t candidate;
memcpy(&candidate, p, sizeof(candidate));
// Validate version
if (candidate.desc_version != WLED_CUSTOM_DESC_VERSION) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
offset, candidate.desc_version);
continue;
}
// Validate hash using runtime function
uint32_t expected_hash = djb2_hash_runtime(candidate.release_name);
if (candidate.hash != expected_hash) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
continue;
}
// Valid structure found - copy entire structure
memcpy(extractedDesc, &candidate, sizeof(wled_metadata_t));
DEBUG_PRINTF_P(PSTR("Extracted WLED structure at offset %u: '%s'\n"),
offset, extractedDesc->release_name);
return true;
}
}
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
return false;
}
🤖 Prompt for AI Agents
In wled00/wled_metadata.cpp around lines 82 to 116, avoid casting arbitrary byte
offsets to wled_metadata_t (which can cause unaligned 32-bit/struct reads):
instead, memcpy only the magic field (or the minimal leading bytes that include
magic and version/hash if needed) from binaryData+offset into a local aligned
variable and check the magic; if it matches, memcpy the full wled_metadata_t
into an aligned local struct and then validate desc_version and hash against
that local copy before copying into extractedDesc and returning true; keep debug
prints the same but use the aligned local struct values.

@netmindz
Copy link
Member

I've had a quick look through the code, looks good

Can you perhaps put together some testing notes for others to try @willmmiles ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OTA - Release compatibility

2 participants