From eaecca9c9e1e4ae47f1fa142b5725fc8bef2bfab Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 23 May 2026 14:12:30 +0100 Subject: [PATCH 01/22] docs(dali_gear): update readme to match implemented behaviour - Document non-standard combined DAPC+CCT master flow - Document QUERY COLOUR TYPE (0xE7) backward frame response (0x02) - Remove stale 'No backward frame responses' limitation - Fix config key names (pin_rx/pin_tx, correct defaults 14/17) - Add Waveshare Pico-DALI2 as example hardware - Add note about enabling White Balance Correction for RGB-only strips - Add lib_deps to platformio_override.ini example snippet --- usermods/dali_gear/library.json | 7 + usermods/dali_gear/readme.md | 140 ++++++++ usermods/dali_gear/usermod_dali_gear.cpp | 437 +++++++++++++++++++++++ wled00/const.h | 1 + wled00/pin_manager.h | 3 +- 5 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 usermods/dali_gear/library.json create mode 100644 usermods/dali_gear/readme.md create mode 100644 usermods/dali_gear/usermod_dali_gear.cpp diff --git a/usermods/dali_gear/library.json b/usermods/dali_gear/library.json new file mode 100644 index 0000000000..bfe6562fb6 --- /dev/null +++ b/usermods/dali_gear/library.json @@ -0,0 +1,7 @@ +{ + "name": "dali_gear", + "build": { "libArchive": false }, + "dependencies": { + "qqqDALI": "https://github.com/netmindz/DALI-Lighting-Interface.git#fix/esp32-volatile-cast" + } +} diff --git a/usermods/dali_gear/readme.md b/usermods/dali_gear/readme.md new file mode 100644 index 0000000000..d300d8f49a --- /dev/null +++ b/usermods/dali_gear/readme.md @@ -0,0 +1,140 @@ +# DALI Gear Usermod + +Makes WLED act as a **DALI control gear** (IEC 62386) — i.e. a light that responds to commands from an external DALI master (wall dimmer, BMS, building automation system, etc.). + +DALI (Digital Addressable Lighting Interface) is a standardised two-wire bus protocol for lighting control. This usermod puts WLED on the bus as a gear device: the DALI master sends brightness/on/off/colour commands, and WLED adjusts its LEDs accordingly. + +> **ESP32 only.** The hardware timer API used for Manchester decoding is not available on ESP8266. + +## Hardware + +You need a DALI bus interface circuit to convert between the DALI bus voltage (9.5–22.5 V) and the ESP32's 3.3 V GPIO levels. + +### Minimal DIY circuit (from [qqqlab/DALI-Lighting-Interface](https://github.com/qqqlab/DALI-Lighting-Interface)) + +``` +3.3V ESP32 5.6V ___ + Zener +----|___|---- 12V Power Supply + ___ Diode | 220 Ω +RX ---+-----|___|---|>|----------+------------- DALI+ + | 10K | + +-+ | + | | 100K ___ |/ PNP DALI BUS + +-+ TX ---|___|----| Transistor + | 1K |\ + | V +GND --+---------------------------+------------- DALI- +``` + +> **Note:** For this circuit the TX polarity must be inverted. The transistor pulls the bus low when the GPIO is HIGH. Configure your TX pin accordingly and invert the output in hardware or adjust the HAL callbacks in the source. + +Commercial DALI interface modules (e.g. Waveshare Pico-DALI2, Mikroe DALI Click) are a simpler alternative. + +### Pin assignment + +| Signal | Direction | Description | +|---|---|---| +| RX | Input | Reads DALI bus state (high = bus idle, low = bus asserted) | +| TX | Output | Drives DALI bus — needed for backward frame responses to QUERY commands | + +Default pins are **RX=14, TX=17** (Waveshare Pico-DALI2). + +Configure both pins in the WLED usermod settings page. + +## Configuration + +| Setting | Default | Description | +|---|---|---| +| Enabled | false | Enable/disable the usermod | +| pin_rx | 14 | GPIO for DALI bus RX | +| pin_tx | 17 | GPIO for DALI bus TX | +| daliAddr | -1 | Short address (0–63) to respond to, or -1 to respond to broadcast only | + +## DALI commands handled + +### Direct Arc Power Control (DAPC) + +When the master sends a DAPC frame, the arc level (0–254) is mapped linearly to WLED brightness (0–255). WLED's existing gamma correction handles perceptual uniformity at the LED output. + +| DALI arc level | WLED behaviour | +|---|---| +| 0 | Turn off | +| 1–254 | Set brightness proportionally, turn on | +| 255 (mask) | Ignored (no change) | + +### Indirect commands + +| Command | Number | WLED action | +|---|---|---| +| OFF | 0 | Turn off | +| UP | 1 | Increase brightness by 10 | +| DOWN | 2 | Decrease brightness by 10 | +| STEP UP | 3 | Increase brightness by 1 | +| STEP DOWN | 4 | Decrease brightness by 1 | +| RECALL MAX LEVEL | 5 | Set brightness to 255, turn on | +| RECALL MIN LEVEL | 6 | Set brightness to 1, turn on | +| STEP DOWN AND OFF | 7 | Decrease by 1; turn off if at minimum | +| ON AND STEP UP | 8 | Turn on if off, then increase by 10 | +| GO TO LAST ACTIVE LEVEL | 10 | Restore last brightness before turn-off | + +### DT8 colour temperature (IEC 62386-209) + +Colour temperature commands from a DALI master are mapped to WLED's CCT value via `strip.setCCT()`. The mired value is converted to Kelvin (`K = 1,000,000 / mireds`). WLED's accepted range is 1900–10091 K; values outside this range are clamped. + +Two CCT application flows are supported: + +**Standard flow (IEC 62386-209 §11.3.4.1):** + +1. `SET DTR0` — lower byte of colour temperature in mireds +2. `SET DTR1` — upper byte of colour temperature in mireds +3. `ENABLE DEVICE TYPE 8` — activates DT8 interpretation +4. `SET TEMPORARY COLOUR TEMPERATURE` (0xE1) — loads DTR0+DTR1 into temporary register +5. `ACTIVATE` (0xE2) — applies the temporary colour temperature + +**Non-standard combined flow (observed in some masters):** + +Some DALI masters skip the `0xE1` + `0xE2` sequence and instead apply the colour temperature implicitly alongside the subsequent DAPC command. The usermod detects this: if DTR0/DTR1 are set and DT8 is active when a DAPC frame arrives, the CCT is applied at the same time as the brightness change. + +**QUERY COLOUR TYPE (0xE7):** + +Some masters query the gear's colour capabilities before sending CCT commands. The usermod responds with a backward frame value of `0x02` (bit 1 = Tc colour temperature supported), sent 4 ms after the query frame to meet the DALI spec requirement (7Te–22Te ≈ 2.9–9.2 ms settling window). + +`SET DTR0`, `SET DTR1`, and `ENABLE DEVICE TYPE 8` are sniffed as broadcast-level frames regardless of the configured `daliAddr`. + +### CCT on RGB-only strips + +`strip.setCCT()` adjusts the colour temperature via WLED's internal CCT pipeline. For an RGB-only strip (no dedicated white or CCT channel), **White Balance Correction** must be enabled in WLED LED settings (Config → LED Preferences → White Balance Correction) for the CCT value to affect the LED output. This makes WLED apply a colour temperature correction on the RGB channels. + +## Addressing + +DALI addressing works as follows: + +- **Broadcast** (`0xFE`/`0xFF`): always handled regardless of `daliAddr` setting +- **Short address** (0–63): set `daliAddr` to the address the DALI master has assigned to this device +- **Group address**: not handled + +Set `daliAddr` to `-1` (default) to respond only to broadcast commands. This is useful for a single-gear installation. + +## Enabling the usermod + +Add to your `platformio_override.ini`: + +```ini +[env:esp32dev] +custom_usermods = dali_gear +lib_deps = + ${env.lib_deps} + https://github.com/netmindz/DALI-Lighting-Interface.git#fix/esp32-volatile-cast +``` + +## Limitations + +- No short address commissioning via DALI bus (set the address manually in WLED config) +- No group address support +- No DALI scene mapping + +## Dependencies + +- [qqqlab/DALI-Lighting-Interface](https://github.com/qqqlab/DALI-Lighting-Interface) (GPL-3.0) + Low-level Manchester-encoded DALI bus driver by qqqlab. + This usermod uses the fork at `https://github.com/netmindz/DALI-Lighting-Interface.git#fix/esp32-volatile-cast` which includes an ESP32 volatile-cast fix. diff --git a/usermods/dali_gear/usermod_dali_gear.cpp b/usermods/dali_gear/usermod_dali_gear.cpp new file mode 100644 index 0000000000..f734395409 --- /dev/null +++ b/usermods/dali_gear/usermod_dali_gear.cpp @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// DALI Gear Usermod for WLED +// Makes WLED act as a DALI control gear (IEC 62386) — i.e. a light that +// responds to commands from an external DALI master (wall dimmer, BMS, etc.). +// +// Phase 1: RX-only bus listening. Handles DAPC (direct arc power control) +// and basic indirect commands (OFF, MAX, MIN, UP, DOWN, LAST ACTIVE LEVEL). +// Phase 2: DT8 colour temperature (IEC 62386-209). Handles SET DTR0/DTR1 +// special commands, ENABLE DEVICE TYPE 8, SET TEMPORARY COLOUR TEMPERATURE, +// ACTIVATE to map DALI Tc (mireds) → WLED CCT (Kelvin), and backward frame +// response to QUERY COLOUR TYPE (0xe7) to declare Tc support to the master. +// +// Hardware: requires a DALI bus interface circuit (see readme.md). +// ESP32 only — uses hardware timer API not available on ESP8266. + +#include "wled.h" + +#ifndef ARDUINO_ARCH_ESP32 +#error "dali_gear usermod requires ESP32 (hardware timer API not available on ESP8266)" +#endif + +#include + +// --------------------------------------------------------------------------- +// DALI frame parsing helpers +// --------------------------------------------------------------------------- + +// Returns true if the address byte of a forward frame is addressed to us. +// daliAddr: our configured short address (0–63), or -1 to accept broadcast only. +static bool daliAddressedToMe(uint8_t addrByte, int8_t daliAddr) { + // Broadcast: 1111 111x (0xFE or 0xFF) + if ((addrByte | 0x01) == 0xFF) return true; + // Short address: 0AAA AAA x — top bit 0 + if (!(addrByte & 0x80) && daliAddr >= 0) { + uint8_t frameAddr = (addrByte >> 1) & 0x3F; + return frameAddr == (uint8_t)daliAddr; + } + // Group address: 100A AAA x — not handled in phase 1 + return false; +} + +// Map a DALI arc level (1–254) to WLED bri (1–255). +// Linear mapping is correct here: WLED's gamma correction handles the LED +// output curve, serving the same perceptual-uniformity purpose as DALI's +// logarithmic arc power table. +static uint8_t daliLevelToWledBri(uint8_t level) { + if (level == 0) return 0; + // level 1–254 → bri 1–255 + return (uint8_t)(((uint16_t)level * 255u + 127u) / 254u); +} + +// --------------------------------------------------------------------------- +// ISR and timer — file-scope so the ISR can reach the Dali instance +// --------------------------------------------------------------------------- + +static Dali _dali; +static hw_timer_t *_daliTimer = nullptr; + +static void ARDUINO_ISR_ATTR daliTimerISR() { + _dali.timer(); +} + +// --------------------------------------------------------------------------- +// Usermod class +// --------------------------------------------------------------------------- + +class DaliGearUsermod : public Usermod { + private: + bool _enabled = false; + bool _initDone = false; + int8_t _rxPin = 14; // default: Waveshare Pico-DALI2 RX + int8_t _txPin = 17; // default: Waveshare Pico-DALI2 TX + int8_t _daliAddr = -1; // -1 = respond to broadcast only + uint8_t _lastDaliLevel = 0; // last DALI arc level received (for info panel) + + // DT8 (IEC 62386-209) colour temperature state + uint8_t _dtr0 = 0; // Data Transfer Register 0 (low byte of Tc mireds) + uint8_t _dtr1 = 0; // Data Transfer Register 1 (high byte of Tc mireds) + bool _dt8Active = false; // true after ENABLE DEVICE TYPE 8 + uint16_t _tempCCT = 0; // temporary colour temperature register (mireds) + uint16_t _lastCCTKelvin = 0; // last applied CCT in Kelvin (for info panel) + + // Backward frame scheduling — DALI requires response 7Te–22Te (≈2.9–9.2ms) + // after the forward frame stop bits. We schedule via timestamp. + uint8_t _pendingBF = 0; // backward frame byte to send (0 = none pending) + uint32_t _pendingBFTime = 0; // millis() threshold — send when now >= this + + static const char _name[]; + static const char _enabled_key[]; + + // --------------------------------------------------------------------------- + // Bus HAL callbacks (static so they can be passed as function pointers) + // --------------------------------------------------------------------------- + static uint8_t busIsHigh() { + return digitalRead(_rxPinStatic); + } + static void busSetLow() { + digitalWrite(_txPinStatic, LOW); + } + static void busSetHigh() { + digitalWrite(_txPinStatic, HIGH); + } + + // Static copies of pins needed by the HAL callbacks + static int8_t _rxPinStatic; + static int8_t _txPinStatic; + + // --------------------------------------------------------------------------- + // Schedule a DALI backward frame to be sent after the mandatory settling time. + // DALI IEC 62386-102 requires 7Te (≈2.9ms) min, 22Te (≈9.2ms) max. + // We target 4ms — safely inside the window even with loop jitter. + // --------------------------------------------------------------------------- + void scheduleBF(uint8_t byte) { + _pendingBF = byte; + _pendingBFTime = millis() + 4; // 4ms after frame received in loop() + } + + // --------------------------------------------------------------------------- + // Apply a DALI arc level to WLED + // --------------------------------------------------------------------------- + void applyLevel(uint8_t daliLevel) { + _lastDaliLevel = daliLevel; + if (daliLevel == 0) { + briLast = bri ? bri : briLast; // preserve last brightness for toggle + bri = 0; + } else { + bri = daliLevelToWledBri(daliLevel); + } + stateUpdated(CALL_MODE_DIRECT_CHANGE); + } + + // --------------------------------------------------------------------------- + // Apply a colour temperature in mireds to WLED via strip.setCCT(Kelvin) + // --------------------------------------------------------------------------- + void applyCCT(uint16_t mireds) { + if (mireds == 0) return; // 0 mireds is undefined / mask value — ignore + // Convert mireds to Kelvin. Clamp to WLED's accepted range (1900–10091 K). + uint32_t kelvin = 1000000UL / mireds; + if (kelvin < 1900) kelvin = 1900; + if (kelvin > 10091) kelvin = 10091; + _lastCCTKelvin = (uint16_t)kelvin; + strip.setCCT(_lastCCTKelvin); + stateUpdated(CALL_MODE_DIRECT_CHANGE); + DEBUG_PRINTF("[DALI] CCT applied: %u mireds → %u K\n", mireds, (unsigned)kelvin); + } + + // --------------------------------------------------------------------------- + // Handle an indirect DALI command (S=1 in address byte) + // --------------------------------------------------------------------------- + void handleCommand(uint8_t cmd) { + switch (cmd) { + case DALI_OFF: + applyLevel(0); + break; + case DALI_UP: + if (bri > 0) { + bri = (bri > 245) ? 255 : bri + 10; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + } + break; + case DALI_DOWN: + if (bri > 10) bri -= 10; + else if (bri > 0) bri = 1; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_RECALL_MAX_LEVEL: + bri = 255; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_RECALL_MIN_LEVEL: + bri = 1; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_GO_TO_LAST_ACTIVE_LEVEL: + bri = briLast ? briLast : 128; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_ON_AND_STEP_UP: + if (bri == 0) bri = 1; + else bri = (bri > 245) ? 255 : bri + 10; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_STEP_UP: + if (bri < 255) bri++; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_STEP_DOWN: + if (bri > 1) bri--; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_STEP_DOWN_AND_OFF: + if (bri <= 1) bri = 0; + else bri--; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + + // DT8 (IEC 62386-209) application extended commands. + // These are only valid when preceded by ENABLE DEVICE TYPE 8 (addr=0xC1, cmd=8). + // 0xE1 = SET TEMPORARY COLOUR TEMPERATURE — loads DTR0+DTR1 into temp register. + // 0xE2 = ACTIVATE — applies the temporary colour temperature. + // 0xE7 = QUERY COLOUR TYPE — master asks which DT8 colour modes are supported. + // Response bitmask (IEC 62386-209 §11.3.4.2): + // bit 0 = XY colour, bit 1 = Tc colour temperature, bit 2 = Primary N, + // bit 3 = RGBWAF. We support Tc only → respond 0x02. + case 0xE1: + if (_dt8Active) { + _tempCCT = ((uint16_t)_dtr1 << 8) | _dtr0; + DEBUG_PRINTF("[DALI] SET TEMPORARY COLOUR TEMPERATURE: %u mireds (DTR1=0x%02x DTR0=0x%02x)\n", + _tempCCT, _dtr1, _dtr0); + } else { + DEBUG_PRINTLN(F("[DALI] SET TEMPORARY COLOUR TEMPERATURE received but DT8 not active — ignored")); + } + break; + case 0xE2: + if (_dt8Active && _tempCCT > 0) { + DEBUG_PRINTF("[DALI] ACTIVATE: applying %u mireds\n", _tempCCT); + applyCCT(_tempCCT); + } else { + DEBUG_PRINTF("[DALI] ACTIVATE: skipped (dt8Active=%d tempCCT=%u)\n", _dt8Active, _tempCCT); + } + _dt8Active = false; + break; + + case 0xE7: + // QUERY COLOUR TYPE — respond regardless of _dt8Active state + // (master needs to know our capabilities before enabling DT8) + DEBUG_PRINTLN(F("[DALI] QUERY COLOUR TYPE → scheduling backward frame 0x02 (Tc supported)")); + scheduleBF(0x02); + break; + + default: + DEBUG_PRINTF("[DALI] unhandled command 0x%02x (%u) — ignored\n", cmd, cmd); + break; + } + } + + public: + + void setup() override { + if (!_enabled || _rxPin < 0 || _txPin < 0) { + _initDone = true; + return; + } + + // Claim pins via WLED pin manager + if (!PinManager::allocatePin(_rxPin, false, PinOwner::UM_DALI_GEAR) || + !PinManager::allocatePin(_txPin, true, PinOwner::UM_DALI_GEAR)) { + DEBUG_PRINTLN(F("[DALI] Pin allocation failed")); + _enabled = false; + _initDone = true; + return; + } + + // Configure GPIO + pinMode(_rxPin, INPUT); + pinMode(_txPin, OUTPUT); + digitalWrite(_txPin, HIGH); // idle bus state (not asserting bus) + + // Store static copies for HAL callbacks + _rxPinStatic = _rxPin; + _txPinStatic = _txPin; + + _dali.begin(busIsHigh, busSetLow, busSetHigh); + + // Hardware timer: IDF v4 API + // Timer 1 (timer 0 is used by SparkFunDMX), prescaler 80 → 1 MHz tick. + // Alarm at 104 ticks → ~9615 Hz ≈ 1200 baud × 8 oversample. + _daliTimer = timerBegin(1, 80, true); + timerAttachInterrupt(_daliTimer, &daliTimerISR, true); + timerAlarmWrite(_daliTimer, 104, true); + timerAlarmEnable(_daliTimer); + + DEBUG_PRINTF("[DALI] Gear usermod initialised (RX=%d TX=%d addr=%d)\n", + _rxPin, _txPin, _daliAddr); + _initDone = true; + } + + + void loop() override { + if (!_enabled || !_initDone || _rxPin < 0) return; + + // Send any pending backward frame once the settling window opens (≥7Te ≈ 2.9ms). + if (_pendingBF && (millis() >= _pendingBFTime)) { + uint8_t bf = _pendingBF; + _pendingBF = 0; + uint8_t result = _dali.tx(&bf, 8); + DEBUG_PRINTF("[DALI] backward frame 0x%02x sent (tx result=%u)\n", bf, result); + } + + uint8_t data[4]; + uint8_t bits = _dali.rx(data); + + if (bits == 0) return; // nothing received + + // A DALI forward frame is exactly 16 bits (2 bytes). + // 1-bit returns are normal bus-idle sampling noise from the library — discard silently. + // Log only genuinely unexpected lengths (partial frames: 3–15 bits). + if (bits != 16) { + if (bits > 2) { + DEBUG_PRINTF("[DALI] partial frame: %u bits (data: 0x%02x 0x%02x 0x%02x 0x%02x)\n", + bits, data[0], data[1], data[2], data[3]); + } + return; + } + + uint8_t addrByte = data[0]; + uint8_t cmdByte = data[1]; + + DEBUG_PRINTF("[DALI] raw frame: addr=0x%02x cmd=0x%02x\n", addrByte, cmdByte); + + // Sniff special broadcast commands that are NOT gear-addressed. + // These must be processed regardless of our _daliAddr setting. + // 0xA3 xx — SET DTR0 (Data Transfer Register 0) = xx + // 0xC3 xx — SET DTR1 (Data Transfer Register 1) = xx + // 0xC1 08 — ENABLE DEVICE TYPE 8 + if (addrByte == 0xA3) { + _dtr0 = cmdByte; + DEBUG_PRINTF("[DALI] SET DTR0 = 0x%02x (%u)\n", cmdByte, cmdByte); + return; + } + if (addrByte == 0xC3) { + _dtr1 = cmdByte; + DEBUG_PRINTF("[DALI] SET DTR1 = 0x%02x (%u)\n", cmdByte, cmdByte); + return; + } + if (addrByte == 0xC1) { + if (cmdByte == 8) { + _dt8Active = true; + DEBUG_PRINTLN(F("[DALI] ENABLE DEVICE TYPE 8")); + } else { + DEBUG_PRINTF("[DALI] ENABLE DEVICE TYPE %u (not handled)\n", cmdByte); + } + return; + } + + if (!daliAddressedToMe(addrByte, _daliAddr)) { + DEBUG_PRINTF("[DALI] frame not for us: addr=0x%02x (our addr=%d) — ignored\n", + addrByte, _daliAddr); + return; + } + + bool isDapc = !(addrByte & 0x01); // S bit = 0 → DAPC + + if (isDapc) { + if (cmdByte == 255) { + DEBUG_PRINTLN(F("[DALI] DAPC 255 (mask) — ignored")); + } else { + DEBUG_PRINTF("[DALI] DAPC level=%u → bri=%u\n", cmdByte, daliLevelToWledBri(cmdByte)); + applyLevel(cmdByte); + // Some masters use a non-standard combined flow: DTR0/DTR1 set the colour + // temperature, ENABLE DEVICE TYPE 8 arms it, and the subsequent DAPC applies + // both brightness and CCT in one go (without 0xE1+0xE2). + if (_dt8Active && (_dtr1 || _dtr0)) { + uint16_t mireds = ((uint16_t)_dtr1 << 8) | _dtr0; + applyCCT(mireds); + } + _dt8Active = false; + } + } else { + DEBUG_PRINTF("[DALI] command 0x%02x (%u)\n", cmdByte, cmdByte); + handleCommand(cmdByte); + } + } + + + void addToJsonInfo(JsonObject& root) override { + if (!_initDone) return; + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray arr = user.createNestedArray(FPSTR(_name)); + if (!_enabled) { + arr.add(F("disabled")); + return; + } + if (_rxPin < 0 || _txPin < 0) { + arr.add(F("pins not configured")); + return; + } + arr.add(_lastDaliLevel); + arr.add(F(" DALI level")); + if (_lastCCTKelvin > 0) { + JsonArray cctArr = user.createNestedArray(F("DALIGear CCT")); + cctArr.add(_lastCCTKelvin); + cctArr.add(F(" K")); + } + } + + + void addToConfig(JsonObject& root) override { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled_key)] = _enabled; + top["pin_rx"] = _rxPin; + top["pin_tx"] = _txPin; + top["daliAddr"] = _daliAddr; + } + + + bool readFromConfig(JsonObject& root) override { + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[FPSTR(_enabled_key)], _enabled, false); + configComplete &= getJsonValue(top["pin_rx"], _rxPin, (int8_t)14); + configComplete &= getJsonValue(top["pin_tx"], _txPin, (int8_t)17); + configComplete &= getJsonValue(top["daliAddr"], _daliAddr, (int8_t)-1); + + return configComplete; + } + + + void appendConfigData() override { + oappend(F("addInfo('")); + oappend(String(FPSTR(_name)).c_str()); + oappend(F(":pin_rx',1,'DALI RX pin');")); + oappend(F("addInfo('")); + oappend(String(FPSTR(_name)).c_str()); + oappend(F(":pin_tx',1,'DALI TX pin');")); + oappend(F("addInfo('")); + oappend(String(FPSTR(_name)).c_str()); + oappend(F(":daliAddr',1,'Short address (0\u201363) or -1 for broadcast only');")); + } + + + uint16_t getId() override { return USERMOD_ID_DALI_GEAR; } +}; + +// Static member definitions +int8_t DaliGearUsermod::_rxPinStatic = -1; +int8_t DaliGearUsermod::_txPinStatic = -1; + +const char DaliGearUsermod::_name[] PROGMEM = "DALIGear"; +const char DaliGearUsermod::_enabled_key[] PROGMEM = "enabled"; + +static DaliGearUsermod dali_gear_usermod; +REGISTER_USERMOD(dali_gear_usermod); diff --git a/wled00/const.h b/wled00/const.h index 70373316fd..97885d26c9 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -230,6 +230,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_DALI_GEAR 59 //Usermod "usermod_dali_gear.cpp" //Wifi encryption type #ifdef WLED_ENABLE_WPA_ENTERPRISE diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 7bdd5cfc20..3eb6ebc0d6 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -76,7 +76,8 @@ enum struct PinOwner : uint8_t { UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins - UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY, // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_DALI_GEAR = USERMOD_ID_DALI_GEAR // 0x3B // Usermod "usermod_dali_gear.cpp" }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); From 0fc601c67921b7de0597e255a8b5b9f2e4696357 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 23 May 2026 16:44:20 +0100 Subject: [PATCH 02/22] esp32s3dev_8MB_opi_dali_gear --- platformio_override.ini | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 platformio_override.ini diff --git a/platformio_override.ini b/platformio_override.ini new file mode 100644 index 0000000000..ac85b5d5a9 --- /dev/null +++ b/platformio_override.ini @@ -0,0 +1,11 @@ +[env:esp32s3dev_8MB_opi_dali_gear] +;; ESP32-S3 dev board (8MB Flash, QSPI PSRAM) with dali_gear usermod +extends = env:esp32s3dev_8MB_qspi +custom_usermods = dali_gear +lib_deps = ${env:esp32s3dev_8MB_qspi.lib_deps} + https://github.com/netmindz/DALI-Lighting-Interface.git#fix/esp32-volatile-cast +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_dali_gear\" + -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=0 ;; use UART0 via built-in JTAG/serial debug unit (303a:1001) + -DBOARD_HAS_PSRAM + -D WLED_DEBUG From 23a93604899fb0612862beb39796e9024ad1cd05 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 23 May 2026 16:46:44 +0100 Subject: [PATCH 03/22] esp32s3dev_8MB_opi_dali_gear --- platformio.ini | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/platformio.ini b/platformio.ini index f08e7c151f..c834381183 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,29 +10,8 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = nodemcuv2 - esp8266_2m - esp01_1m_full - nodemcuv2_160 - esp8266_2m_160 - esp01_1m_full_160 - nodemcuv2_compat - esp8266_2m_compat - esp01_1m_full_compat - esp32dev - esp32dev_debug - esp32_eth - esp32_wrover - lolin_s2_mini - esp32c3dev - esp32c3dev_qio - esp32S3_wroom2 - esp32s3dev_16MB_opi - esp32s3dev_8MB_opi - esp32s3dev_8MB_qspi - esp32s3dev_8MB_none - esp32s3_4M_qspi - usermods +default_envs = + esp32s3dev_8MB_opi_dali_gear src_dir = ./wled00 data_dir = ./wled00/data From 8394038b1500a74b8baa6cf0e1e97abace785b98 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 11:20:38 +0100 Subject: [PATCH 04/22] fix(dali_gear): address code review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - library.json: pin qqqDALI dependency to immutable commit SHA (e39a7da0) instead of branch ref for reproducible builds - readme.md: add 'text' language tag to ASCII circuit diagram fenced code block (fixes markdownlint MD040) - setup(): fix pin leak — deallocate RX pin if TX allocation fails --- usermods/dali_gear/library.json | 2 +- usermods/dali_gear/readme.md | 2 +- usermods/dali_gear/usermod_dali_gear.cpp | 12 +++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/usermods/dali_gear/library.json b/usermods/dali_gear/library.json index bfe6562fb6..865d94045d 100644 --- a/usermods/dali_gear/library.json +++ b/usermods/dali_gear/library.json @@ -2,6 +2,6 @@ "name": "dali_gear", "build": { "libArchive": false }, "dependencies": { - "qqqDALI": "https://github.com/netmindz/DALI-Lighting-Interface.git#fix/esp32-volatile-cast" + "qqqDALI": "https://github.com/netmindz/DALI-Lighting-Interface.git#e39a7da06242010bbb6771532c4ac17b3ec73834" } } diff --git a/usermods/dali_gear/readme.md b/usermods/dali_gear/readme.md index d300d8f49a..ad1e3e8bf0 100644 --- a/usermods/dali_gear/readme.md +++ b/usermods/dali_gear/readme.md @@ -12,7 +12,7 @@ You need a DALI bus interface circuit to convert between the DALI bus voltage (9 ### Minimal DIY circuit (from [qqqlab/DALI-Lighting-Interface](https://github.com/qqqlab/DALI-Lighting-Interface)) -``` +```text 3.3V ESP32 5.6V ___ Zener +----|___|---- 12V Power Supply ___ Diode | 220 Ω diff --git a/usermods/dali_gear/usermod_dali_gear.cpp b/usermods/dali_gear/usermod_dali_gear.cpp index f734395409..631b87f2eb 100644 --- a/usermods/dali_gear/usermod_dali_gear.cpp +++ b/usermods/dali_gear/usermod_dali_gear.cpp @@ -244,9 +244,15 @@ class DaliGearUsermod : public Usermod { } // Claim pins via WLED pin manager - if (!PinManager::allocatePin(_rxPin, false, PinOwner::UM_DALI_GEAR) || - !PinManager::allocatePin(_txPin, true, PinOwner::UM_DALI_GEAR)) { - DEBUG_PRINTLN(F("[DALI] Pin allocation failed")); + if (!PinManager::allocatePin(_rxPin, false, PinOwner::UM_DALI_GEAR)) { + DEBUG_PRINTLN(F("[DALI] RX pin allocation failed")); + _enabled = false; + _initDone = true; + return; + } + if (!PinManager::allocatePin(_txPin, true, PinOwner::UM_DALI_GEAR)) { + DEBUG_PRINTLN(F("[DALI] TX pin allocation failed")); + PinManager::deallocatePin(_rxPin, PinOwner::UM_DALI_GEAR); _enabled = false; _initDone = true; return; From 6bc19e94fbcc350a3cbcec4ca2cf2803eec05e50 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 11:29:28 +0100 Subject: [PATCH 05/22] =?UTF-8?q?feat(dali=5Fgear):=20address=20PR=20revie?= =?UTF-8?q?w=20=E2=80=94=20TX=20invert,=20query=20responses,=20QUERY=20COL?= =?UTF-8?q?OUR=20TYPE=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tx_inverted config option: corrects TX polarity for single-stage inverting circuits (e.g. qqqDALI DIY PNP). Default false is correct for the Waveshare Pico-DALI2 (NPN + opto = double inversion). - Add QUERY STATUS (0x90), QUERY CONTROL GEAR PRESENT (0x91) and QUERY ACTUAL LEVEL (0xA0) backward frame responses, so masters that probe for gear presence before sending DT8 commands will find us. - Fix QUERY COLOUR TYPE command byte: spec (IEC 62386-209) uses 0xF7; handle both 0xF7 and 0xE7 (non-standard masters) via case fallthrough. - readme: warn that qqqDALI DIY PNP circuit is conceptual and not isolated; document tx_inverted setting and new query responses. --- usermods/dali_gear/readme.md | 19 +++- usermods/dali_gear/usermod_dali_gear.cpp | 116 +++++++++++++++++------ 2 files changed, 104 insertions(+), 31 deletions(-) diff --git a/usermods/dali_gear/readme.md b/usermods/dali_gear/readme.md index ad1e3e8bf0..4baaa3f5f0 100644 --- a/usermods/dali_gear/readme.md +++ b/usermods/dali_gear/readme.md @@ -26,9 +26,9 @@ RX ---+-----|___|---|>|----------+------------- DALI+ GND --+---------------------------+------------- DALI- ``` -> **Note:** For this circuit the TX polarity must be inverted. The transistor pulls the bus low when the GPIO is HIGH. Configure your TX pin accordingly and invert the output in hardware or adjust the HAL callbacks in the source. +> **⚠️ Warning:** This is a conceptual schematic for experimentation — it is **not isolated** and exposes the ESP32 GPIO to DALI bus voltages through a resistor divider only. Do not use it in a production installation. The PNP transistor produces a **single inversion**: GPIO HIGH drives the bus LOW (asserted). Enable **TX Inverted** in the usermod settings when using this circuit. -Commercial DALI interface modules (e.g. Waveshare Pico-DALI2, Mikroe DALI Click) are a simpler alternative. +Commercial DALI interface modules with proper isolation (e.g. Waveshare Pico-DALI2, Mikroe DALI Click) are strongly recommended for any real installation. ### Pin assignment @@ -48,6 +48,7 @@ Configure both pins in the WLED usermod settings page. | Enabled | false | Enable/disable the usermod | | pin_rx | 14 | GPIO for DALI bus RX | | pin_tx | 17 | GPIO for DALI bus TX | +| tx_inverted | false | Invert TX polarity. Enable for single-stage inverting circuits (e.g. DIY PNP). Leave off for Waveshare Pico-DALI2 and other NPN+opto-isolated boards. | | daliAddr | -1 | Short address (0–63) to respond to, or -1 to respond to broadcast only | ## DALI commands handled @@ -77,6 +78,16 @@ When the master sends a DAPC frame, the arc level (0–254) is mapped linearly t | ON AND STEP UP | 8 | Turn on if off, then increase by 10 | | GO TO LAST ACTIVE LEVEL | 10 | Restore last brightness before turn-off | +### Query commands (backward frame responses) + +These allow a DALI master to detect gear presence and read basic status. Responses are sent as DALI backward frames 4 ms after the query, within the IEC 62386-102 required window of 7Te–22Te (≈2.9–9.2 ms). + +| Command | Byte | Response | +|---|---|---| +| QUERY STATUS | 0x90 | Status byte: bit 2 = lamp on, bit 6 = no short address | +| QUERY CONTROL GEAR PRESENT | 0x91 | `0xFF` (Yes, I am here) | +| QUERY ACTUAL LEVEL | 0xA0 | Current arc level (0–254) derived from WLED brightness | + ### DT8 colour temperature (IEC 62386-209) Colour temperature commands from a DALI master are mapped to WLED's CCT value via `strip.setCCT()`. The mired value is converted to Kelvin (`K = 1,000,000 / mireds`). WLED's accepted range is 1900–10091 K; values outside this range are clamped. @@ -95,9 +106,9 @@ Two CCT application flows are supported: Some DALI masters skip the `0xE1` + `0xE2` sequence and instead apply the colour temperature implicitly alongside the subsequent DAPC command. The usermod detects this: if DTR0/DTR1 are set and DT8 is active when a DAPC frame arrives, the CCT is applied at the same time as the brightness change. -**QUERY COLOUR TYPE (0xE7):** +**QUERY COLOUR TYPE (0xF7 / 0xE7):** -Some masters query the gear's colour capabilities before sending CCT commands. The usermod responds with a backward frame value of `0x02` (bit 1 = Tc colour temperature supported), sent 4 ms after the query frame to meet the DALI spec requirement (7Te–22Te ≈ 2.9–9.2 ms settling window). +Some masters query the gear's colour capabilities before sending CCT commands. Per IEC 62386-209 §11.3.4.2, this command is `0xF7`. Some non-standard masters send `0xE7` instead; both are handled. The usermod responds with `0x02` (bit 1 = Tc colour temperature supported), sent 4 ms after the query frame to meet the DALI spec requirement (7Te–22Te ≈ 2.9–9.2 ms settling window). `SET DTR0`, `SET DTR1`, and `ENABLE DEVICE TYPE 8` are sniffed as broadcast-level frames regardless of the configured `daliAddr`. diff --git a/usermods/dali_gear/usermod_dali_gear.cpp b/usermods/dali_gear/usermod_dali_gear.cpp index 631b87f2eb..d51de2b106 100644 --- a/usermods/dali_gear/usermod_dali_gear.cpp +++ b/usermods/dali_gear/usermod_dali_gear.cpp @@ -9,7 +9,8 @@ // Phase 2: DT8 colour temperature (IEC 62386-209). Handles SET DTR0/DTR1 // special commands, ENABLE DEVICE TYPE 8, SET TEMPORARY COLOUR TEMPERATURE, // ACTIVATE to map DALI Tc (mireds) → WLED CCT (Kelvin), and backward frame -// response to QUERY COLOUR TYPE (0xe7) to declare Tc support to the master. +// responses to QUERY STATUS, QUERY CONTROL GEAR PRESENT, QUERY ACTUAL LEVEL, +// and QUERY COLOUR TYPE (0xF7 per spec; also 0xE7 for non-standard masters). // // Hardware: requires a DALI bus interface circuit (see readme.md). // ESP32 only — uses hardware timer API not available on ESP8266. @@ -36,7 +37,7 @@ static bool daliAddressedToMe(uint8_t addrByte, int8_t daliAddr) { uint8_t frameAddr = (addrByte >> 1) & 0x3F; return frameAddr == (uint8_t)daliAddr; } - // Group address: 100A AAA x — not handled in phase 1 + // Group address: 100A AAA x — not handled return false; } @@ -50,6 +51,12 @@ static uint8_t daliLevelToWledBri(uint8_t level) { return (uint8_t)(((uint16_t)level * 255u + 127u) / 254u); } +// Map WLED bri (1–255) back to a DALI arc level (1–254), for QUERY ACTUAL LEVEL. +static uint8_t wledBriToDaliLevel(uint8_t b) { + if (b == 0) return 0; + return (uint8_t)(((uint16_t)b * 254u + 127u) / 255u); +} + // --------------------------------------------------------------------------- // ISR and timer — file-scope so the ISR can reach the Dali instance // --------------------------------------------------------------------------- @@ -71,6 +78,10 @@ class DaliGearUsermod : public Usermod { bool _initDone = false; int8_t _rxPin = 14; // default: Waveshare Pico-DALI2 RX int8_t _txPin = 17; // default: Waveshare Pico-DALI2 TX + bool _txInverted = false; // true for circuits with a single-stage inverting TX driver + // (e.g. qqqDALI DIY PNP circuit). + // false (default) for the Waveshare Pico-DALI2 and other + // boards with double-inversion (NPN + opto-isolator). int8_t _daliAddr = -1; // -1 = respond to broadcast only uint8_t _lastDaliLevel = 0; // last DALI arc level received (for info panel) @@ -90,21 +101,29 @@ class DaliGearUsermod : public Usermod { static const char _enabled_key[]; // --------------------------------------------------------------------------- - // Bus HAL callbacks (static so they can be passed as function pointers) + // Bus HAL callbacks (static so they can be passed as function pointers). + // TX polarity depends on interface hardware: + // _txInverted = false (default): GPIO HIGH = bus idle, GPIO LOW = assert bus. + // Used by Waveshare Pico-DALI2 (NPN + opto-isolator = double inversion). + // _txInverted = true: GPIO LOW = bus idle, GPIO HIGH = assert bus. + // Used by the qqqDALI DIY PNP circuit (single inversion via PNP transistor). // --------------------------------------------------------------------------- static uint8_t busIsHigh() { return digitalRead(_rxPinStatic); } static void busSetLow() { - digitalWrite(_txPinStatic, LOW); + // "set bus low" = assert the DALI bus + digitalWrite(_txPinStatic, _txInvertedStatic ? HIGH : LOW); } static void busSetHigh() { - digitalWrite(_txPinStatic, HIGH); + // "set bus high" = release the DALI bus (idle) + digitalWrite(_txPinStatic, _txInvertedStatic ? LOW : HIGH); } - // Static copies of pins needed by the HAL callbacks + // Static copies of pins/config needed by the HAL callbacks static int8_t _rxPinStatic; static int8_t _txPinStatic; + static bool _txInvertedStatic; // --------------------------------------------------------------------------- // Schedule a DALI backward frame to be sent after the mandatory settling time. @@ -195,14 +214,48 @@ class DaliGearUsermod : public Usermod { stateUpdated(CALL_MODE_DIRECT_CHANGE); break; + // IEC 62386-102 §11.2 query commands — backward frame responses. + // These allow a DALI master to detect gear presence and read basic status + // before sending DT8 or other application commands. + + case 0x90: { + // QUERY STATUS — respond with status byte. + // Bit 2 = lamp arc power on (1 if bri > 0). + // Bit 6 = missing short address (1 if no address configured). + // All other status/fault bits = 0 (no failures to report). + uint8_t status = ((bri > 0) ? 0x04u : 0x00u) + | ((_daliAddr < 0) ? 0x40u : 0x00u); + DEBUG_PRINTF("[DALI] QUERY STATUS → 0x%02x\n", status); + scheduleBF(status); + break; + } + + case 0x91: + // QUERY CONTROL GEAR PRESENT — respond 0xFF ("Yes"). + // Many masters send this first to detect whether any gear is on the bus; + // silence here causes the master to skip all subsequent commands. + DEBUG_PRINTLN(F("[DALI] QUERY CONTROL GEAR PRESENT → 0xFF")); + scheduleBF(0xFF); + break; + + case 0xA0: + // QUERY ACTUAL LEVEL — respond with current arc level (0–254). + // Derived from the current WLED brightness so it stays accurate even if + // bri was changed via the WLED UI rather than a DALI command. + DEBUG_PRINTF("[DALI] QUERY ACTUAL LEVEL → %u\n", wledBriToDaliLevel(bri)); + scheduleBF(wledBriToDaliLevel(bri)); + break; + // DT8 (IEC 62386-209) application extended commands. // These are only valid when preceded by ENABLE DEVICE TYPE 8 (addr=0xC1, cmd=8). // 0xE1 = SET TEMPORARY COLOUR TEMPERATURE — loads DTR0+DTR1 into temp register. // 0xE2 = ACTIVATE — applies the temporary colour temperature. - // 0xE7 = QUERY COLOUR TYPE — master asks which DT8 colour modes are supported. - // Response bitmask (IEC 62386-209 §11.3.4.2): - // bit 0 = XY colour, bit 1 = Tc colour temperature, bit 2 = Primary N, - // bit 3 = RGBWAF. We support Tc only → respond 0x02. + // 0xF7 = QUERY COLOUR TYPE (IEC 62386-209 §11.3.4.2) — master asks which DT8 + // colour modes are supported. Response bitmask: + // bit 0 = XY colour, bit 1 = Tc colour temperature, + // bit 2 = Primary N, bit 3 = RGBWAF. We support Tc only → 0x02. + // Note: some non-standard masters send this as 0xE7 instead. Both are + // handled here to maximise interoperability. case 0xE1: if (_dt8Active) { _tempCCT = ((uint16_t)_dtr1 << 8) | _dtr0; @@ -222,9 +275,10 @@ class DaliGearUsermod : public Usermod { _dt8Active = false; break; - case 0xE7: - // QUERY COLOUR TYPE — respond regardless of _dt8Active state - // (master needs to know our capabilities before enabling DT8) + case 0xE7: // non-standard masters send QUERY COLOUR TYPE here (spec says 0xF7) + case 0xF7: // QUERY COLOUR TYPE — IEC 62386-209 §11.3.4.2 + // Respond regardless of _dt8Active state; master needs to know our + // capabilities before it will send ENABLE DEVICE TYPE 8. DEBUG_PRINTLN(F("[DALI] QUERY COLOUR TYPE → scheduling backward frame 0x02 (Tc supported)")); scheduleBF(0x02); break; @@ -261,11 +315,13 @@ class DaliGearUsermod : public Usermod { // Configure GPIO pinMode(_rxPin, INPUT); pinMode(_txPin, OUTPUT); - digitalWrite(_txPin, HIGH); // idle bus state (not asserting bus) + // Idle state: bus not asserted. Polarity depends on interface circuit. + digitalWrite(_txPin, _txInverted ? LOW : HIGH); // Store static copies for HAL callbacks - _rxPinStatic = _rxPin; - _txPinStatic = _txPin; + _rxPinStatic = _rxPin; + _txPinStatic = _txPin; + _txInvertedStatic = _txInverted; _dali.begin(busIsHigh, busSetLow, busSetHigh); @@ -277,8 +333,8 @@ class DaliGearUsermod : public Usermod { timerAlarmWrite(_daliTimer, 104, true); timerAlarmEnable(_daliTimer); - DEBUG_PRINTF("[DALI] Gear usermod initialised (RX=%d TX=%d addr=%d)\n", - _rxPin, _txPin, _daliAddr); + DEBUG_PRINTF("[DALI] Gear usermod initialised (RX=%d TX=%d txInv=%d addr=%d)\n", + _rxPin, _txPin, (int)_txInverted, _daliAddr); _initDone = true; } @@ -397,9 +453,10 @@ class DaliGearUsermod : public Usermod { void addToConfig(JsonObject& root) override { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled_key)] = _enabled; - top["pin_rx"] = _rxPin; - top["pin_tx"] = _txPin; - top["daliAddr"] = _daliAddr; + top["pin_rx"] = _rxPin; + top["pin_tx"] = _txPin; + top["tx_inverted"] = _txInverted; + top["daliAddr"] = _daliAddr; } @@ -407,10 +464,11 @@ class DaliGearUsermod : public Usermod { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); - configComplete &= getJsonValue(top[FPSTR(_enabled_key)], _enabled, false); - configComplete &= getJsonValue(top["pin_rx"], _rxPin, (int8_t)14); - configComplete &= getJsonValue(top["pin_tx"], _txPin, (int8_t)17); - configComplete &= getJsonValue(top["daliAddr"], _daliAddr, (int8_t)-1); + configComplete &= getJsonValue(top[FPSTR(_enabled_key)], _enabled, false); + configComplete &= getJsonValue(top["pin_rx"], _rxPin, (int8_t)14); + configComplete &= getJsonValue(top["pin_tx"], _txPin, (int8_t)17); + configComplete &= getJsonValue(top["tx_inverted"], _txInverted, false); + configComplete &= getJsonValue(top["daliAddr"], _daliAddr, (int8_t)-1); return configComplete; } @@ -425,6 +483,9 @@ class DaliGearUsermod : public Usermod { oappend(F(":pin_tx',1,'DALI TX pin');")); oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); + oappend(F(":tx_inverted',1,'Invert TX — enable for single-stage inverting circuits (e.g. DIY PNP). Leave off for Waveshare Pico-DALI2 and NPN+opto boards.');")); + oappend(F("addInfo('")); + oappend(String(FPSTR(_name)).c_str()); oappend(F(":daliAddr',1,'Short address (0\u201363) or -1 for broadcast only');")); } @@ -433,8 +494,9 @@ class DaliGearUsermod : public Usermod { }; // Static member definitions -int8_t DaliGearUsermod::_rxPinStatic = -1; -int8_t DaliGearUsermod::_txPinStatic = -1; +int8_t DaliGearUsermod::_rxPinStatic = -1; +int8_t DaliGearUsermod::_txPinStatic = -1; +bool DaliGearUsermod::_txInvertedStatic = false; const char DaliGearUsermod::_name[] PROGMEM = "DALIGear"; const char DaliGearUsermod::_enabled_key[] PROGMEM = "enabled"; From 0d77a272a191c1f2e7da6af906a32791e384a780 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 11:33:23 +0100 Subject: [PATCH 06/22] =?UTF-8?q?fix(dali=5Fgear):=20do=20not=20respond=20?= =?UTF-8?q?to=200xE7=20=E2=80=94=20QUERY=20COLOUR=20TYPE=20is=200xF7=20onl?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per IEC 62386-209, QUERY COLOUR TYPE is command 0xF7. Command 0xE7 is not a query and must not produce a backward frame response. The previous 0xE7 fallthrough was added for a non-standard master but violates the spec. Now that 0x90/0x91/0xA0 presence queries are handled, a compliant master will discover the gear via those and correctly use 0xF7. --- usermods/dali_gear/readme.md | 4 ++-- usermods/dali_gear/usermod_dali_gear.cpp | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/usermods/dali_gear/readme.md b/usermods/dali_gear/readme.md index 4baaa3f5f0..75cbc8087c 100644 --- a/usermods/dali_gear/readme.md +++ b/usermods/dali_gear/readme.md @@ -106,9 +106,9 @@ Two CCT application flows are supported: Some DALI masters skip the `0xE1` + `0xE2` sequence and instead apply the colour temperature implicitly alongside the subsequent DAPC command. The usermod detects this: if DTR0/DTR1 are set and DT8 is active when a DAPC frame arrives, the CCT is applied at the same time as the brightness change. -**QUERY COLOUR TYPE (0xF7 / 0xE7):** +**QUERY COLOUR TYPE (0xF7):** -Some masters query the gear's colour capabilities before sending CCT commands. Per IEC 62386-209 §11.3.4.2, this command is `0xF7`. Some non-standard masters send `0xE7` instead; both are handled. The usermod responds with `0x02` (bit 1 = Tc colour temperature supported), sent 4 ms after the query frame to meet the DALI spec requirement (7Te–22Te ≈ 2.9–9.2 ms settling window). +Some masters query the gear's colour capabilities before sending CCT commands. Per IEC 62386-209 §11.3.4.2, this command is `0xF7`. The usermod responds with `0x02` (bit 1 = Tc colour temperature supported), sent 4 ms after the query frame to meet the DALI spec requirement (7Te–22Te ≈ 2.9–9.2 ms settling window). Command `0xE7` does not generate a backward frame response (it is not a query command per the spec). `SET DTR0`, `SET DTR1`, and `ENABLE DEVICE TYPE 8` are sniffed as broadcast-level frames regardless of the configured `daliAddr`. diff --git a/usermods/dali_gear/usermod_dali_gear.cpp b/usermods/dali_gear/usermod_dali_gear.cpp index d51de2b106..b4282489a5 100644 --- a/usermods/dali_gear/usermod_dali_gear.cpp +++ b/usermods/dali_gear/usermod_dali_gear.cpp @@ -10,7 +10,7 @@ // special commands, ENABLE DEVICE TYPE 8, SET TEMPORARY COLOUR TEMPERATURE, // ACTIVATE to map DALI Tc (mireds) → WLED CCT (Kelvin), and backward frame // responses to QUERY STATUS, QUERY CONTROL GEAR PRESENT, QUERY ACTUAL LEVEL, -// and QUERY COLOUR TYPE (0xF7 per spec; also 0xE7 for non-standard masters). +// and QUERY COLOUR TYPE (0xF7 per IEC 62386-209). // // Hardware: requires a DALI bus interface circuit (see readme.md). // ESP32 only — uses hardware timer API not available on ESP8266. @@ -275,11 +275,18 @@ class DaliGearUsermod : public Usermod { _dt8Active = false; break; - case 0xE7: // non-standard masters send QUERY COLOUR TYPE here (spec says 0xF7) + case 0xE7: + // 0xE7 is not QUERY COLOUR TYPE per IEC 62386-209 — do not respond. + // (QUERY COLOUR TYPE is 0xF7; some non-standard masters mistakenly use + // 0xE7, but sending a backward frame here would violate the spec.) + DEBUG_PRINTLN(F("[DALI] cmd 0xE7 (not a query — no response)")); + break; + case 0xF7: // QUERY COLOUR TYPE — IEC 62386-209 §11.3.4.2 // Respond regardless of _dt8Active state; master needs to know our // capabilities before it will send ENABLE DEVICE TYPE 8. - DEBUG_PRINTLN(F("[DALI] QUERY COLOUR TYPE → scheduling backward frame 0x02 (Tc supported)")); + // Response bitmask: bit 1 = Tc colour temperature supported → 0x02. + DEBUG_PRINTLN(F("[DALI] QUERY COLOUR TYPE (0xF7) → scheduling backward frame 0x02 (Tc supported)")); scheduleBF(0x02); break; From 670a6fd3c54ef14d92f33e16f03d7493f473c4b3 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 11:35:22 +0100 Subject: [PATCH 07/22] feat(dali_gear): respond to QUERY DEVICE TYPE (0x18) with 0x08 Conformant DALI-2 masters send QUERY DEVICE TYPE before issuing ENABLE DEVICE TYPE 8 or any DT8 application extended commands. Without this response, such masters skip CCT control entirely. Update readme query-responses table and top-of-file comment. --- usermods/dali_gear/readme.md | 1 + usermods/dali_gear/usermod_dali_gear.cpp | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/usermods/dali_gear/readme.md b/usermods/dali_gear/readme.md index 75cbc8087c..8daa8ff68b 100644 --- a/usermods/dali_gear/readme.md +++ b/usermods/dali_gear/readme.md @@ -86,6 +86,7 @@ These allow a DALI master to detect gear presence and read basic status. Respons |---|---|---| | QUERY STATUS | 0x90 | Status byte: bit 2 = lamp on, bit 6 = no short address | | QUERY CONTROL GEAR PRESENT | 0x91 | `0xFF` (Yes, I am here) | +| QUERY DEVICE TYPE | 0x18 | `0x08` (device type 8 = colour control) | | QUERY ACTUAL LEVEL | 0xA0 | Current arc level (0–254) derived from WLED brightness | ### DT8 colour temperature (IEC 62386-209) diff --git a/usermods/dali_gear/usermod_dali_gear.cpp b/usermods/dali_gear/usermod_dali_gear.cpp index b4282489a5..81e04d54b1 100644 --- a/usermods/dali_gear/usermod_dali_gear.cpp +++ b/usermods/dali_gear/usermod_dali_gear.cpp @@ -9,7 +9,8 @@ // Phase 2: DT8 colour temperature (IEC 62386-209). Handles SET DTR0/DTR1 // special commands, ENABLE DEVICE TYPE 8, SET TEMPORARY COLOUR TEMPERATURE, // ACTIVATE to map DALI Tc (mireds) → WLED CCT (Kelvin), and backward frame -// responses to QUERY STATUS, QUERY CONTROL GEAR PRESENT, QUERY ACTUAL LEVEL, +// responses to QUERY STATUS (0x90), QUERY CONTROL GEAR PRESENT (0x91), +// QUERY DEVICE TYPE (0x18), QUERY ACTUAL LEVEL (0xA0), // and QUERY COLOUR TYPE (0xF7 per IEC 62386-209). // // Hardware: requires a DALI bus interface circuit (see readme.md). @@ -238,6 +239,14 @@ class DaliGearUsermod : public Usermod { scheduleBF(0xFF); break; + case 0x18: + // QUERY DEVICE TYPE — respond 0x08 (device type 8 = colour control, IEC 62386-209). + // Conformant DALI-2 masters send this before issuing ENABLE DEVICE TYPE 8 or any + // DT8 application extended commands. Silence causes such masters to skip CCT control. + DEBUG_PRINTLN(F("[DALI] QUERY DEVICE TYPE → 0x08")); + scheduleBF(0x08); + break; + case 0xA0: // QUERY ACTUAL LEVEL — respond with current arc level (0–254). // Derived from the current WLED brightness so it stays accurate even if From 71767b785254276672b257db950c039afdb981fb Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 11:43:35 +0100 Subject: [PATCH 08/22] ci: discover and build custom PlatformIO envs from usermods platformio_override.ini.sample files Adds two new jobs to the Usermod CI workflow to address issue #5648: - get_custom_build_envs: scans all usermods/*/platformio_override.ini.sample files and emits a matrix of {usermod, env} pairs by extracting [env:*] section names - build_custom: builds each discovered environment by copying the .ini.sample as platformio_override.ini and running pio run -e This allows PRs introducing usermods with custom build environments (such as pixels_dice_tray) to have those environments validated in CI without committing platformio_override.ini to the repository. --- .github/workflows/usermods.yml | 62 +++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 24eda32ece..63f0100c5e 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -74,4 +74,64 @@ jobs: cat platformio_override.ini - name: Build firmware - run: pio run -e ${{ matrix.environment }} + run: pio run -e ${{ matrix.environment }} + + + get_custom_build_envs: + name: Gather Custom Build Environments + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Find usermods with custom build environments + id: custom_envs + run: | + result='[]' + for sample in $(find usermods/ -name "platformio_override.ini.sample" | sort); do + usermod=$(dirname "$sample" | xargs basename) + envs=$(grep -E '^\[env:[^]]+\]' "$sample" | sed 's/^\[env:\(.*\)\]$/\1/') + for env in $envs; do + result=$(echo "$result" | jq --arg u "$usermod" --arg e "$env" '. + [{usermod: $u, env: $e}]') + done + done + echo "matrix=$(echo "$result" | jq -c '.')" >> $GITHUB_OUTPUT + outputs: + matrix: ${{ steps.custom_envs.outputs.matrix }} + + + build_custom: + name: Build Custom Env (${{ matrix.usermod }} / ${{ matrix.env }}) + runs-on: ubuntu-latest + needs: get_custom_build_envs + if: needs.get_custom_build_envs.outputs.matrix != '[]' + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.get_custom_build_envs.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + - run: npm ci + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: | + ~/.platformio/.cache + ~/.buildcache + build_output + key: pio-${{ runner.os }}-${{ matrix.env }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} + restore-keys: pio-${{ runner.os }}-${{ matrix.env }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}- + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + - name: Install PlatformIO + run: pip install -r requirements.txt + - name: Apply custom build environment + run: cp -v "usermods/${{ matrix.usermod }}/platformio_override.ini.sample" platformio_override.ini + - name: Build firmware + run: pio run -e ${{ matrix.env }} From badfcfc8e7644f39ea1bca34e481d94300b471a5 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 11:56:41 +0100 Subject: [PATCH 09/22] ci: consolidate usermod build envs into per-usermod platformio_override.ini.sample files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move usermod-specific PlatformIO environments out of the root platformio_override.sample.ini and into dedicated files within each usermod's own directory, making them discoverable by CI: - Moved elekstube_ips env → usermods/EleksTube_IPS/ - Moved esp32dev_usermod_RF433 env → usermods/usermod_v2_RF433/ Extracted build environments from README docs into new sample files for: AHT10_v2, BME280_v2, INA226_v2, LD2410_v2 (fix LD2140_v2 typo), LDR_Dusk_Dawn_v2 (fix invalid inline comment), MAX17048_v2, Temperature, mpu6050_imu, sht (esp32 + d1_mini envs), usermod_v2_klipper_percentage (convert -D flag to custom_usermods) These files are now picked up by the get_custom_build_envs / build_custom CI jobs added in the previous commit. --- platformio_override.sample.ini | 31 ++---------------- .../AHT10_v2/platformio_override.ini.sample | 9 ++++++ .../BME280_v2/platformio_override.ini.sample | 9 ++++++ .../platformio_override.ini.sample | 32 +++++++++++++++++++ .../INA226_v2/platformio_override.ini.sample | 23 +++++++++++++ .../LD2410_v2/platformio_override.ini.sample | 9 ++++++ .../platformio_override.ini.sample | 10 ++++++ .../platformio_override.ini.sample | 9 ++++++ .../platformio_override.ini.sample | 10 ++++++ .../platformio_override.ini.sample | 10 ++++++ usermods/sht/platformio_override.ini.sample | 13 ++++++++ .../platformio_override.ini.sample | 11 +++++++ .../platformio_override.ini.sample | 10 ++++++ 13 files changed, 157 insertions(+), 29 deletions(-) create mode 100644 usermods/AHT10_v2/platformio_override.ini.sample create mode 100644 usermods/BME280_v2/platformio_override.ini.sample create mode 100644 usermods/EleksTube_IPS/platformio_override.ini.sample create mode 100644 usermods/INA226_v2/platformio_override.ini.sample create mode 100644 usermods/LD2410_v2/platformio_override.ini.sample create mode 100644 usermods/LDR_Dusk_Dawn_v2/platformio_override.ini.sample create mode 100644 usermods/MAX17048_v2/platformio_override.ini.sample create mode 100644 usermods/Temperature/platformio_override.ini.sample create mode 100644 usermods/mpu6050_imu/platformio_override.ini.sample create mode 100644 usermods/sht/platformio_override.ini.sample create mode 100644 usermods/usermod_v2_RF433/platformio_override.ini.sample create mode 100644 usermods/usermod_v2_klipper_percentage/platformio_override.ini.sample diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index 16e73be007..e8787e613f 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -506,42 +506,15 @@ lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ # EleksTube-IPS +# See usermods/EleksTube_IPS/platformio_override.ini.sample # ------------------------------------------------------------------------------ -[env:elekstube_ips] -extends = esp32 ;; use default esp32 platform -board = esp32dev -upload_speed = 921600 -custom_usermods = ${env:esp32dev.custom_usermods} RTC EleksTube_IPS -build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED - -D DATA_PINS=12 - -D RLYPIN=27 - -D BTNPIN=34 - -D PIXEL_COUNTS=6 - # Display config - -D ST7789_DRIVER - -D TFT_WIDTH=135 - -D TFT_HEIGHT=240 - -D CGRAM_OFFSET - -D TFT_SDA_READ - -D TFT_MOSI=23 - -D TFT_SCLK=18 - -D TFT_DC=25 - -D TFT_RST=26 - -D SPI_FREQUENCY=40000000 - -D USER_SETUP_LOADED -monitor_filters = esp32_exception_decoder # ------------------------------------------------------------------------------ # Usermod examples # ------------------------------------------------------------------------------ -# 433MHz RF remote example for esp32dev -[env:esp32dev_usermod_RF433] -extends = env:esp32dev -custom_usermods = - ${env:esp32dev.custom_usermods} - RF433 +# 433MHz RF remote example: see usermods/usermod_v2_RF433/platformio_override.ini.sample # External usermod from a git repository. # The library's `library.json` must include `"build": {"libArchive": false}`. diff --git a/usermods/AHT10_v2/platformio_override.ini.sample b/usermods/AHT10_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..56bba5aa2e --- /dev/null +++ b/usermods/AHT10_v2/platformio_override.ini.sample @@ -0,0 +1,9 @@ +# AHT10/AHT15/AHT20 temperature/humidity usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = aht10_example + +[env:aht10_example] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} AHT10 diff --git a/usermods/BME280_v2/platformio_override.ini.sample b/usermods/BME280_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..39f2f808e7 --- /dev/null +++ b/usermods/BME280_v2/platformio_override.ini.sample @@ -0,0 +1,9 @@ +# BME280_v2 usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_bme280_d1_mini + +[env:usermod_bme280_d1_mini] +extends = env:d1_mini +custom_usermods = ${env:d1_mini.custom_usermods} BME280_v2 diff --git a/usermods/EleksTube_IPS/platformio_override.ini.sample b/usermods/EleksTube_IPS/platformio_override.ini.sample new file mode 100644 index 0000000000..5e761ce5f8 --- /dev/null +++ b/usermods/EleksTube_IPS/platformio_override.ini.sample @@ -0,0 +1,32 @@ +# EleksTube IPS clock build environment +# Copy to platformio_override.ini in the WLED root to use. +# +# Note: usermods/EleksTube_IPS/library.json is currently disabled. +# To enable custom_usermods support, rename library.json.disabled to library.json. + +[platformio] +default_envs = elekstube_ips + +[env:elekstube_ips] +extends = esp32 ;; use default esp32 platform +board = esp32dev +upload_speed = 921600 +custom_usermods = ${env:esp32dev.custom_usermods} RTC EleksTube_IPS +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED + -D DATA_PINS=12 + -D RLYPIN=27 + -D BTNPIN=34 + -D PIXEL_COUNTS=6 + # Display config + -D ST7789_DRIVER + -D TFT_WIDTH=135 + -D TFT_HEIGHT=240 + -D CGRAM_OFFSET + -D TFT_SDA_READ + -D TFT_MOSI=23 + -D TFT_SCLK=18 + -D TFT_DC=25 + -D TFT_RST=26 + -D SPI_FREQUENCY=40000000 + -D USER_SETUP_LOADED +monitor_filters = esp32_exception_decoder diff --git a/usermods/INA226_v2/platformio_override.ini.sample b/usermods/INA226_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..c38f572f47 --- /dev/null +++ b/usermods/INA226_v2/platformio_override.ini.sample @@ -0,0 +1,23 @@ +# INA226 power monitor usermod build environments +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = ina226_example + +# Minimal example — enable the usermod with default settings +[env:ina226_example] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} INA226 +build_flags = ${env:esp32dev.build_flags} + ; -D USERMOD_INA226_DEBUG ; uncomment to add debug status to the info modal + +# Custom calibration example — adjust constants to match your hardware +[env:ina226_custom] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} INA226 +build_flags = ${env:esp32dev.build_flags} + -D INA226_ENABLED_DEFAULT=true + -D INA226_SHUNT_MICRO_OHMS=2888 + -D INA226_DEFAULT_CURRENT_RANGE=10000 + -D INA226_CURRENT_OFFSET_MA=-118 + -D INA226_CHECK_INTERVAL_MS=1000 diff --git a/usermods/LD2410_v2/platformio_override.ini.sample b/usermods/LD2410_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..407b14dea2 --- /dev/null +++ b/usermods/LD2410_v2/platformio_override.ini.sample @@ -0,0 +1,9 @@ +# LD2410 presence sensor usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_LD2410_v2_esp32dev + +[env:usermod_LD2410_v2_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} LD2410_v2 diff --git a/usermods/LDR_Dusk_Dawn_v2/platformio_override.ini.sample b/usermods/LDR_Dusk_Dawn_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..d12d73d8dc --- /dev/null +++ b/usermods/LDR_Dusk_Dawn_v2/platformio_override.ini.sample @@ -0,0 +1,10 @@ +# LDR Dusk/Dawn usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_LDR_Dusk_Dawn_esp32dev + +[env:usermod_LDR_Dusk_Dawn_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + LDR_Dusk_Dawn diff --git a/usermods/MAX17048_v2/platformio_override.ini.sample b/usermods/MAX17048_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..0de0ca5fde --- /dev/null +++ b/usermods/MAX17048_v2/platformio_override.ini.sample @@ -0,0 +1,9 @@ +# MAX17048 battery fuel gauge usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_max17048_d1_mini + +[env:usermod_max17048_d1_mini] +extends = env:d1_mini +custom_usermods = ${env:d1_mini.custom_usermods} MAX17048_v2 diff --git a/usermods/Temperature/platformio_override.ini.sample b/usermods/Temperature/platformio_override.ini.sample new file mode 100644 index 0000000000..3ebaeabeca --- /dev/null +++ b/usermods/Temperature/platformio_override.ini.sample @@ -0,0 +1,10 @@ +# Temperature (DS18B20) usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_temperature_esp32dev + +[env:usermod_temperature_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + Temperature diff --git a/usermods/mpu6050_imu/platformio_override.ini.sample b/usermods/mpu6050_imu/platformio_override.ini.sample new file mode 100644 index 0000000000..cc5a591f63 --- /dev/null +++ b/usermods/mpu6050_imu/platformio_override.ini.sample @@ -0,0 +1,10 @@ +# MPU-6050 IMU usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_mpu6050_imu_esp32dev + +[env:usermod_mpu6050_imu_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + mpu6050_imu diff --git a/usermods/sht/platformio_override.ini.sample b/usermods/sht/platformio_override.ini.sample new file mode 100644 index 0000000000..e9d8b8d1a5 --- /dev/null +++ b/usermods/sht/platformio_override.ini.sample @@ -0,0 +1,13 @@ +# SHT temperature/humidity sensor usermod build environments +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = custom_esp32dev_usermod_sht + +[env:custom_esp32dev_usermod_sht] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} sht + +[env:custom_d1_mini_usermod_sht] +extends = env:d1_mini +custom_usermods = ${env:d1_mini.custom_usermods} sht diff --git a/usermods/usermod_v2_RF433/platformio_override.ini.sample b/usermods/usermod_v2_RF433/platformio_override.ini.sample new file mode 100644 index 0000000000..69c2d04c46 --- /dev/null +++ b/usermods/usermod_v2_RF433/platformio_override.ini.sample @@ -0,0 +1,11 @@ +# RF433 remote usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = esp32dev_usermod_RF433 + +[env:esp32dev_usermod_RF433] +extends = env:esp32dev +custom_usermods = + ${env:esp32dev.custom_usermods} + RF433 diff --git a/usermods/usermod_v2_klipper_percentage/platformio_override.ini.sample b/usermods/usermod_v2_klipper_percentage/platformio_override.ini.sample new file mode 100644 index 0000000000..5239a51ae7 --- /dev/null +++ b/usermods/usermod_v2_klipper_percentage/platformio_override.ini.sample @@ -0,0 +1,10 @@ +# Klipper percentage usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = esp32klipper + +[env:esp32klipper] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + usermod_v2_klipper_percentage From 2daf17a53bed86792a968661e8c9f2c496096cae Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 12:09:39 +0100 Subject: [PATCH 10/22] run on push --- .github/workflows/usermods.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 63f0100c5e..cc3c9ea374 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -4,6 +4,9 @@ on: pull_request: paths: - usermods/** + push: + paths: + - usermods/** env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true From af4b39efc44017f2441d8b4f4177130098672c24 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 12:21:34 +0100 Subject: [PATCH 11/22] Fix AHT10_v2 example --- usermods/AHT10_v2/platformio_override.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/usermods/AHT10_v2/platformio_override.ini b/usermods/AHT10_v2/platformio_override.ini index 74dcd659bb..a5123c3c7e 100644 --- a/usermods/AHT10_v2/platformio_override.ini +++ b/usermods/AHT10_v2/platformio_override.ini @@ -1,5 +1,3 @@ [env:aht10_example] extends = env:esp32dev -build_flags = - ${common.build_flags} ${esp32.build_flags} - ; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal +custom_usermods = ${env:esp32dev.custom_usermods} AHT10_v2 From d459e6c7310f9bd57a7431cb76edef9f07962fd0 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 12:26:32 +0100 Subject: [PATCH 12/22] no d1_mini env --- usermods/BME280_v2/platformio_override.ini.sample | 4 ++-- usermods/sht/platformio_override.ini.sample | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/usermods/BME280_v2/platformio_override.ini.sample b/usermods/BME280_v2/platformio_override.ini.sample index 39f2f808e7..d812efeba8 100644 --- a/usermods/BME280_v2/platformio_override.ini.sample +++ b/usermods/BME280_v2/platformio_override.ini.sample @@ -4,6 +4,6 @@ [platformio] default_envs = usermod_bme280_d1_mini -[env:usermod_bme280_d1_mini] -extends = env:d1_mini +[env:usermod_esp8266_2m] +extends = env:esp8266_2m custom_usermods = ${env:d1_mini.custom_usermods} BME280_v2 diff --git a/usermods/sht/platformio_override.ini.sample b/usermods/sht/platformio_override.ini.sample index e9d8b8d1a5..665c1d8862 100644 --- a/usermods/sht/platformio_override.ini.sample +++ b/usermods/sht/platformio_override.ini.sample @@ -8,6 +8,6 @@ default_envs = custom_esp32dev_usermod_sht extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} sht -[env:custom_d1_mini_usermod_sht] -extends = env:d1_mini +[env:custom_esp8266_2m_usermod_sht] +extends = env:esp8266_2m custom_usermods = ${env:d1_mini.custom_usermods} sht From e68bd2a3a5aec801eee33a3ce31aaebe21563c22 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 12:27:42 +0100 Subject: [PATCH 13/22] no d1_mini env --- usermods/BME280_v2/platformio_override.ini.sample | 4 ++-- usermods/DHT/platformio_override.ini | 8 ++++---- usermods/MAX17048_v2/platformio_override.ini.sample | 8 ++++---- usermods/SN_Photoresistor/platformio_override.ini | 4 ++-- usermods/sht/platformio_override.ini.sample | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/usermods/BME280_v2/platformio_override.ini.sample b/usermods/BME280_v2/platformio_override.ini.sample index d812efeba8..12fa389e59 100644 --- a/usermods/BME280_v2/platformio_override.ini.sample +++ b/usermods/BME280_v2/platformio_override.ini.sample @@ -2,8 +2,8 @@ # Copy to platformio_override.ini in the WLED root to use. [platformio] -default_envs = usermod_bme280_d1_mini +default_envs = usermod_bme280_esp8266_2m [env:usermod_esp8266_2m] extends = env:esp8266_2m -custom_usermods = ${env:d1_mini.custom_usermods} BME280_v2 +custom_usermods = ${env:esp8266_2m.custom_usermods} BME280_v2 diff --git a/usermods/DHT/platformio_override.ini b/usermods/DHT/platformio_override.ini index 6ec2fb9992..e78137b774 100644 --- a/usermods/DHT/platformio_override.ini +++ b/usermods/DHT/platformio_override.ini @@ -8,10 +8,10 @@ ; USERMOD_DHT_MQTT - publish measurements to the MQTT broker ; USERMOD_DHT_STATS - For debug, report delay stats -[env:d1_mini_usermod_dht_C] -extends = env:d1_mini -custom_usermods = ${env:d1_mini.custom_usermods} DHT -build_flags = ${env:d1_mini.build_flags} -D USERMOD_DHT_CELSIUS +[env:esp8266_2m_usermod_dht_C] +extends = env:esp8266_2m +custom_usermods = ${env:esp8266_2m.custom_usermods} DHT +build_flags = ${env:esp8266_2m.build_flags} -D USERMOD_DHT_CELSIUS [env:custom32_LEDPIN_16_usermod_dht_C] extends = env:custom32_LEDPIN_16 diff --git a/usermods/MAX17048_v2/platformio_override.ini.sample b/usermods/MAX17048_v2/platformio_override.ini.sample index 0de0ca5fde..dd339bf758 100644 --- a/usermods/MAX17048_v2/platformio_override.ini.sample +++ b/usermods/MAX17048_v2/platformio_override.ini.sample @@ -2,8 +2,8 @@ # Copy to platformio_override.ini in the WLED root to use. [platformio] -default_envs = usermod_max17048_d1_mini +default_envs = usermod_max17048_esp8266_2m -[env:usermod_max17048_d1_mini] -extends = env:d1_mini -custom_usermods = ${env:d1_mini.custom_usermods} MAX17048_v2 +[env:usermod_max17048_esp8266_2m] +extends = env:esp8266_2m +custom_usermods = ${env:esp8266_2m.custom_usermods} MAX17048_v2 diff --git a/usermods/SN_Photoresistor/platformio_override.ini b/usermods/SN_Photoresistor/platformio_override.ini index 91bc5de2a6..fa75342f7f 100644 --- a/usermods/SN_Photoresistor/platformio_override.ini +++ b/usermods/SN_Photoresistor/platformio_override.ini @@ -8,8 +8,8 @@ ; USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE - the resistor size, defaults to 10000.0 (10K hms) ; USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE - the offset value to report on, defaults to 25 ; -[env:usermod_sn_photoresistor_d1_mini] -extends = env:d1_mini +[env:usermod_sn_photoresistor_esp8266_2m] +extends = env:esp8266_2m build_flags = ${common.build_flags_esp8266} -D USERMOD_SN_PHOTORESISTOR diff --git a/usermods/sht/platformio_override.ini.sample b/usermods/sht/platformio_override.ini.sample index 665c1d8862..65e3ffd153 100644 --- a/usermods/sht/platformio_override.ini.sample +++ b/usermods/sht/platformio_override.ini.sample @@ -10,4 +10,4 @@ custom_usermods = ${env:esp32dev.custom_usermods} sht [env:custom_esp8266_2m_usermod_sht] extends = env:esp8266_2m -custom_usermods = ${env:d1_mini.custom_usermods} sht +custom_usermods = ${env:esp8266_2m.custom_usermods} sht From 50cd90053d394371d227c3b0b128dcaa70338435 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 12:31:17 +0100 Subject: [PATCH 14/22] ci: filter usermod matrix to only build changed usermod directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of building every usermod with a library.json on each PR, use git diff to identify which usermods/ subdirectories were actually touched and intersect that with the known-good library.json list. This reduces CI time significantly for PRs that only modify one or two usermods (previously every PR triggered ~40 usermods × 4 chipsets). Also removes the unnecessary PlatformIO install from get_usermod_envs (the step only uses shell/jq, not pio) and adds fetch-depth: 0 to ensure the base branch is available for the diff. --- .github/workflows/usermods.yml | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index cc3c9ea374..fa5f6bc702 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -20,23 +20,35 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 with: - python-version: '3.12' - cache: 'pip' - - name: Install PlatformIO - run: pip install -r requirements.txt - - name: Get default environments + fetch-depth: 0 + - name: Get changed usermod environments id: envs run: | - echo "usermods=$(find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | jq -R | grep -v PWM_fan | grep -v BME68X_v2| grep -v pixels_dice_tray | jq --slurp -c)" >> $GITHUB_OUTPUT + # Usermods whose directories changed in this PR + changed=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \ + | grep '^usermods/' | cut -d/ -f2 | sort -u || true) + + # All usermods with a library.json (excluding known-incompatible ones) + all=$(find usermods/ -name library.json \ + | xargs dirname | xargs -n 1 basename \ + | grep -v PWM_fan | grep -v BME68X_v2 | grep -v pixels_dice_tray \ + | sort || true) + + if [ -z "$changed" ] || [ -z "$all" ]; then + echo "usermods=[]" >> $GITHUB_OUTPUT + else + usermods=$(comm -12 <(echo "$all") <(echo "$changed") | jq -R | jq --slurp -c) + echo "usermods=$usermods" >> $GITHUB_OUTPUT + fi outputs: usermods: ${{ steps.envs.outputs.usermods }} build: # Only run for pull requests from forks (not from branches within wled/WLED) - if: github.event.pull_request.head.repo.full_name != github.repository + # Skip when no changed usermods were found (e.g. only non-library changes) + if: github.event.pull_request.head.repo.full_name != github.repository && needs.get_usermod_envs.outputs.usermods != '[]' name: Build Enviornments runs-on: ubuntu-latest needs: get_usermod_envs From fb0c1dfcd0a8a2cf95e34f6dbea3329a7b98ff7a Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 12:42:18 +0100 Subject: [PATCH 15/22] SN_Photoresistor --- usermods/SN_Photoresistor/platformio_override.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usermods/SN_Photoresistor/platformio_override.ini b/usermods/SN_Photoresistor/platformio_override.ini index fa75342f7f..c5e05ed3ee 100644 --- a/usermods/SN_Photoresistor/platformio_override.ini +++ b/usermods/SN_Photoresistor/platformio_override.ini @@ -1,6 +1,5 @@ ; Options ; ------- -; USERMOD_SN_PHOTORESISTOR - define this to have this user mod included wled00\usermods_list.cpp ; USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds ; USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - the number of milliseconds after boot to take first measurement, defaults to 20 seconds ; USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE - the voltage supplied to the sensor, defaults to 5v @@ -10,7 +9,8 @@ ; [env:usermod_sn_photoresistor_esp8266_2m] extends = env:esp8266_2m +custom_usermods = ${env:esp8266_2m.custom_usermods} SN_Photoresistor build_flags = ${common.build_flags_esp8266} - -D USERMOD_SN_PHOTORESISTOR + -D USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL=60 lib_deps = ${env.lib_deps} From 9b4eafa8b7bd101fdd2039499c084c011faba01c Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 12:58:52 +0100 Subject: [PATCH 16/22] fix(ci): use PR base SHA for diff and guard jobs against push events github.base_ref is empty on push events, causing 'ambiguous argument origin/...HEAD'. Fix by: - Adding github.event_name == 'pull_request' guard to both jobs so they never run on push events where pull_request context is absent - Replacing origin/${{ github.base_ref }}...HEAD with ${{ github.event.pull_request.base.sha }} HEAD which uses the concrete base commit SHA provided by GitHub directly --- .github/workflows/usermods.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index fa5f6bc702..05b72370da 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -15,7 +15,7 @@ jobs: get_usermod_envs: # Only run for pull requests from forks (not from branches within wled/WLED) - if: github.event.pull_request.head.repo.full_name != github.repository + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository name: Gather Usermods runs-on: ubuntu-latest steps: @@ -26,7 +26,7 @@ jobs: id: envs run: | # Usermods whose directories changed in this PR - changed=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \ + changed=$(git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD \ | grep '^usermods/' | cut -d/ -f2 | sort -u || true) # All usermods with a library.json (excluding known-incompatible ones) @@ -48,7 +48,7 @@ jobs: build: # Only run for pull requests from forks (not from branches within wled/WLED) # Skip when no changed usermods were found (e.g. only non-library changes) - if: github.event.pull_request.head.repo.full_name != github.repository && needs.get_usermod_envs.outputs.usermods != '[]' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && needs.get_usermod_envs.outputs.usermods != '[]' name: Build Enviornments runs-on: ubuntu-latest needs: get_usermod_envs From 30075640733bd445cb87ba6823653a5aef29a10d Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 13:00:57 +0100 Subject: [PATCH 17/22] ESP32 builds all V4 --- .../platformio_override.sample.ini | 6 +++--- .../platformio_override.sample.ini | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini index f4fa8c9d8b..ef0e00848f 100644 --- a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini @@ -2,10 +2,10 @@ default_envs = esp32dev_fld [env:esp32dev_fld] -extends = env:esp32dev_V4 -custom_usermods = ${env:esp32dev_V4.custom_usermods} four_line_display_ALT +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} four_line_display_ALT build_flags = - ${env:esp32dev_V4.build_flags} + ${env:esp32dev.build_flags} -D FLD_TYPE=SH1106 -D I2CSCLPIN=27 -D I2CSDAPIN=26 diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini index 2511d2fa38..c943bdbe25 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini @@ -2,10 +2,10 @@ default_envs = esp32dev_re [env:esp32dev_re] -extends = env:esp32dev_V4 -custom_usermods = ${env:esp32dev_V4.custom_usermods} rotary_encoder_ui_ALT +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} rotary_encoder_ui_ALT build_flags = - ${env:esp32dev_V4.build_flags} + ${env:esp32dev.build_flags} -D USERMOD_ROTARY_ENCODER_GPIO=INPUT -D ENCODER_DT_PIN=21 -D ENCODER_CLK_PIN=23 From 83e32eae2d54b33b04a701e73f592cf3066ed790 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 13:08:26 +0100 Subject: [PATCH 18/22] fix: rename/fix platformio_override sample files across usermods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename *.ini (gitignored) and *.sample.ini (wrong extension order) to the correct platformio_override.ini.sample convention so CI discovers them - Fix AHT10_v2: custom_usermods AHT10 → AHT10_v2 (match library.json name) - Fix INA226_v2: custom_usermods INA226 → INA226_v2 (both envs) - Fix TTGO-T-Display: replace direct [env:esp32dev] override with a named env that extends esp32dev; add note that library.json is absent so custom_usermods is not available for this usermod Affected usermods: AHT10_v2, DHT, INA226_v2, SN_Photoresistor, TTGO-T-Display, Temperature, four_line_display_ALT, rotary_encoder_ui_ALT --- usermods/AHT10_v2/platformio_override.ini | 3 --- usermods/AHT10_v2/platformio_override.ini.sample | 2 +- ...erride.ini => platformio_override.ini.sample} | 0 usermods/INA226_v2/platformio_override.ini | 6 ------ .../INA226_v2/platformio_override.ini.sample | 4 ++-- ...erride.ini => platformio_override.ini.sample} | 0 usermods/TTGO-T-Display/platformio_override.ini | 8 -------- .../platformio_override.ini.sample | 16 ++++++++++++++++ usermods/Temperature/platformio_override.ini | 5 ----- ...sample.ini => platformio_override.ini.sample} | 0 ...sample.ini => platformio_override.ini.sample} | 0 11 files changed, 19 insertions(+), 25 deletions(-) delete mode 100644 usermods/AHT10_v2/platformio_override.ini rename usermods/DHT/{platformio_override.ini => platformio_override.ini.sample} (100%) delete mode 100644 usermods/INA226_v2/platformio_override.ini rename usermods/SN_Photoresistor/{platformio_override.ini => platformio_override.ini.sample} (100%) delete mode 100644 usermods/TTGO-T-Display/platformio_override.ini create mode 100644 usermods/TTGO-T-Display/platformio_override.ini.sample delete mode 100644 usermods/Temperature/platformio_override.ini rename usermods/usermod_v2_four_line_display_ALT/{platformio_override.sample.ini => platformio_override.ini.sample} (100%) rename usermods/usermod_v2_rotary_encoder_ui_ALT/{platformio_override.sample.ini => platformio_override.ini.sample} (100%) diff --git a/usermods/AHT10_v2/platformio_override.ini b/usermods/AHT10_v2/platformio_override.ini deleted file mode 100644 index a5123c3c7e..0000000000 --- a/usermods/AHT10_v2/platformio_override.ini +++ /dev/null @@ -1,3 +0,0 @@ -[env:aht10_example] -extends = env:esp32dev -custom_usermods = ${env:esp32dev.custom_usermods} AHT10_v2 diff --git a/usermods/AHT10_v2/platformio_override.ini.sample b/usermods/AHT10_v2/platformio_override.ini.sample index 56bba5aa2e..993b99ce39 100644 --- a/usermods/AHT10_v2/platformio_override.ini.sample +++ b/usermods/AHT10_v2/platformio_override.ini.sample @@ -6,4 +6,4 @@ default_envs = aht10_example [env:aht10_example] extends = env:esp32dev -custom_usermods = ${env:esp32dev.custom_usermods} AHT10 +custom_usermods = ${env:esp32dev.custom_usermods} AHT10_v2 diff --git a/usermods/DHT/platformio_override.ini b/usermods/DHT/platformio_override.ini.sample similarity index 100% rename from usermods/DHT/platformio_override.ini rename to usermods/DHT/platformio_override.ini.sample diff --git a/usermods/INA226_v2/platformio_override.ini b/usermods/INA226_v2/platformio_override.ini deleted file mode 100644 index 9968cbf721..0000000000 --- a/usermods/INA226_v2/platformio_override.ini +++ /dev/null @@ -1,6 +0,0 @@ -[env:ina226_example] -extends = env:esp32dev -custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2 -build_flags = - ${env:esp32dev.build_flags} - ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal diff --git a/usermods/INA226_v2/platformio_override.ini.sample b/usermods/INA226_v2/platformio_override.ini.sample index c38f572f47..b735ac5048 100644 --- a/usermods/INA226_v2/platformio_override.ini.sample +++ b/usermods/INA226_v2/platformio_override.ini.sample @@ -7,14 +7,14 @@ default_envs = ina226_example # Minimal example — enable the usermod with default settings [env:ina226_example] extends = env:esp32dev -custom_usermods = ${env:esp32dev.custom_usermods} INA226 +custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2 build_flags = ${env:esp32dev.build_flags} ; -D USERMOD_INA226_DEBUG ; uncomment to add debug status to the info modal # Custom calibration example — adjust constants to match your hardware [env:ina226_custom] extends = env:esp32dev -custom_usermods = ${env:esp32dev.custom_usermods} INA226 +custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2 build_flags = ${env:esp32dev.build_flags} -D INA226_ENABLED_DEFAULT=true -D INA226_SHUNT_MICRO_OHMS=2888 diff --git a/usermods/SN_Photoresistor/platformio_override.ini b/usermods/SN_Photoresistor/platformio_override.ini.sample similarity index 100% rename from usermods/SN_Photoresistor/platformio_override.ini rename to usermods/SN_Photoresistor/platformio_override.ini.sample diff --git a/usermods/TTGO-T-Display/platformio_override.ini b/usermods/TTGO-T-Display/platformio_override.ini deleted file mode 100644 index 7e42d9a54a..0000000000 --- a/usermods/TTGO-T-Display/platformio_override.ini +++ /dev/null @@ -1,8 +0,0 @@ -[env:esp32dev] -build_flags = ${common.build_flags_esp32} -; PIN defines - uncomment and change, if needed: -; -D LEDPIN=2 - -D BTNPIN=35 -; -D IRPIN=4 -; -D RLYPIN=12 -; -D RLYMDE=1 diff --git a/usermods/TTGO-T-Display/platformio_override.ini.sample b/usermods/TTGO-T-Display/platformio_override.ini.sample new file mode 100644 index 0000000000..2777b4f21c --- /dev/null +++ b/usermods/TTGO-T-Display/platformio_override.ini.sample @@ -0,0 +1,16 @@ +; TTGO-T-Display usermod build example. +; Note: this usermod has no library.json so custom_usermods is not available. +; The usermod.cpp must be included manually in your build. + +[platformio] +default_envs = ttgo_t_display_example + +[env:ttgo_t_display_example] +extends = env:esp32dev +build_flags = ${env:esp32dev.build_flags} +; PIN defines - uncomment and change, if needed: +; -D LEDPIN=2 + -D BTNPIN=35 +; -D IRPIN=4 +; -D RLYPIN=12 +; -D RLYMDE=1 diff --git a/usermods/Temperature/platformio_override.ini b/usermods/Temperature/platformio_override.ini deleted file mode 100644 index a53b5974d9..0000000000 --- a/usermods/Temperature/platformio_override.ini +++ /dev/null @@ -1,5 +0,0 @@ -; Options -; ------- -; USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds -; - diff --git a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini b/usermods/usermod_v2_four_line_display_ALT/platformio_override.ini.sample similarity index 100% rename from usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini rename to usermods/usermod_v2_four_line_display_ALT/platformio_override.ini.sample diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.ini.sample similarity index 100% rename from usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini rename to usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.ini.sample From 818c3cbced938917d3ad627806f73e01150edfcf Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 13:09:17 +0100 Subject: [PATCH 19/22] perf: filter custom build matrix to changed usermods on PRs On pull_request events, get_custom_build_envs now only scans usermod directories that changed in the PR (matching the behaviour of the get_usermod_envs job). On push events (e.g. merging a PR) it still scans all usermods to validate the full set. --- .github/workflows/usermods.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 05b72370da..a890c96307 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -97,11 +97,30 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Find usermods with custom build environments id: custom_envs run: | + # On PRs: only scan usermods whose directories changed. + # On push: scan all usermods (validates the full set on merge). + if [ "${{ github.event_name }}" = "pull_request" ]; then + changed=$(git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD \ + | grep '^usermods/' | cut -d/ -f2 | sort -u || true) + if [ -z "$changed" ]; then + echo "matrix=[]" >> $GITHUB_OUTPUT + exit 0 + fi + samples=$(for mod in $changed; do + f="usermods/$mod/platformio_override.ini.sample" + [ -f "$f" ] && echo "$f" + done | sort) + else + samples=$(find usermods/ -name "platformio_override.ini.sample" | sort) + fi + result='[]' - for sample in $(find usermods/ -name "platformio_override.ini.sample" | sort); do + for sample in $samples; do usermod=$(dirname "$sample" | xargs basename) envs=$(grep -E '^\[env:[^]]+\]' "$sample" | sed 's/^\[env:\(.*\)\]$/\1/') for env in $envs; do From 76a505fb378415c2e3869c644248ed4ed86d1db9 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 13:12:58 +0100 Subject: [PATCH 20/22] fix release name --- usermods/pixels_dice_tray/platformio_override.ini.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/pixels_dice_tray/platformio_override.ini.sample b/usermods/pixels_dice_tray/platformio_override.ini.sample index 6b4fa7768e..74237c2aa0 100644 --- a/usermods/pixels_dice_tray/platformio_override.ini.sample +++ b/usermods/pixels_dice_tray/platformio_override.ini.sample @@ -75,7 +75,7 @@ board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_qspi +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_qspi_dice\" -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") From 76b1e0a078acbe4dfd06209aaab3fc9e04d67d76 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 13:14:05 +0100 Subject: [PATCH 21/22] fix release name --- usermods/pixels_dice_tray/platformio_override.ini.sample | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usermods/pixels_dice_tray/platformio_override.ini.sample b/usermods/pixels_dice_tray/platformio_override.ini.sample index 74237c2aa0..f51c2190b3 100644 --- a/usermods/pixels_dice_tray/platformio_override.ini.sample +++ b/usermods/pixels_dice_tray/platformio_override.ini.sample @@ -13,7 +13,7 @@ board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=T-QT-PRO-8MB +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"T-QT-PRO-8MB_dice\" -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") @@ -105,7 +105,7 @@ lib_deps = ${esp32s3.lib_deps} # https://github.com/wled-dev/WLED/issues/1382 ; [env:esp32dev_dice] ; extends = env:esp32dev -; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 +; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_dice\" ; ; Enable Pixels dice mod ; -D USERMOD_PIXELS_DICE_TRAY ; lib_deps = ${esp32.lib_deps} From fb7e30967aa5fadcde2bae8ff36ffcc8899f2a48 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 24 May 2026 13:16:03 +0100 Subject: [PATCH 22/22] Revert "esp32s3dev_8MB_opi_dali_gear" This reverts commit 23a93604899fb0612862beb39796e9024ad1cd05. --- platformio.ini | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index c834381183..f08e7c151f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,8 +10,29 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = - esp32s3dev_8MB_opi_dali_gear +default_envs = nodemcuv2 + esp8266_2m + esp01_1m_full + nodemcuv2_160 + esp8266_2m_160 + esp01_1m_full_160 + nodemcuv2_compat + esp8266_2m_compat + esp01_1m_full_compat + esp32dev + esp32dev_debug + esp32_eth + esp32_wrover + lolin_s2_mini + esp32c3dev + esp32c3dev_qio + esp32S3_wroom2 + esp32s3dev_16MB_opi + esp32s3dev_8MB_opi + esp32s3dev_8MB_qspi + esp32s3dev_8MB_none + esp32s3_4M_qspi + usermods src_dir = ./wled00 data_dir = ./wled00/data