diff --git a/usermods/boot_lor/library.json b/usermods/boot_lor/library.json new file mode 100644 index 0000000000..396e406459 --- /dev/null +++ b/usermods/boot_lor/library.json @@ -0,0 +1,11 @@ +{ + "name": "boot_lor", + "version": "1.0.0", + "description": "Set WLED realtime override at boot", + "frameworks": "arduino", + "platforms": "espressif32", + "build": { + "libArchive": false, + "srcDir": "." + } +} diff --git a/usermods/boot_lor/readme.md b/usermods/boot_lor/readme.md new file mode 100644 index 0000000000..7845e6bd9b --- /dev/null +++ b/usermods/boot_lor/readme.md @@ -0,0 +1,139 @@ +# Usermods API v2 boot_lor usermod + +## Installation + +Add `boot_lor` to `custom_usermods` in your PlatformIO environment and compile! + +## Overview + +`boot_lor` is a usermod that sets the WLED realtime override mode (`lor`) at boot. + +It is designed for setups where WLED is primarily controlled via external APIs (e.g. HomeKit, Home Assistant, or custom integrations), and realtime streaming protocols (such as DDP) are only used occasionally. + +By default, WLED enables realtime streaming at boot when data is received. This can interfere with API-driven control flows. This usermod ensures that a desired `lor` mode (typically `lor:2`) is enforced during startup. + +--- + +## Use Case + +This usermod is intended for environments where: + +- WLED is primarily controlled via an external system (e.g. HomeKit via Homebridge, Home Assistant, or direct API usage) +- Multiple controllers should behave as **independent devices** under normal operation +- Realtime streaming (DDP, E1.31, etc.) is used **only when explicitly enabled** + +### Example scenario + +- Two WLED controllers are used as separate lights in HomeKit +- A secondary setup uses WLED "virtual LEDs" to mirror one controller to another via DDP +- This works well for effects and synchronized control + +**Problem:** +- At boot, realtime communication may take control as soon as packets arrive, overriding API-based control unexpectedly. +- Using JSON or HTTP API in a boot preset does not reliably set lor + +**Solution:** +Set `lor:2` at boot to disable realtime takeover by default. +When realtime control is desired, manually switch back to `lor:0`. + +--- + +## Behavior + +The usermod applies the configured `lor` value during startup using the following sequence: + +### Default behavior + +```text +WiFi connected → (optional delay) → apply lor → assert for N seconds → stop +``` + +- Waits for network connectivity +- Waits an additional configurable delay (if set) +- Applies the configured `lor` value +- Reasserts it for a short period to allow system "settling" +- Stops running after completion + +--- + +## Configuration + +The following options are available under `"boot_lor"` in the WLED config: + +```json +"boot_lor": { + "bootLor": 2, + "additionalWaitSec": 0, + "assertForSec": 10 +} +``` + +### Options + +| Name | Type | Default | Description | +|---------------------|------|---------|-------------| +| `bootLor` | int | `2` | Realtime override mode to apply. Valid values: `-1` (disabled), `0`, `1`, `2` | +| `additionalWaitSec` | int | `0` | Additional delay (in seconds) after trigger (connection) before applying | +| `assertForSec` | int | `10` | Duration (in seconds) to reassert the value after first application | + +--- + +## Recommended Settings + +For most DDP / API-first setups: + +```json +{ + "bootLor": 2, + "additionalWaitSec": 0, + "assertForSec": 10 +} +``` + +This ensures: + +- Realtime streaming does not take control unexpectedly at boot +- The system waits for network connectivity before acting +- The setting is reinforced briefly to avoid race conditions + +--- + +## Notes + +- It does **not** interfere with realtime streaming once the assertion period is over (see startup sequence) +- It simply ensures a predictable startup state + +--- + +## Status + +After completion, the usermod stops running and has no ongoing impact on system performance. + +--- + +## JSON Info + +The current state is exposed under `info -> u`: + +```text +Boot LOR: [bootLor, state, realtimeOverride] +``` + +Where: +- `state` is one of `waiting`, `applied`, or `finished` + +--- + +## Tested +- Build: esp32dev +- Runtime: tested on ESP32-D0WD-V3 + +--- + +## Summary + +This usermod provides a simple and reliable way to: + +- Default to API-based control at boot +- Avoid unintended realtime takeover +- Retain full flexibility to enable realtime modes when desired diff --git a/usermods/boot_lor/usermod_boot_lor.cpp b/usermods/boot_lor/usermod_boot_lor.cpp new file mode 100644 index 0000000000..cfb2a47065 --- /dev/null +++ b/usermods/boot_lor/usermod_boot_lor.cpp @@ -0,0 +1,186 @@ +#include "wled.h" + +/** + * @brief Applies a configured WLED realtime override mode during startup. + * + * The usermod waits for network connectivity, waits an optional additional + * delay, then applies the configured realtime override value and reasserts it + * for a short settling period. + */ +class BootLorUsermod : public Usermod { +private: + static const char _name[] PROGMEM; ///< JSON configuration key for this usermod. + + int8_t bootLor = 2; ///< Realtime override mode to apply; -1 disables the usermod. + uint8_t additionalWaitSec = 0; ///< Additional delay, in seconds, after network connection. + uint16_t assertForSec = 10; ///< Duration, in seconds, to reassert the configured override. + + unsigned long connectedMs = 0; ///< Timestamp when network connectivity became available. + unsigned long firstAppliedMs = 0; ///< Timestamp of the first successful realtime override application. + + bool connectedSeen = false; ///< True once the network connection timestamp has been captured. + bool applied = false; ///< True once the configured realtime override has been applied. + bool finished = false; ///< True once the assertion window has completed. + + /** + * @brief Checks whether a realtime override value is valid. + * + * @param value Realtime override value to validate. + * @return true if the value is -1, 0, 1, or 2. + */ + bool isValidLor(int8_t value) const { + return value >= -1 && value <= 2; + } + + /** + * @brief Checks whether this usermod should run. + * + * @return true when the configured realtime override is valid and not disabled. + */ + bool isEnabled() const { + return isValidLor(bootLor) && bootLor >= 0; + } + + /** + * @brief Stores the timestamp used to start the post-connection wait period. + * + * @param now Current millis() timestamp. + */ + void setConnectedTime(unsigned long now) { + connectedMs = now; + connectedSeen = true; + } + + /** + * @brief Checks whether the configured post-connection wait period has elapsed. + * + * @return true once enough time has passed since network connection. + */ + bool additionalWaitElapsed() const { + const unsigned long waitMs = (unsigned long)additionalWaitSec * 1000UL; + return millis() - connectedMs >= waitMs; + } + + /** + * @brief Checks whether all conditions are met to apply the realtime override. + * + * @return true when the usermod is enabled, connected, and past its wait period. + */ + bool readyToApply() const { + if (!isEnabled() || finished || !connectedSeen) return false; + if (!WLED_CONNECTED) return false; + + return additionalWaitElapsed(); + } + + /** + * @brief Applies the configured realtime override and records first application time. + */ + void applyBootLor() { + if (realtimeOverride != bootLor) { + realtimeOverride = bootLor; + } + + if (!applied) { + applied = true; + firstAppliedMs = millis(); + } + } + + /** + * @brief Applies and reasserts the configured realtime override when ready. + */ + void runIfReady() { + if (!readyToApply()) return; + + applyBootLor(); + + if (millis() - firstAppliedMs > (unsigned long)assertForSec * 1000UL) { + finished = true; + } + } + +public: + /** + * @brief Initializes the usermod. + */ + void setup() override {} + + /** + * @brief Starts the wait timer when networking becomes available. + */ + void connected() override { + if (!connectedSeen) { + setConnectedTime(millis()); + } + } + + /** + * @brief Runs the non-blocking assertion state machine. + */ + void loop() override { + runIfReady(); + } + + /** + * @brief Adds this usermod's settings to the WLED configuration JSON. + * + * @param root Root configuration JSON object. + */ + void addToConfig(JsonObject& root) override { + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) top = root.createNestedObject(FPSTR(_name)); + + top[F("bootLor")] = bootLor; + top[F("additionalWaitSec")] = additionalWaitSec; + top[F("assertForSec")] = assertForSec; + } + + /** + * @brief Reads this usermod's settings from the WLED configuration JSON. + * + * @param root Root configuration JSON object. + * @return true if this usermod's configuration object exists. + */ + bool readFromConfig(JsonObject& root) override { + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) return false; + + int8_t newBootLor = top[F("bootLor")] | bootLor; + if (isValidLor(newBootLor)) bootLor = newBootLor; + + additionalWaitSec = top[F("additionalWaitSec")] | additionalWaitSec; + assertForSec = top[F("assertForSec")] | assertForSec; + + return true; + } + + /** + * @brief Adds runtime status information to the WLED info JSON. + * + * @param root Root info JSON object. + */ + void addToJsonInfo(JsonObject& root) override { + JsonObject user = root[F("u")]; + if (user.isNull()) user = root.createNestedObject(F("u")); + + JsonArray infoArr = user.createNestedArray(F("Boot LOR")); + infoArr.add(bootLor); + infoArr.add(finished ? F("finished") : applied ? F("applied") : F("waiting")); + infoArr.add(realtimeOverride); + } + + /** + * @brief Returns the registered usermod identifier. + * + * @return USERMOD_ID_BOOT_LOR. + */ + uint16_t getId() override { + return USERMOD_ID_BOOT_LOR; + } +}; + +const char BootLorUsermod::_name[] PROGMEM = "boot_lor"; + +static BootLorUsermod boot_lor_usermod; +REGISTER_USERMOD(boot_lor_usermod); diff --git a/wled00/const.h b/wled00/const.h index 62d9c45f4d..cd15abbf4a 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -225,6 +225,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" #define USERMOD_ID_USER_FX 58 //Usermod "user_fx" +#define USERMOD_ID_BOOT_LOR 59 //Usermod "boot_lor" //Wifi encryption type #ifdef WLED_ENABLE_WPA_ENTERPRISE