|====================================| | ArtiMONITOR Definition Version 1.01 | | Written by: Michel van Osenbruggen | | CopyRight 2026 ArtiLED B.V. | |====================================| Latest change : 11-06-2026 =============== | Description | =============== The ArtiLED ArtiMONITOR protocol carries scene/mode running-state from each Node to the Hub. Every Node sends a UDP unicast packet to the Hub on a fixed cadence (heartbeat) and immediately whenever its running state changes. The Hub stores the last-seen state per Node with a TTL so silent Nodes naturally fall out of the live picture. Sibling of ArtiSTATE (port 1099) — same direction (Node → Hub), same unicast pattern, just a different fact type. ======================= | Protocol Definition | ======================= Protocol : UDP unicast Direction : Node -> Hub (target is the configured Hub IP from /opt/artiled/data/hub on each Node) Port : 1096 ========================== | Data Format Definition | ========================== All text : magic|ident:type:name magic : 8-byte string (see Magic Packet Definition) ident : The Node's own ident (text, typically 15 chars, matching its ArtiSTATE / ArtiDISCOVERY ident) type : One of {scene, mode, idle, catalog_scenes, catalog_modes, catalog_scenes_stale, catalog_modes_stale} name : Scene or Mode name (UTF-8). Empty for type=idle. For catalog types: comma-separated list of ALL scene/mode names the Node has (full snapshot, empty = Node has none). Empty for *_stale. Separators : '|' between magic and the rest ':' between ident, type and name =========================== | Magic Packet Definition | =========================== Magic Packet : Mo3reuPh ====================== | Type Definition | ====================== scene : Node is currently running a scene named mode : Node is currently running a mode named idle : Node is currently running nothing (name is empty) catalog_scenes : Full snapshot of the scene names this Node has catalog_modes : Full snapshot of the mode names this Node has catalog_scenes_stale : Scene catalog changed but is too large for one catalog_modes_stale packet — Hub should invalidate its cached copy and wait for its periodic verification sync Catalog messages are always FULL snapshots, never deltas: a lost packet is healed by the next snapshot or by the Hub's hourly verification sync. A Node has at most ONE active entity at a time — scenes and modes are mutually exclusive in practice (every scene script starts with stop_modes_running(); modes and scenes are not designed to overlap). ================ | Cadence | ================ Heartbeat : Every 15 seconds, regardless of state On-change : Immediately on any change to the running entity (start of a scene/mode, end of a scene/mode, transition between two running entities). Catalog : Once at sender startup; within 0.5s when a scene/mode save/delete/rename handler raises the local catalog-changed flag; within 60s for any other catalog change (self-diff). Heartbeats keep the Hub-side TTL fresh so that a silent Node falls out of the live picture automatically. On-change pushes minimise the latency between a real event and Hub visibility. ===================== | Hub-side Storage | ===================== Each received packet sets Redis key: node::running (hash) type scene | mode | idle name scene/mode name (empty for idle) last_seen epoch seconds EXPIRE applied per write 45 seconds (3 x heartbeat interval) When the key expires, the Node has gone silent — the Hub-side API treats this as "running unknown" (NOT "running idle"). Catalog packets set (full replace per packet): node::scenes (set) all scene names the Node has node::modes (set) all mode names the Node has node::scenes_synced (string) epoch seconds of last snapshot node::modes_synced (string) epoch seconds of last snapshot EXPIRE applied per write 25 hours The _synced marker distinguishes "Node has none" (marker present, set absent) from "never reported" (both absent) — consumers fail open when the marker is absent. *_stale messages delete the marker. The Hub's hourly node-list sync writes the same keys, so the two sources are interchangeable. ===================== | Example Packets | ===================== Mo3reuPh|SheCh6feegh9chu:scene:Cinema Mo3reuPh|SheCh6feegh9chu:mode:Disco Mo3reuPh|SheCh6feegh9chu:idle: Mo3reuPh|SheCh6feegh9chu:catalog_scenes:Cinema,Dark,Warm White Mo3reuPh|SheCh6feegh9chu:catalog_modes:Disco,Aurora Mo3reuPh|SheCh6feegh9chu:catalog_scenes_stale: ===================== | Error Handling | ===================== - Packet without the correct magic : Dropped silently - Packet with malformed type field : Dropped (logged when debug) - Packet with unknown ident : Stored anyway — the Hub does not validate ident against MySQL (mirrors ArtiSTATE's behavior) - No source-IP allowlist : Same as ArtiSTATE (LAN perimeter is the security boundary) ===================== | Hub-side daemon | ===================== Daemon : artiled_monitor.py systemd unit : artiled_monitor.service Binds : UDP 0.0.0.0:1096 ===================== | Node-side sender | ===================== Daemon : artiled_sender.py (standalone service) systemd unit : artiled_sender.service Runs on : every device with scenes and/or modes — Hub, Node, Master, Audio, Video, Synthesizer Mechanism : Polls local Redis every 0.5s for scene:*:running / mode:*:running flags (set by the scene.py / mode.py libraries inside running scripts) and sends on-change plus a 15s keepalive. Catalog snapshots are read from scenes//name and modes//name on disk.