Endpoint telemetry and terminal monitoring
Yoke monitoring is optional and read-only. Pairing, reconciliation and media keep operating when the monitor, Prometheus or Thermite is absent. The same Python model feeds a terminal dashboard, a JSON API and a Prometheus exporter. No broker, central registry, cloud account or third-party runtime package is required. ADR 0020 records the boundary.
Research and choice
Research checked against primary sources on 2026-11-02:
| Approach | Decision for Yoke |
|---|---|
| Prometheus HTTP pull | Implement. Thermite already supports it; an ordinary scrape also reports whether the exporter itself is reachable. Cache the rendered response between samples. |
| Versioned JSON snapshot | Implement alongside Prometheus. A UI or third-party manager can read structured endpoint details without parsing time-series text or importing Yoke. |
| OpenTelemetry / OTLP | Keep as an external adapter option. Its richer data model supports cumulative/delta streams, but adding an SDK/exporter or collector is unnecessary for this small stdlib-only device reference. |
| Pushgateway | Do not require it for continuously running devices. Its series lifecycle and failure behavior add management work that ordinary pull avoids. |
| Extra multicast telemetry frames | Do not add them. Existing authenticated announcements/events already describe endpoint state; resource samples must not consume YWP/1's routing-description budget. |
Prometheus documents its text exposition format, metric units and bounded labels, and Pushgateway lifecycle limitations. The OpenTelemetry data model informs the cumulative-counter choice. These sources support the transport and representation decisions; the Yoke schema and implementation are original.
The TUI uses Python's optional curses module. Its resource panels, histories and endpoint table are inspired by btop; no btop code or dependency is used. Sampling is separate from drawing, and terminal updates are batched.
Run locally
# Hardware-free, network-free dashboard preview.
yoke top --demo --once
yoke top --demo
# Observe a control VLAN and sample this collector's Linux host.
yoke --interface 192.0.2.10 --psk-file /etc/yoke/psk top
# Explicitly start the optional loopback-only HTTP exporter.
yoke --interface 192.0.2.10 --psk-file /etc/yoke/psk metrics
# Reuse an exporter, avoiding another multicast observer and host sampler.
yoke top --url http://127.0.0.1:9754
# Scriptable snapshot; stdout contains JSON only.
yoke metrics --once --format json
The default sample interval is one second; --interval accepts 0.2–60
seconds. One-shot live collection waits 2.5 seconds by default, configurable
with --window. top --once --width 100 --height 30 produces plain text
without terminal escape codes. Interactive mode needs a terminal with curses.
Keys: arrows or j/k select an endpoint; s changes sort; Space pauses
the display while collection continues; ? shows help; q or Escape quits.
Resize adapts the layout. Source failures are displayed explicitly rather
than presenting cached data as a fresh live observation.
--demo is synthetic and labeled DEMO. It reads no device sockets or host
resource files. Current YWP/1 endpoints are USB and HDMI. IR routing and its
device-specific counters are not implemented; the monitor does not invent
an IR device or report unavailable measurements as zero.
Collection scope
One passive socket receives the existing control multicast group. It never
sends application datagrams, instantiates a driver or joins a media group.
Normal kernel IGMP membership traffic still occurs. With a site key, the
same authentication and replay checks as yoke snoop govern observations.
Open mode remains explicitly unauthenticated. Invalid packets contribute
only to aggregate verdict counters, not spoofed per-device series.
Endpoint liveness means an accepted announcement within 6.5 seconds. It is
separate from the last announced idle, pending, active or error
state. Backend active does not prove physical output or payload flow.
Observation counts are cumulative only while that device is retained by
this collector; restart or eviction resets them. Lost packets and events
before observer startup cannot be recovered as lifetime totals. Devices
are forgotten after the existing five-minute observation retention period,
and storage is capped by the existing 4,096-node bound.
Host panels always say COLLECTOR HOST. They describe the machine running
the monitor, including when that machine is a warden for a remote appliance.
--local-node usb-desk records an explicit association; it does not prove
that an appliance's CPU or temperature was measured. Onboard Linux devices
can run their own exporter for host resources. Existing node_exporter is
preferable when comprehensive OS/process telemetry is required.
The small host sampler reads bounded /proc and /sys files for CPU,
memory, swap, interface counters and thermal zones. The first CPU/network
rate, missing files and counter resets produce unknown values. Network
bytes are host NIC traffic, while device control bytes are accepted
YWP/1 traffic; neither is an HDMI or USB media-throughput measurement.
Export contract
Explicit yoke metrics invocation creates these HTTP routes:
| Route | Representation |
|---|---|
/metrics |
Prometheus text 0.0.4, UTF-8, HELP/TYPE, cumulative counters and gauges |
/v1/snapshot |
JSON with schema_version: 1, uptime_seconds, authentication, devices, observer, and nullable host |
Both responses are serialized together once per sample and served from an
immutable cache. Scraping performs no host reads, device queries or event
reprocessing. There is no command/upload API. Requests and client response
sizes are bounded. JSON unknown fields are ignored for compatibility;
invalid types, nonfinite values, duplicate keys and unsupported schema
versions are rejected by the supplied Python client. The snapshot's
sample_interval_seconds advertises its cadence; a remote display detects
non-advancing samples using that interval, independently of HTTP success.
Older snapshots without the field use a conservative 60-second cadence.
The client bounds network transport work with one deadline; DNS resolution
follows the operating system's resolver policy.
| Metric | Meaning |
|---|---|
yoke_device_info{node_id,kind,role} |
Stable endpoint identity dimensions |
yoke_device_up{node_id} |
Recent accepted announcement, independently of backend state |
yoke_device_state{node_id,state} |
Four fixed state gauges for the last announcement |
yoke_device_last_seen_age_seconds{node_id} |
Monotonic announcement age at sample time |
yoke_device_control_frames_total{node_id} |
Accepted observed frames from an announced endpoint |
yoke_device_control_bytes_total{node_id} |
Bytes in those control datagrams, not media |
yoke_device_events_total{node_id,event} |
Observed events with fixed vocabulary and an other bucket |
yoke_observer_datagrams_total{verdict} |
Aggregate accepted/rejected observation counters |
yoke_observer_uptime_seconds |
This collection process's elapsed time |
yoke_host_* |
Available collector-host CPU, memory, NIC and temperature measurements |
Names, error text, pairing targets, versions and operation IDs are available
in JSON where applicable; they are never metric labels. There are no
per-packet time series or exported payload bytes. Unknown gauges are omitted
from Prometheus and represented as null in JSON. Use rate(..._total[5m])
to derive rates; don't infer packet loss from gaps in YWP sequence numbers,
which include intentionally skipped durable reservations.
Thermite and third-party integration
A single collector on a control VLAN can export observed state for all its
endpoints without adding new endpoint transmissions. Thermite optionally
scrapes that collector using metrics.yoke_targets in its host configuration:
metrics:
yoke_targets:
- "127.0.0.1:9754"
Thermite uses its existing 30-second scrape interval and generates an
optional Yoke dashboard. The exporter still samples once per second for a
smooth local/remote TUI. Monitoring does not become a home-control readiness
requirement. Other Prometheus users can configure an ordinary static target
with path /metrics; other applications can request /v1/snapshot or use
yoke.telemetry.client.SnapshotClient.
HTTP defaults to 127.0.0.1:9754. To expose metadata on an explicit trusted
management address, use --listen 192.0.2.10 --expose. The endpoint has no
authentication; restrict it to the intended monitoring network or place it
behind an operator-managed authenticated TLS proxy. The client verifies HTTPS
normally and does not follow redirects. Never use the control PSK as an HTTP
credential. Run the exporter under your chosen service supervisor for
unattended use.
Reuse and verification
telemetry.model owns the versioned snapshot; collector folds accepted
control observations; host samples OS counters; prometheus renders the
standard representation; http caches/serves it; client validates remote
JSON; runtime owns the passive worker; tui renders and handles keys.
demo supplies deterministic hardware-free samples. telemetry.cli only
adapts command arguments. The control core and endpoint runner are unchanged.
Run make verify for deterministic parsing, sampling, rendering, loopback
HTTP/multicast and CLI tests. Firmware crypto tests may require the separate
pinned SDK gate described in ESP32 updates. Software
performance measurements must distinguish sample/serialization work from a
cached HTTP read; they do not establish Radxa or ESP32 resource overhead.
Software measurements
On 2026-11-02, CPython 3.14.9 on x86-64 Linux, 20 repeats of a synthetic healthy fleet (one accepted USB announcement per device, no events or host measurements in the fleet encoding workload):
| Retained devices | Sample, validate and encode median | JSON bytes | Prometheus bytes |
|---|---|---|---|
| 32 | 0.497 ms | 7,341 | 16,984 |
| 256 | 3.702 ms | 57,606 | 127,701 |
The cached-representation pointer read measured about 0.056 microseconds;
this excludes HTTP, TLS and network costs. Separate local /proc//sys
sampling measured about 0.45 ms median over 100 samples. These are workstation
software measurements, not hardware endpoint budgets or throughput results.
Reproduce fleet measurements with:
python benchmarks/telemetry.py --nodes 32 --repeat 20
python benchmarks/telemetry.py --nodes 256 --repeat 20
# Or: make benchmark-telemetry NODES=32 REPEAT=20
There is an existing scaling limit: Snooper scans retained caches during
packet admission. At the full 4,096-node bound, per-datagram observation cost
is materially higher than for a small fleet; cached exposition does not
remove that cost. Measure large fleets and event storms on their intended
collector host before choosing a capacity. The upper bound is a memory and
validation limit, not a promised small-board operating point. This pass
preserves the existing snooper's expiry behavior rather than changing the
protocol observer to make a benchmark faster.