Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
eaecca9
docs(dali_gear): update readme to match implemented behaviour
netmindz May 23, 2026
0fc601c
esp32s3dev_8MB_opi_dali_gear
netmindz May 23, 2026
23a9360
esp32s3dev_8MB_opi_dali_gear
netmindz May 23, 2026
8394038
fix(dali_gear): address code review comments
netmindz May 24, 2026
6bc19e9
feat(dali_gear): address PR review — TX invert, query responses, QUER…
netmindz May 24, 2026
0d77a27
fix(dali_gear): do not respond to 0xE7 — QUERY COLOUR TYPE is 0xF7 only
netmindz May 24, 2026
670a6fd
feat(dali_gear): respond to QUERY DEVICE TYPE (0x18) with 0x08
netmindz May 24, 2026
71767b7
ci: discover and build custom PlatformIO envs from usermods platformi…
netmindz May 24, 2026
badfcfc
ci: consolidate usermod build envs into per-usermod platformio_overri…
netmindz May 24, 2026
2daf17a
run on push
netmindz May 24, 2026
af4b39e
Fix AHT10_v2 example
netmindz May 24, 2026
d459e6c
no d1_mini env
netmindz May 24, 2026
e68bd2a
no d1_mini env
netmindz May 24, 2026
50cd900
ci: filter usermod matrix to only build changed usermod directories
netmindz May 24, 2026
fb0c1df
SN_Photoresistor
netmindz May 24, 2026
9b4eafa
fix(ci): use PR base SHA for diff and guard jobs against push events
netmindz May 24, 2026
3007564
ESP32 builds all V4
netmindz May 24, 2026
83e32ea
fix: rename/fix platformio_override sample files across usermods
netmindz May 24, 2026
818c3cb
perf: filter custom build matrix to changed usermods on PRs
netmindz May 24, 2026
76a505f
fix release name
netmindz May 24, 2026
76b1e0a
fix release name
netmindz May 24, 2026
133aecb
Merge branch 'usermod-ci' into dali2
netmindz May 24, 2026
fb7e309
Revert "esp32s3dev_8MB_opi_dali_gear"
netmindz May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions usermods/dali_gear/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "dali_gear",
"build": { "libArchive": false },
"dependencies": {
"qqqDALI": "https://github.com/netmindz/DALI-Lighting-Interface.git#fix/esp32-volatile-cast"
}
}
140 changes: 140 additions & 0 deletions usermods/dali_gear/readme.md
Original file line number Diff line number Diff line change
@@ -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-
```

@coderabbitai coderabbitai Bot May 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the fenced code block.

Line 15 triggers markdownlint MD040. Add a fence language (e.g., text) to satisfy linting.

Proposed change
-```
+```text
 3.3V ESP32         5.6V               ___
                    Zener        +----|___|---- 12V Power Supply
             ___    Diode        |     220 Ω
 RX ---+-----|___|---|>|----------+------------- DALI+
...
 GND --+---------------------------+------------- DALI-
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

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

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 15-15: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/dali_gear/readme.md` around lines 15 - 27, The fenced ASCII diagram
in the readme triggers markdownlint MD040 because it lacks a language tag;
update the fenced code block surrounding the diagram (the triple-backtick block
that currently contains the ASCII schematic with lines like "3.3V ESP32", "RX
---+-----|___|---|>|----------+------------- DALI+", and "GND
--+---------------------------+------------- DALI-") to include a language
identifier (for example "text") immediately after the opening ``` so the block
becomes ```text and satisfies the linter.

@mff-atx mff-atx May 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A PNP would pull the GPIO above 12v, and there would be no isolation. The Waveshare has an NPN and an Opto, so the output should be normally at 0 v, and go to 3.3v during xmit. https://files.waveshare.com/wiki/Pico-DALI2/Pico-DALI2.pdf. I added an inverter for testing - and this fw release is not responding on the GPIO I selected ( 17 ). It is adjusting the light from Rx I set to 15. Pins are shown used by usermod - but only after a reboot. I tested a E7 command and verified the 02 response. Can you invert the output?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the actual DALI command bytes used in the usermod for QUERY COLOUR TYPE and other backward frame responses
rg -n "0xE7|0xF7|0x90|0x91|0xA0|QUERY|backward|response" usermods/dali_gear/usermod_dali_gear.cpp

Length of output: 906


🏁 Script executed:

#!/bin/bash
# Get more context around the QUERY COLOUR TYPE handling and the full loop/frame processing
rg -n "0xE7|0xF7|0x90|0x91|0xA0|addr|broadcast|short.addr|frame|command" usermods/dali_gear/usermod_dali_gear.cpp -A2 -B2 | head -120

Length of output: 5446


🏁 Script executed:

#!/bin/bash
# Look for TX inversion, inverted, or polarity options in the qqqDALI library or the usermod
rg -rn "invert|polarity|active.low|active.high|tx.*pin|pin.*tx" usermods/dali_gear/ -i

# Also check the full command handling switch block for what commands produce backward frames
rg -n "case 0x|scheduleBF|backward" usermods/dali_gear/usermod_dali_gear.cpp -A1

# Check qqqDALI library for invert/polarity API
fd -e h -e cpp . usermods/dali_gear/ --exec grep -n "invert\|polarity\|active" {}

Length of output: 2795


@mff-atx, thank you for the thorough hardware testing — these are all valid and important findings. Here's a breakdown of each point:


1. 🐛 0xE70xF7 bug confirmed

The code at line 225 handles case 0xE7: for QUERY COLOUR TYPE, but you are correct: per IEC 62386-209 §11.3.4.2, the application extended query commands occupy the 0xF0–0xFF range, making QUERY COLOUR TYPE = 0xF7, not 0xE7. 0xE7 would fall in the "set" command range and is not QUERY COLOUR TYPE. The fix needed in usermod_dali_gear.cpp:

-        case 0xE7:
+        case 0xF7:

…and the same correction in the comment on line 202 and the file header comment on line 12.


2. ⚡ TX output polarity / inversion

There is currently no software inversion option in the code. For hardware like the Waveshare Pico-DALI2 (NPN + opto, normally-low, driven-high on transmit) you may need the opposite polarity from the default idle-HIGH behaviour. A _txInverted config boolean could solve this without requiring an external inverter:

// in setup():
digitalWrite(_txPin, _txInverted ? LOW : HIGH);  // idle state

// in HAL tx callback (HIGH → assertion becomes inverted):
digitalWrite(_nStatic, _txInvertedStatic ? HIGH : LOW);   // assert bus
// ...
digitalWrite(_nStatic, _txInvertedStatic ? LOW  : HIGH);  // release bus

A corresponding config key "tx_inverted" in addToConfig/readFromConfig would expose this in the UI.


3. 🔌 Missing backward frame responses: 0x91, 0x90, 0xA0

You are correct that many DALI masters run a commissioning/presence-detection sequence before they query DT8. The most commonly required responses are:

Command (cmd byte) Name Required response
0x90 QUERY STATUS status byte
0x91 QUERY CONTROL GEAR PRESENT 0xFF ("Yes")
0xA0 QUERY ACTUAL LEVEL current arc level byte

Without 0xFF to 0x91, many masters consider the gear absent and never proceed to 0xF7. These need to be added to the handleCommand() switch block alongside the existing DT8 cases.


4. ⚠️ ASCII circuit diagram (PNP safety concern)

You are right — the diagram sourced from the upstream qqqlab README shows a PNP transistor wired in a way that would expose the ESP32 GPIO to bus-level voltage without isolation. The README should be updated to either replace that diagram with one matching the actual Waveshare Pico-DALI2 (NPN + opto-isolator) or add a prominent warning that the qqqlab diagram is conceptual and not safe to build as-is.


Would you like me to open a GitHub issue to track these items (correct command byte, TX inversion config, missing gear-presence responses, circuit diagram correction) for the PR author @netmindz?

🐇 🔧

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hi @mff-atx - Are you using the Pico-Dali2 board or the DIY circuit?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Using the Pico-DALI2, qqqlab DIY circuit has many flaws, see update on https://github.com/mff-atx/DALI-Lighting-Interface/tree/patch-1


> **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.
Loading
Loading