Skip to content
spectalivePublic

About

Touch lighting desk for a QLC+ show, drawn from a qlctool desk map

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

221 Commits

Folders and files

Repository files navigation

dmxdesk

A touch lighting desk for a QLC+ show. It runs on a Linux framebuffer device with a touchscreen, talks to the QLC+ 5 web interface of the machine running the show (the master), and draws its pages, sections and controls from a desk map instead of from the Virtual Console's own layout.

The map is written beside the show by qlctool deskmap (spectalive/qlctool), so any QLC+ workspace that toolkit can describe gets a desk. The desk never trusts the map on its own: it fetches the console the master actually serves (/vc.json), validates the map against it, and only then presses widgets over the WebSocket (/qlcplusWS). A control the console cannot back is shown disabled, with the reason on its tile, rather than dropped.

If the WebSocket briefly drops, the top status bar says "Esperando conexión..." without covering the controls. Touch actions stay disabled until the live console has been read again. A different show loaded on the master still gets the blocking mismatch card.

Priority hits use the source Flash button instead of starting the map's ordinary SingleShot chaser. The QLC+ controller must expose that button's flashMaxHoldMs, flashOverride and flashForceLTP in /vc.json; the desk rejects a hit when the controller cannot guarantee its deadline or priority after a lost WebSocket. The tablet also sends release on touch-up. All Vibra hit sources, including fog, have controller deadlines matched to their burstMs values.

It was written for, and is shipped on, a Denver TAQ-102 tablet (1024x600 LVDS panel, Silead touchscreen) by spectalive/taq102, which cross-compiles it with Buildroot. The defaults in platform/linux-fb/dmxdesk.c (map under /etc/taq102/, settings under /data/, fonts under /usr/share/fonts/taq102) are that appliance's; the map, the master and the input devices can be given on the command line:

dmxdesk [--host H] [--port P] [--map FILE] [--card /dev/dri/cardN]
        [--touch /dev/input/eventN] [--power /dev/input/eventN]
        [--view PAGE,BANK] [--setup]
dmxdesk --find ADDR/PREFIX PORT     # sweep a subnet for a QLC+ master

show/vibra.desk.json is the map of the Vibra show, generated by qlctool deskmap; the tests use it, with the console captured in tests/dmx-desk/fixtures/vc-vibra.json, as the real case. tools/desk-map-version.py <checkout> prints the provenance line (workspace SHA-256, checkout HEAD, map SHA-256) that an image ships beside the map.

Layout

core/               the portable desk, in C: session, /vc.json, the map,
                    layout, painting, fonts, settings, the master search
                    and the loop itself (desk_run)
platform/linux-fb/  the Linux framebuffer device: DRM presentation, evdev
                    touch, power key, backlight, battery and Wi-Fi, and main
dmxdesk.sources     every source the executable links, core and platform
tests/dmx-desk/     host tests, one program per *_test.c; fixtures/ and audit/
show/               the Vibra desk map
fonts/              Inter, the typeface the tests measure text with (OFL-1.1)
tools/              host test runner, dependency fetchers, QLC+ probe, icon
                    and mockup generators
docs/design/        mockups and frames of the desk's design

The loop is the same on every machine. core/desk_run.c turns a struct desk_app (core/desk_app.h: the show, the models, who owns which finger, the settings surface, the session) through a struct desk_platform (core/desk_platform.h): a table of the device calls the desk needs - a clock, a wait, touches in, the power key, the display, the services behind the settings surface, the bar's facts, and the frame out. platform/linux-fb/ fills that table (linux_desk_*.c) and main there only parses arguments, opens the devices and calls desk_run; a second backend fills its own table and reuses the loop unchanged. tests/dmx-desk/desk_run_test.c drives the loop through a scripted platform with no Linux under it.

The console the desk drives sits behind a second table, struct desk_console (core/desk_console.h). The desk sends it commands in its own words (core/desk_command.h: toggle, stop-all, flash, function on or off, grand master, speed dial time and multiplier) and reads back events (core/desk_event.h: a function's state, the grand master, a speed dial's echo). core/qlc_console.c is the QLC+ adapter: it encodes each command as a web-access frame, decodes the master's pushes and steps the session. The snapshot the desk builds its model from is a surface of its own, struct desk_surface (core/desk_surface.h): one item per control under the console's id, with its kind (toggle, flash, stop-all, other button, fader, dial, group), the group holding it, the function it drives and its live state. The QLC+ adapter translates /vc.json into it (core/qlc_surface_of.c), so the map validation, the speed dials and the tiles never see QLC+'s widget tree, and an adapter for another console fills the same surface.

core/osc_console.c is the second adapter: any console driven by plain OSC over UDP (dmxdesk --osc FILE --host H --port P [--osc-listen PORT]). OSC cannot ask a console what it has, so a surface file declares it: the same fields as struct desk_surface, plus the address each control answers to (format in core/osc_map_parse.h). A press is a button going to 1 and back to 0; a flash or a function carries its 1 or 0, the grand master its level as 0..1, a dial its time and multiplier as ints. A console that reports its state back ("feedback": true) is heard on the listen port, from its own host only; one that does not is not heard at all, and has the desk loop its own commands back, so it shows what it asked for. tests/dmx-desk/osc_vibra_test.c writes the Vibra console out as such a file and checks the desk builds the very same model and speed dials from it.

QLC+'s own OSC input was the first real console it drove (2026-10-28, a headless QLC+ 5.2.2 on loopback): buttons, solo frames, flashes and stop-all work as above, with QLC+'s feedback heard. Four things a QLC+ surface file has to say, each found there:

  • a speed dial takes its time as 0..1 of the widget's AbsoluteValue range ("msRange": [0, 2000]), since QLC+ divides an int above 255 down to 0, and arrives in 8-bit steps (504 ms becomes 501);
  • its multiplier is one input per factor ("factorAddresses", mapped to the dial's inputs 6-9 for 1/16x-1/2x, 4 for 1x, 10-13 for 2x-16x);
  • QLC+ 5 sends no runtime feedback for a speed dial, nor for a fader moved by its own input, so those items take "feedback": false and the desk echoes them;
  • a widget on a hidden page of a multipage frame ignores external input (VCPage::inputValueChanged requires it visible), so it must be declared "disabled": true; 17 of the 181 controls Vibra's map names are there, the grand master among them.

tools/qlc-osc-surface writes that file from QLC+'s own /vc.json, with all four rules applied (core/qlc_osc_surface_json.c), so nobody has to type it. It runs on the bench, not on the tablet:

tools/get-cjson.sh                      # once, if tools/vendor/cjson is empty
tools/build-qlc-osc-surface.sh          # builds output/qlc-osc-surface
curl -s http://<master>:9999/vc.json > vc.json
output/qlc-osc-surface --prefix /vibra --grand-master 266 vc.json > vibra.osc.json
output/qlc-osc-surface --prefix /vibra --channels vc.json > vibra.osc.channels
dmxdesk --osc vibra.osc.json --host <master> --port 7700 --osc-listen 9000 --map vibra.desk.json

The surface file alone is half of it: QLC+ must also listen. The workspace needs an OSC input on universe 1 (QLC+ listens on UDP 7700 for universe 1, 7700 + n - 1 in general), and the same line as its feedback patch with feedbackIP set to the desk's address and feedbackPort to --osc-listen (9000 by default for universe 1). Each widget then needs the external inputs the --channels table lists, one line each: <widget id> <input id> <channel> <path>. The channel is the CRC-16 that QLC+'s OSC plugin computes from the path, and --channels checks it against a mapping QLC+ accepted. Input 0 is a button's press, a fader's level or a dial's wheel; a dial's multipliers are its inputs 4 and 6-13. Vibra has 518 such inputs plus 18 multiplier buttons, too many to enter by hand, so the show generator writes them: spectalive/qlctool v0.2.6 and later, given [controllers] osc = "/vibra" in the show description, adds an OSC input universe after the show's own and binds every widget by this same rule. Beside Vibra's one DMX universe that is the second universe, UDP 7701, with feedback to port 9001 by default; pass --port 7701 --osc-listen 9001. The generator leaves feedback out, because its address is the desk's on the night: add the feedback patch on the same line in QLC+'s Inputs/Outputs tab.

Three limits remain. The hidden-page rule takes the frame pages that were showing when /vc.json was served, so take it with the operator's usual pages up; a burst whose priority Flash sits on a hidden page is disabled with it. A burst goes out as a press of its priority Flash widget, which every map spectalive/qlctool generates names (priorityWidget; osc_vibra_test checks all 25 of Vibra's); only a hand-written map without one falls back to DESK_CMD_FUNCTION, which reaches QLC+ only through a functions entry in the surface file, and is otherwise dropped. A dial's time arrives in QLC+'s 8-bit steps.

Building

The desk needs a C99 compiler, libdrm, cJSON 1.7.19 and stb_truetype. Both third-party sources are fetched, not vendored in the repository, at the versions the Buildroot build pins:

tools/get-cjson.sh      # cJSON 1.7.19 into tools/vendor/cjson/
tools/get-stb.sh        # stb_truetype.h at Buildroot 2026.02.3's stb commit

cJSON is compiled into the executable rather than linked as a shared library. On a Linux host with libdrm's headers:

mkdir -p output/include/cjson output/include/stb
cp tools/vendor/cjson/cJSON.h output/include/cjson/
cp tools/vendor/stb/stb_truetype.h output/include/stb/
cc -O2 -w -c tools/vendor/cjson/cJSON.c -o output/cJSON.o
cc -std=gnu99 -Wall -Wextra -Werror -O2 -Icore -Iplatform/linux-fb -Ioutput/include \
    $(pkg-config --cflags libdrm) -o output/dmxdesk $(cat dmxdesk.sources) output/cJSON.o \
    $(pkg-config --libs libdrm) -lm -pthread

The device build is the Buildroot package br2-external/package/dmxdesk in spectalive/taq102, which fetches a tagged release of this repository and compiles the same file list with the same flags.

Tests

The host tests run on macOS, with AddressSanitizer and UndefinedBehaviorSanitizer:

tools/get-cjson.sh && tools/get-stb.sh    # once
tools/test-dmx-desk-host.sh

Each tests/dmx-desk/*_test.c names the sources it links in its // SOURCES: line; the runner builds and runs every one and exits non-zero if any fails. On a Linux host six of them do not pass yet (see TODO.md). Before them it compiles every core/ source with only core/ on the include path (core_without_platform), so a core file that reaches for a platform header fails the run.

Licence

GPL-2.0-or-later, in LICENSE, the terms of the TAQ-102 repository this code was developed in. The Inter fonts are under the SIL Open Font License 1.1 (fonts/LICENSE-Inter.txt); cJSON (MIT) and stb_truetype (public domain or MIT) are fetched from their upstreams and not redistributed here.

About

Touch lighting desk for a QLC+ show, drawn from a qlctool desk map

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages