DLED

DLED / LPD-1

Open tools to program and play the LPU-1 lighting controller over the network,
and an ESP32 device that speaks its protocol.

Licence: GPL-3.0 Flutter ESP32 Protocol

--- ## What is this? The LPU-1 is a small stand-alone DMX lighting controller: it holds a show of up to 64 fixtures and 64 playbacks and plays it on its DMX output, Art-Net and sACN. It is programmed over Ethernet with an app. **DLED** is an independent, open-source set of tools that talk to the LPU-1 on the network: | | Part | What it does | |---|---|---| | 📱 | **DLED app** (`dled_flutter/`) | Program and play the LPU-1 from Windows, Android or a browser: playbacks, cues, a live programmer with FX, fixture patch, stored positions, show files | | 🌐 | **DLED light page** (`dled_flutter/light/`) | The same app as a small web page for any phone or tablet browser | | 🔌 | **DLED bridge** (`dled_flutter/bin/`, `firmware/dled_bridge/`) | Serves the light page and passes its traffic to the LPU, on a PC or on a small ESP32 box | | 💡 | **LPD-1** (`firmware/lpd1/`) | An ESP32 device that speaks the LPU-1's protocol and computes the DMX itself: the apps work with it as with an LPU-1 | | 🔬 | **Protocol notes and tools** (`analysis/`, `lpd/`) | How the protocol and the `.show` format work, a Python library, an emulator, and DMX measurements |

The DLED app: 64 playbacks with faders, speed knobs and FLASH buttons
The DLED app's PLAYBACK page (not connected): 64 playbacks, a row of faders, speed knobs and FLASH buttons.

> DLED is not affiliated with or endorsed by the maker of the LPU-1. The > device's name is used only to say what these tools work with. In the code > and notes, **"the reference app"** means the app that comes with the > LPU-1; DLED was tested against it so both can be used side by side. ## Compatibility and where DLED is going | | with an LPU-1 | with an LPD-1 | |---|---|---| | **DLED app and light page** | ✅ everything the LPU-1 can do | ✅ the same, plus new DLED features as they come | | **The reference app** | ✅ (its own app) | ✅ the LPU-1 features; not the new DLED features | - **Version 1.x is fully LPU-1 compatible.** Everything in DLED today works with an LPU-1, and the LPD-1 behaves like one. The release tagged [`v1.0`](https://github.com/tobios123/dled/releases/tag/v1.0) is that state, and stays available as it is. - **From 2.x the LPD-1 gets features of its own.** They are added on top of the LPU-1 protocol, not in place of it. The app checks what it's talking to (the LPD-1 reports its name and version when it's found) and offers a new feature only where the device has it. With an LPU-1 the app keeps working as in 1.x. - Features that need an LPD-1 will be marked **LPD-1 only** in this README and in the app. ## How the pieces fit

How the pieces fit: the DLED app, light page and CLI talk to an LPU-1 or LPD-1, the light page through the DLED bridge; the devices drive the fixtures

A browser cannot send UDP, so the light page talks to a **bridge** over a WebSocket and the bridge sends the datagrams. The app and the CLI talk to the device directly. ## A session, step by step

A session: discovery, reading the show, GO and fader with notifications back

Every write is echoed by the device, and every change (from any app) is sent as a notification, so all connected apps stay in step. The full protocol is in [`analysis/FINDINGS.md`](analysis/FINDINGS.md) (section 20) and [`dled_flutter/VN_PROTOCOL.md`](dled_flutter/VN_PROTOCOL.md). ## What we found The short version of the protocol and engine notes. Each point links to the details. - **One UDP port for everything.** Apps find the LPU-1 with a broadcast (`visual_identify`) and then talk to it on UDP 16945. Every message is a 20-byte header (which object: fixture, playback or cue; which property; up to three indexes) plus a value. Every write is echoed, and every change is pushed to all apps as a notification. ([FINDINGS](analysis/FINDINGS.md) sections 1 and 20) - **A show is 64 fixtures and 64 playbacks of up to 48 cues.** Each cue is stored as 4 blocks of 800 bytes, one value per fixture attribute. The app reads and writes the show block by block. ([SHOW_DOWNLOAD_FLOW](analysis/SHOW_DOWNLOAD_FLOW.md)) - **49 cues hang the LPU-1.** Writing a 49th cue into a playback stops the device answering until a power cycle. DLED refuses such a show before sending anything. ([DMX_ENGINE](lpd/DMX_ENGINE.md) section 12a) - **A fixture is a string of function codes**, two hex digits per DMX channel (`01` intensity, `02` pan, `04` red ...). Code `00` ends the fixture, and a fine channel is its code + `20`. ([FINDINGS](analysis/FINDINGS.md) section 3) - **Cue values are 16 bits**, little-endian. Pan and tilt are signed around the centre. ([DMX_ENGINE](lpd/DMX_ENGINE.md) section 2) - **DMX out:** Art-Net and sACN together, 40 frames a second, always all 512 channels. A GO shows up 1 to 4 frames later. ([DMX_ENGINE](lpd/DMX_ENGINE.md) section 1) - **The playback fader dims only intensity**; colour and position stay. FX (intensity, colour, position) follow fixed wave shapes with a phase spread over the fixtures, and the speed knob scales them. ([DMX_ENGINE](lpd/DMX_ENGINE.md) sections 3 to 7) - **`.show` files are XML.** The reference app opens the files DLED writes, but drops sections it doesn't know. That's why DLED keeps stored positions in a file of their own next to the show. ## Features

The DLED app: PLAYBACK, EDIT, PATCH and OPTIONS and what each holds

- **Live:** every slider, GO and fader goes to the device at once, and changes made elsewhere show up in the app. - **Show files:** `.show` files that the reference app opens as well. - **Positions:** store the aim of every moving head on a tile and recall it in any cue, per fixture (saved next to the show as `NAME.positions.json`). - **No pop-ups:** the app never interrupts a show with messages. ## Repository map ```text dled_flutter/ the app (Flutter), the light page, the CLI and the PC bridge tool/ light page build, icons, fixture library tool lib/core/ protocol, .show files, cue records: pure Dart, shared by all lib/screens/ PLAYBACK, EDIT, POSITIONS, PATCH, OPTIONS light/ the light page (HTML / JS / CSS + the shared Dart as JS) bin/ dled_cli.dart, dled_bridge.dart test/ the app's tests firmware/ lpd1/ LPD-1: DMX engine (C++17), VN server, ESP32 board code, tests dled_bridge/ the DLED bridge on an ESP32 (WT32-ETH01) analysis/ protocol notes, Python library (analysis/lpu), tools, tests lpd/ DMX measurements of the LPU-1 and the tests behind them samples/ example .show files docs/ pictures and diagrams for this README .claude/skills/ Claude Code skills (see "Claude Code skills" below) ``` ## Getting started ### The app Needs [Flutter](https://docs.flutter.dev/get-started/install) 3.x. ```bash cd dled_flutter flutter test flutter run -d windows ``` `flutter build apk` builds the Android app. On Windows, saved shows go to `Documents\DLED\shows`. Open **OPTIONS**, choose your LPU in the list, and the app downloads its show. ### The light page in a browser ```bash cd dled_flutter dart run tool/build_light.dart dart run bin/dled_bridge.dart --web light/dist --port 8081 ``` Open `http://:8081` on a phone or tablet in the same network. For a stand-alone box, flash `firmware/dled_bridge` to a WT32-ETH01 (see the notes at the top of its `platformio.ini`); it then serves the page at `http://dled.local`. ### The command line ```bash cd dled_flutter dart run bin/dled_cli.dart discover dart run bin/dled_cli.dart status --ip 192.168.1.12 dart run bin/dled_cli.dart download --ip 192.168.1.12 --show backup.show ``` Also `info`, `go`, `flash`, `release`, `release-all`, `upload` (a dry run until `--yes`, then backup, upload and read-back check), `playback`, `set`, `monitor` and `show`. On Windows, `dled.cmd` runs it from any folder. ### LPD-1 Needs [PlatformIO](https://platformio.org/). The engine's tests run on a PC (with gcc): ```bash cd firmware/lpd1 python -m platformio test -e native python -m platformio run -e wt32-eth01 ``` Wiring and the first flash: [`firmware/lpd1/FIRST_FLASH.md`](firmware/lpd1/FIRST_FLASH.md). Later updates go over the network (`-e wt32-eth01-ota`). ### Python library and tools ```bash python -m unittest discover -s analysis/tests python analysis/scripts/lpu_emulator.py --show samples/64fixtures.show ``` Python 3.9+, standard library. [`analysis/LPU_TOOLS.md`](analysis/LPU_TOOLS.md) lists the tools; the emulator is a fake LPU-1 for trying things without hardware. ## Claude Code skills The repository comes with five skills for [Claude Code](https://claude.com/claude-code), in `.claude/skills/`. Open the repository in Claude Code and they are there. Ask in plain words (for example "what's on my LPU?") or call one by name (`/lpu-show-read`). | Skill | What it does | |---|---| | `lpu-show-read` | Explains a `.show` file: the patch with every DMX channel named, the playbacks and their options, and every cue with its values and FX | | `lpu-show-download` | Downloads the show from an LPU-1 into a `.show` file and/or a readable report, e.g. as a backup | | `lpu-show-upload` | Puts a `.show` onto an LPU-1: a dry run first, then a backup of what's there, the upload, and a read-back check | | `lpu-personality-add` | Adds a fixture to the library from QLC+, Avolites, grandMA or Freestyler files or a PDF manual, and shows you the result before writing it | | `vn-capture-decode` | Turns a capture of LPU-1 traffic (`.pcapng`, or live) into readable lines: which button, fader or write it was | Uploading replaces the whole show on the device, so the upload skill always asks before it sends anything. ## Fixture library PATCH picks fixtures from `dled_flutter/assets/personalities.xml`: per manufacturer the models and their modes, one function code per DMX channel. This repository ships one made-up example (**Generic RGBW Par**, 4 ch and 8 ch). Add your own fixtures: ```bash python dled_flutter/tool/personality_add.py read FOLDER > spec.json python dled_flutter/tool/personality_add.py add spec.json --write ``` It reads QLC+, Avolites, grandMA and Freestyler fixture files. A fuller library of your own can live next to it as `personalities.local.xml` (not in Git): the app, the light page and the tools use it when it is there. ## How DLED was made DLED follows what the LPU-1 does, measured on a real device: the network traffic between an app and the LPU-1 (for the protocol and the `.show` format) and the LPU-1's DMX output for 32 test shows (for the LPD-1's engine, see [`lpd/DMX_ENGINE.md`](lpd/DMX_ENGINE.md)). This is done for interoperability, so DLED and the reference app can be used together on the same device. ## What we tested

Test shows run on a real LPU-1, its Art-Net recorded, the LPD-1 engine replays them and is scored

- **The LPD-1 against a real LPU-1.** 32 test shows were run on an LPU-1, covering cues, fades, follow and go modes, FX, the fader, the programmer, more than one playback, and 64 fixtures. Its DMX output was recorded each time. The LPD-1's engine replays every recording and matches 99.9 to 100 % of the values. The rest is moments where the LPU-1 itself acted oddly. - **Automated tests:** - 248 for the app (protocol, `.show` files, cue records, the CLI against the emulator); - 44 for the LPD-1 (the engine replays and the protocol server); - 23 for the Python library. The commands are under [Contributing](#contributing). - **Against the reference app:** shows saved by DLED open in it. - **Show upload and download on the device:** the upload tools (CLI and skill) read the show back after every upload and compare it. A completely full show (64 playbacks x 48 cues) was written, and a sample of it was read back. - **Stored positions:** stored and recalled on a real LPU-1. The LPD-1 gives the same cue records. - **Not tested:** the Android, iOS and macOS builds of the app. Testing was done with the Windows app and the light page in a browser. ## Contributing Issues and pull requests are welcome. Please run the tests of the part you change (`flutter test`, `python -m platformio test -e native`, `python -m unittest discover -s analysis/tests`). Do not add your own network captures, other people's files or fixture libraries you may not share. (The tests' reference frames in `dled_flutter/test/core_data/` are small decoded samples the tests need.) ## Credits and third-party code **Code that DLED follows:** - **DMX output of the LPD-1** (`firmware/lpd1/lib/dmxout`): it sends DMX the way [WLED](https://github.com/wled/WLED) does in its `wled00/src/dependencies/dmx/SparkFunDMX.cpp`, by SparkFun Electronics and the WLED project, under the MIT licence: - UART2, TX on GPIO 2 to a MAX485; - 250 kbaud 8N2; - the break made by sending a 0 byte at 83 333 baud. Unlike that code, DLED's version doesn't wait for a frame to finish sending. **Libraries and tools, downloaded when you build (not in this repository):** | Used by | What | Licence | |---|---|---| | App | [Flutter](https://flutter.dev) and Dart | BSD-3-Clause | | App | provider, xml, file_picker | MIT | | App | shared_preferences, path_provider, web, wakelock_plus, package_info_plus | BSD-3-Clause | | LPD-1, DLED bridge | [Arduino core for ESP32](https://github.com/espressif/arduino-esp32) | LGPL-2.1 | | DLED bridge | [AsyncTCP and ESPAsyncWebServer](https://github.com/mathieucarbou/ESPAsyncWebServer) (mathieucarbou) | LGPL-3.0 | | LPD-1 tests | [Unity](https://github.com/ThrowTheSwitch/Unity) (through PlatformIO) | MIT | | Capture tools (optional) | [Wireshark / tshark](https://www.wireshark.org) | GPL-2.0 | **In this repository:** the DejaVu Sans fonts in `dled_flutter/assets/fonts/` ([licence](dled_flutter/assets/fonts/LICENSE-DejaVu.txt)). **Made with:** DLED was written with the help of Claude (Anthropic) in Claude Code. The commits say so. ## Licence [GPL-3.0](LICENSE): you may use, study, change and share DLED; changed versions that you pass on must be GPL-3.0 too and come with their source. The DejaVu Sans fonts in `dled_flutter/assets/fonts/` keep their own free licence (`LICENSE-DejaVu.txt` there).