Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
11 changes: 11 additions & 0 deletions usermods/boot_lor/library.json
Original file line number Diff line number Diff line change
@@ -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": "."
}
}
144 changes: 144 additions & 0 deletions usermods/boot_lor/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# 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
- Optionally waits for the first UDP packet (recommended for DDP setups)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- 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,
"waitForUdpPacket": true,
"additionalWaitSec": 0,
"assertForSec": 10
}
```

### Options

| Name | Type | Default | Description |
|---------------------|------|---------|-------------|
| `bootLor` | int | `2` | Realtime override mode to apply. Valid values: `-1` (disabled), `0`, `1`, `2` |
| `waitForUdpPacket` | bool | `true` | Wait for first UDP packet before starting the delay timer |
| `additionalWaitSec` | int | `0` | Additional delay (in seconds) after trigger (connection or UDP) 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,
"waitForUdpPacket": true,
"additionalWaitSec": 0,
"assertForSec": 10
}
```

This ensures:

- Realtime streaming does not take control unexpectedly at boot
- The system waits for actual network activity before acting
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- The setting is reinforced briefly to avoid race conditions

---

## Notes

- This usermod does **not** block or modify UDP traffic
- It does **not** interfere with realtime streaming once `lor` is manually changed
- It simply ensures a predictable startup state
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

---

## 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
185 changes: 185 additions & 0 deletions usermods/boot_lor/usermod_boot_lor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#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 constexpr const char* _name = "boot_lor"; ///< 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[_name];
if (top.isNull()) top = root.createNestedObject(_name);

top["bootLor"] = bootLor;
top["additionalWaitSec"] = additionalWaitSec;
top["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[_name];
if (top.isNull()) return false;

int8_t newBootLor = top["bootLor"] | bootLor;
if (isValidLor(newBootLor)) bootLor = newBootLor;

additionalWaitSec = top["additionalWaitSec"] | additionalWaitSec;
assertForSec = top["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["u"];
if (user.isNull()) user = root.createNestedObject("u");

JsonArray infoArr = user.createNestedArray("Boot LOR");
infoArr.add(bootLor);
infoArr.add(finished ? "finished" : applied ? "applied" : "waiting");
infoArr.add(realtimeOverride);
}

/**
* @brief Returns the registered usermod identifier.
*
* @return USERMOD_ID_BOOT_LOR.
*/
uint16_t getId() override {
return USERMOD_ID_BOOT_LOR;
}
};

static BootLorUsermod boot_lor_usermod;
REGISTER_USERMOD(boot_lor_usermod);
1 change: 1 addition & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down