LEDit Device Client
A small Python package for Raspberry Pi Zero (or any Pi) devices driving an RGB LED matrix (HUB75 panels). It connects out to your LEDit server over WebSocket, pulls frames, and renders them onto the panel.
Because the device pulls from the server, there is no inbound port, no static IP, and no credentials beyond a per-device token. Updating the server requires no changes to the device — new features appear automatically on the next frame.
How it works
- The server renders each source (F1, weather, calendar, news, stocks, …) to a PNG at the device's configured width × height.
- Frames stream to the device at
ws://<server>/ws/device/<token>. - The client decodes each frame and pushes it to the matrix.
- The cycle interval (how long each source shows) is configured per device
on the server (
refresh_interval, default 60 seconds).
Requirements
- Python 3.8+
- rpi-rgb-led-matrix (C++ library + Python bindings, installed separately)
Pillow,websocket-client, and the OpenTelemetry packages (installed automatically by pip)
Install on a Pi Zero
# System packages
sudo apt update && sudo apt install -y python3-pip python3-pil git
# rpi-rgb-led-matrix (build Python bindings)
git clone https://github.com/hzeller/rpi-rgb-led-matrix.git
cd rpi-rgb-led-matrix
make build-python PYTHON=$(which python3)
sudo make install-python PYTHON=$(which python3)
# This package (published to PyPI on every LEDit release)
pip3 install ledit
Or from a checkout of this repo:
pip3 install ./device
For development (editable install):
pip3 install -e .
Configuration
All configuration is via environment variables:
| Variable | Default | Purpose |
|---|---|---|
LEDIT_SERVER |
ws://localhost:8080 |
WebSocket URL of the server |
LEDIT_TOKEN |
(required) | Device token (admin → Devices); may be omitted after auto-provisioning (persisted to ~/.config/ledit/token) |
LEDIT_UPDATE_INTERVAL |
3600 |
Firmware OTA poll interval (seconds); 0 disables |
LEDIT_UPDATE_CHANNEL |
(empty) | OTA channel (empty = server default channel) |
LEDIT_COLS |
64 |
Panel width |
LEDIT_ROWS |
64 |
Panel height |
LEDIT_CHAIN |
1 |
Chained panels |
LEDIT_PARALLEL |
1 |
Parallel chains |
LEDIT_HARDWARE_MAPPING |
regular |
rpi-rgb-led-matrix mapping |
LEDIT_BRIGHTNESS |
80 |
Startup brightness, 0–100 (live hint overrides) |
LEDIT_GPIO_SLOWDOWN |
1 |
Set >1 on Pi 4 / fast boards |
LEDIT_PREVIEW_DIR |
(unset) | Save frames as PNGs (no hardware) |
LEDIT_SPECTRUM |
0 |
Opt in to the audio spectrum tap (1/true) |
LEDIT_BUTTON_SHORT_MS |
500 |
Nominal short-press window (ms) |
LEDIT_BUTTON_LONG_MS |
800 |
Hold threshold; press ≥ this emits hold (ms) |
LEDIT_BUTTON_HOLD_REPEAT_MS |
0 |
Repeat hold every N ms while held (0 = once) |
Protocol v2 (brightness, spectrum, buttons)
The client connects with ?protocol=2. Servers that understand it reply with a
{"type":"welcome","protocol":2,"capabilities":["brightness","spectrum","hold"]}
message; if no welcome arrives the client stays in v1 mode with no brightness
hints and no spectrum. All v2 fields are optional and additive — old servers
and old wscat clients keep working unchanged.
- Brightness: frames may carry a
brightnessinteger (0–100). When present and in range the client applies it to the runningrpi-rgb-led-matrixinstance live, without recreating the matrix. Until the first hint theLEDIT_BRIGHTNESSstartup value is used. Absent or out-of-range values leave brightness unchanged. - Spectrum (opt-in, default off): with
LEDIT_SPECTRUM=1, when the server advertisesspectrumand the current frame source is the audio visualizer (audio:visualizer, or the built-in display nameAudio Visualizer), the client captures microphone audio best-effort and sends{"type":"spectrum","bins":[...]}(16 bins, 0–255) at ~20 Hz. No microphone or optional audio library simply means no spectrum is sent — never a crash. Requiresnumpy;sounddeviceis used opportunistically when installed. - Buttons: a short press (released before
LEDIT_BUTTON_LONG_MS) sends the existing{"action":"next"}/{"action":"pause"}on release. A press held to or beyondLEDIT_BUTTON_LONG_MSsends{"action":"hold"}, optionally repeating everyLEDIT_BUTTON_HOLD_REPEAT_MSwhile held. Debounce is preserved. - v1 compatibility: a v1 server (no welcome) or a v1 device (no
protocolparam) degrades to v1 behaviour. Frames never change key names or the PNG format.
OpenTelemetry
The device exports traces, metrics, and logs to an OTLP-compatible backend
(the same way the LEDit server does). Everything is off by default — if
OTEL_EXPORTER_OTLP_ENDPOINT is not set the client runs exactly as before,
with no telemetry overhead.
| Variable | Default | Purpose |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
(unset) | OTLP collector endpoint; unset disables telemetry |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc |
grpc or http/protobuf |
OTEL_SERVICE_NAME |
ledit-device |
Service name attached to exported telemetry |
OTEL_RESOURCE_ATTRIBUTES |
(unset) | Extra resource attributes (e.g. rack=42,zone=west) |
OTEL_TRACES_SAMPLER |
(default) | always_on, always_off, traceidratio, parentbased_* |
Spans cover the WebSocket lifecycle (message received, image/text render,
connection errors) and metrics include device.frames_rendered_total,
device.connection_errors_total, and device.reconnects_total. Device logs
are forwarded to the OTLP backend with trace-context correlation.
Example with a local collector:
LEDIT_TOKEN=<token> OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 ledit-device
Getting the token
- Open the LEDit admin UI → Devices.
- Create a device (name + matrix size + refresh interval).
- Copy the generated token (and full connection URL) from the table.
Discovery and auto-provisioning
Unprovisioned devices can advertise themselves via mDNS and be enrolled from the server without manually copying a token.
- Advertisement: DNS-SD service
_ledit._tcp.localwith TXT recordsid(stable fingerprint),model,version,proto,nonce. The token is never advertised. - Fingerprint:
fingerprint()reads/etc/machine-idwhen available, otherwise a random ID persisted at~/.config/ledit/device_id. Stable across reboots. - Nonce:
new_nonce()generates a fresh value per boot and is included in the TXT records. - Optional dependency:
zeroconfis required only for discovery. Install withpip install 'ledit[discovery]'. If missing, advertising/provisioning is skipped with a warning and manualLEDIT_TOKENmode is unaffected. - API:
discovery.start_advertising()/discovery.stop_advertising()anddiscovery.provision(server_url, fingerprint, nonce, interval, timeout)which pollsGET /api/device/provision?fingerprint=…&nonce=….
Enabling flow:
- Start the device without
LEDIT_TOKEN(with the discovery extra installed). It begins advertising. - In the server admin UI go to Admin → Discovery — the device appears as pending.
- Enroll it. The server binds the fingerprint+nonce to a token.
- The device polls
GET /api/device/provisionuntil the token is returned (once), persists it to~/.config/ledit/token(configurable viaLEDIT_CONFIG_DIR), and then connects to/ws/device/<token>. Subsequent boots use the persisted token andLEDIT_TOKENmay be omitted.
Firmware OTA
firmware.check_and_update(server_url, token, current_version, channel) polls the server manifest, downloads the artifact, verifies sha256, and stages the update atomically.
- Polls
GET /api/device/firmware?version=<current>&channel=<channel>(channel fromLEDIT_UPDATE_CHANNEL). - Downloads from the manifest
url(or/api/device/firmware/<version>/artifact), verifiessha256(andsizewhen provided). - Stages to
~/.config/ledit/staging/(orLEDIT_STAGING_DIR) asfirmware-<version>.binwith anactivatemarker; the running process is never overwritten. A failed or interrupted update leaves the previous version bootable. - Non-fatal on network/parse errors — logs a warning and returns.
- Polling interval is
LEDIT_UPDATE_INTERVAL(default 3600 s); set0to disable.
Inbound webhook signing
When a signing secret is configured in Admin → Webhook settings, inbound webhook requests must be signed. This is separate from LEDit's outbound webhooks (which sign the body only).
- Headers:
X-LEDit-Timestamp: <unix seconds>X-LEDit-Signature: sha256=<hex>where hex isHMAC-SHA256(secret, "<timestamp>.<raw-body>")— the timestamp string, a literal., and the raw request body.
- Verification: missing, stale (>300 s, configurable via
signing_window_seconds), or mismatched signatures get a generic401. - When no signing secret is set, the legacy
X-API-Key/?token=auth is unchanged. If both a signing secret and an API key/token are configured, both are required.
Run
Installed as a package, run the console script:
LEDIT_SERVER=ws://ledit.local:8080 LEDIT_TOKEN=<token> ledit-device
Or without installing (from the device/ directory):
LEDIT_SERVER=ws://ledit.local:8080 LEDIT_TOKEN=<token> python3 -m ledit_device
The client reconnects automatically on network drops.
Test without hardware
LEDIT_SERVER=ws://localhost:8080 LEDIT_TOKEN=<token> \
LEDIT_PREVIEW_DIR=/tmp/ledit_frames python3 -m ledit_device
This writes each received frame as a PNG into LEDIT_PREVIEW_DIR.
Package layout
device/
pyproject.toml # package metadata + console script
ledit_device/
__init__.py # version + public exports
__main__.py # entry point (python -m ledit_device)
config.py # env-var config + logging
display.py # MatrixDisplay / FileDisplay abstractions
client.py # WebSocket frame handling + rendering
telemetry.py # OpenTelemetry init/shutdown (traces, metrics, logs)
tests/
test_client.py # unit tests (no hardware required)
test_telemetry.py # telemetry unit tests
Running tests
The unit tests use a FileDisplay (writes PNGs to a temp dir), so they run
without a panel or the rgbmatrix bindings installed:
cd device
python3 -m unittest discover -s tests -v
Release files for ledit 1.45.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ledit-1.45.0.tar.gz | 32.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ledit-1.45.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 57.0 kB
Release files / ledit-1.45.0.tar.gz
| Download URL | ledit-1.45.0.tar.gz |
|---|---|
| Size | 32.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d565a5be9c8a6f45cf218d84362126f51981edc0a7f30bcc9e3a3a7638e5bda9
|
|
BLAKE2b-256 checksum How to use checksums |
63046b26088702cce3ba97a2a950cba7103ebaa33ee841c04dde815730eb8412
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / ledit-1.45.0-py3-none-any.whl
| Download URL | ledit-1.45.0-py3-none-any.whl |
|---|---|
| Size | 24.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
459d5137870c9ee93d3b342fb6a39c6b60ace4fbb6363230463033b3b1188203
|
|
BLAKE2b-256 checksum How to use checksums |
d81fcc9ebbaef8c0be855396a7303a979cc790dc7db2d26ac1ba696ef8b48ba3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|