Files
Henrik Lemke a82b38d725 Web front-end (PWA) prototype: eco/web, not wired into eco yet
A fourth front-end next to shell, desktop and lab/voila: one gateway
process in the session (tornado, own thread, 127.0.0.1 + token), many
thin browser clients. Values via live_values.LiveReader and writes via
start_write, as in the Qt and ipywidgets grids; writes off by default.
Namespace list with lazy init, device grid, SVG panels, reconnect,
installable as an app. Nothing imports eco.web; start it with
eco.web.start_web() or try it with python -m eco.web --demo.
Plan, protocol and status in web_frontend/.
2026-10-30 18:10:44 +01:00

4.3 KiB

Gateway ↔ client protocol (v0)

Implemented by eco/web/client_session.py (server) and eco/web/static/app.js (client). Rows marked planned are not implemented yet.

JSON over one websocket per client (/ws?token=…), plus a few plain HTTP GETs for static files and SVGs. Versioned from day one so client and gateway can be updated independently (a phone may keep a cached old client).

Envelope

{"v": 0, "id": 17, "op": "subscribe", "args": {...}}      // client → gateway
{"v": 0, "re": 17, "ok": true, "data": {...}}             // reply to id 17
{"v": 0, "ev": "values", "data": {...}}                   // unsolicited event

id is chosen by the client; every request gets exactly one reply (ok:false with error: {type, message} on failure — e.g. MoveBlocked, AccessDenied, NotInitialized, WritesDisabled).

Item addressing

Items are addressed by dotted eco name (alias.get_full_name(), e.g. "mono.energy"). Every row a describe returned is remembered by the session under that name, and subscribe/set/... only accept such names. describe also accepts a top-level component or a public attribute path below one ("vacuum.valve1", nothing starting with _), so deep links survive a reload. The client never sends expressions.

Operations

op args reply / effect
hello {client_version, client_id} {protocol, label, mode: "session", writes: bool, flush_interval}
ping {} {} — the client sends it every 15 s; no reply in 8 s means the connection is dead
names {} {initialized:[…], lazy:[…], failed:{name: reason}}
init {name} planned — today describe of a lazy name initializes it
describe {name} `{name, title, kind, svg, writable, doc?, rows:[{name, label, kind: "assembly"
subscribe {names:[…]} added to this client's LiveReader; values arrive as ev: "values"
unsubscribe {names:[…]}
set {name, value} (mode B, writes on) start_write; state via ev: "write" (moving/done/error)
step {name, sign, step} relative move (▲/▼)
stop {name} stop_move; always allowed when writes are on
step_size {name} {step} — the start value of the ▲/▼ step (the grids' initial_step)
trigger {name} (writes on) runs an AdjustableTrigger
— — SVG panels are plain HTTP: GET /api/svg?name=… returns the SVG, header X-Eco-Prefix is what click markers are relative to; the client re-fetches every 3 s
queues planned {} queue list with tasks (mode B)
queue_call planned `{queue, action: "pause" "resume"
queue_follow planned {queue, from_task?} backlog + live ev: "queue_output"
tweak planned {session_id, action, args} TweakController methods by allow-list

Events

{"ev": "values", "now": 1761390000.2, "data": {"mono.energy": {"text": "7.1", "num": 7.1, "t": 1761390000.12, "state": "live"|"subscribed"|"polled"}}}
{"ev": "values", "now": ..., "data": {"mono.gauge": {"error": "no value (not connected?)", "state": "error", "t": ...}}}
{"ev": "write",  "data": {"name": "...", "state": "busy"|"moving"|"idle"|"error", "message": "..."}}
{"ev": "queue_output", ...}   // planned

Value fields: text always; num for a finite real, bool, enum (labels), cls "green"/"red" for open/closed. Values are coalesced per item and flushed at most every 100 ms per client (latest value wins), so a 100 Hz channel cannot flood a phone over a tunnel. t is the read time on the gateway; the client shows age from it (clock skew: the gateway sends its own now in every values batch).

Value encoding: numbers/str/bool as JSON; numpy scalars converted; arrays only on explicit array subscriptions (later, binary frames); anything else format_value() from live_values as a string.

Not in v0

Binary frames (camera images, arrays), user input routing, multi-user presence, saved layouts on the server.