Client socket
LambentExWeb.UserSocket at /socket — Phoenix channels for native clients,
carrying the same data and the same controls as the LiveView UI.
ws://<host>:<port>/socket/websocket?vsn=2.0.0
No auth, no origin check, longpoll off. Same posture as the web UI: a LAN appliance with no user model. Don’t put it on the internet.
The pairing QR in the web UI (the ⬛ icon, top right of any page) encodes the base URL to point a client at.
Shape
Four channels, one per section of the UI. Each subscribes to the pubsub topics its LiveView subscribes to and pushes them on, so a client is a reducer over pushes exactly like the LiveView is:
| topic | pushes | rate |
|---|---|---|
devices |
devices, probe_result |
2s (7777 scan), 400ms (MQTT) |
machines |
machine, preview |
400ms, ≤4Hz |
links |
link |
400ms |
services |
services, service |
on change |
Payloads are the publish maps with the Elixir-only bits flattened by
LambentExWeb.Channels.Payload: IP tuples become dotted strings, keyword
option lists become objects, atoms become strings.
Every command replies {:ok, %{}} or {:error, %{reason | errors}}. Commands
are casts underneath, so an :ok means accepted, not applied — watch the next
push for the result.
devices
devices pushes %{devices: %{mac => device}} — a snapshot on join, deltas
after. Merge by MAC; a delta only carries what just published. MQTT devices
are absent from the join snapshot and arrive within 400ms.
{"devices": {"8c:fd:49:0f:da:dc": {
"ip": "172.16.15.140", "sox": 2, "ind": 7776, "type": "7777-flex",
"name": "S5", "place": "Cubby", "ord": "grb"}}}
sox is the strand count (0 on a legacy 8266, which still means one strand);
ind is the indicator LED’s UDP port, or absent/null when the device has none.
type is 7777-8266, 7777-flex, or mqtt-tasmota, and decides which of
those controls apply.
| command | params | notes |
|---|---|---|
rename |
id, name |
empty string clears |
replace |
id, place |
empty string clears |
reorder |
id, ord |
rgb/grb/rgbww/… ; 7777 devices only |
poke |
id, mode |
auto (default), indicator, strands |
probe |
ip |
manual find-by-IP; errors on a bad address |
poke in auto mode uses the indicator LED when the device has one and the
strands otherwise. probe answers asynchronously with probe_result:
%{ip:, status: "lambent" | "not_lambent" | "no_iface", sox:}.
machines
machine pushes one machine’s state keyed by id. library is pushed once
on join — %{library: %{class => %{entry => spec}}} — so a client can listen
rather than ask. Join with
%{"previews": false} to skip the preview firehose (32 pixels per machine,
up to 4Hz) if you’re not drawing it.
| command | params | notes |
|---|---|---|
faster / slower |
id |
one notch on the speed ladder |
brighter / dimmer |
id |
moves the target; bgt ramps toward bgtt |
persist |
id |
toggles survival across restarts |
quit |
id |
stops the machine |
library |
— | re-sends the library push (also replies with it) |
library_add |
cls, entry |
starts a library entry |
create |
machine |
same params the web form posts |
id is the machine’s name, not its id field — that’s what the registry is
keyed by.
links
link pushes one link’s state keyed by name, including chan ("all" or an
index) and enabled. Links on the same device only pause each other when their
channels overlap.
| command | params | notes |
|---|---|---|
toggle |
id |
enable/disable; enabling pauses overlapping links |
persist |
id |
|
quit |
id |
|
create |
link |
name, source_id, target_id, optional chan |
create_bulk |
link |
as above with target_ids as a list |
chan is "all" or "0", "1", … . It is optional, defaults to all, and is
forced to 0 for any target with a single channel — legacy 8266s and Tasmota
included. Out-of-range indices fall back to 0.
services
services pushes %{kind: "http" | "mqtt" | "cronos", services: [%{name:, entry:}]}
on join and on change. delete takes kind and id. Creating a service isn’t offered —
the web UI’s three service forms are still stubs.
Trying it
# iex -S mix phx.server, in another shell
{:ok, _} = Application.ensure_all_started(:lambent_ex)
Or drive the channels directly in a test, the way
test/lambent_ex_web/channels/channels_test.exs does — no transport needed.