4.5 KiB · text History 949a2ee

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.

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.