feat(signalk): subscribe WebSocket with sendMeta=all by default#965
Conversation
|
Pushed an additional commit (2da59a9) that adds the consumer-side hook: Without this, the Verified end-to-end: SensESP-based firmware bound to a path with SK-configured zones now sees them ~50ms after subscribe, no HTTP round trip needed. See https://github.com/dirkwa/sensesp-p4-cockpit/blob/jlp-json-layout-player/src/jlp/zone_registry.cpp for a real consumer. |
|
The The The callback fires on the WS task, under the lock, against a freed document
The convention: a path-routed
|
The signalk-server only pushes metadata deltas (units, zones, displayName, displayUnits) over the WebSocket when the client opts in via ?sendMeta=all on the stream URL. Without it, clients that want metadata must do a separate REST GET /signalk/v1/api/.../meta per path — extra round trips, no automatic refresh on metadata changes. Subscribe with sendMeta=all by default so any SensESP-based firmware gets metadata in-stream alongside values. Existing SKValueListener consumers are unaffected: on_receive_updates already iterates only update['values'], so meta-only deltas (which carry an update['meta'] array but no update['values']) are silently ignored by current consumers. Firmware that wants metadata can now parse it directly from the incoming JsonDocument without an additional HTTP round trip. Opt-out via set_send_meta_enabled(false) or the admin UI checkbox for constrained clients that would rather not see the (small) extra traffic. Persisted alongside the other WS settings. Costs are negligible: one meta-delta per subscribed path at subscribe time + on metadata change (typically never).
When the WS subscribes with sendMeta=all, signalk-server pushes metadata in updates[].meta[]. SensESP's on_receive_updates only iterates updates[].values[], so meta entries were silently dropped. Add SKWSClient::on_meta(callback) so consumers can react to meta deltas (units, zones, displayName, displayUnits) without an HTTP /meta round-trip. The callback fires on the WS task; consumers that touch single-threaded subsystems (LVGL, etc.) should marshal back to the event_loop via onDelay(0, ...). Pairs naturally with sendMeta=all (the previous commit) to give consumers a complete in-stream view of SK paths.
Rework the inbound metadata path from a global on_meta() callback into a
path-routed ValueProducer, mirroring how values are handled.
The previous callback fired on the WS task while holding
received_updates_semaphore_, running arbitrary user code inside the
critical section, and passed a JsonObject referencing the transient
receive document, which is freed when on_receive_delta returns (a
use-after-free for any deferred consumer).
Instead:
- Add SKMetadataListener : SKListener, ValueProducer<SKMetaView>. Because
it is-a SKListener, its path is auto-subscribed by subscribe_listeners()
and routed by process_received_updates, same as SKValueListener.
- Emit a typed SKMetaView (displayName, units, description, displayUnits,
zones) with the full received meta object retained in an owned,
refcounted document for lossless access to unmodeled fields. SKMetaView
is the inbound counterpart to the output-only SKMetadata serializer.
- Tag queue entries out of band (ReceivedUpdate{is_meta, doc}) rather than
by JSON shape, since a value can also be an object; route by is_meta plus
a non-virtual-RTTI wants_meta() on SKListener.
- Copy meta entries into owned documents on the WS task (no user code under
the lock); on the main task move the owned doc into a shared_ptr (no extra
deep copy) before emitting.
- Budget value and meta deltas independently so a metadata burst cannot
evict pending values; both caps tunable via SENSESP_MAX_RECEIVED_*_UPDATES.
- Exclude meta deltas from the rx delta count.
|
Thanks for the detailed review — the concurrency and lifetime concerns are spot-on, and I've reworked it accordingly. Pushed a follow-up commit (the branch is also rebased onto 3.4.0). The callback is gone, replaced by a path-routed producer that mirrors the value side:
On the emitted type: I went with a dedicated inbound One thing I want to flag explicitly: for meta I kept Per-kind queue budgets ( |
Add SKListener::parse_meta() as a default-no-op virtual mirroring parse_value(), and have SKWSClient::process_received_updates() call it through the base pointer. This drops the static_cast<SKMetadataListener*> downcast and the signalk_ws_client include of signalk_metadata_listener.h, so the WS client no longer depends on the concrete listener subclass and the "anything overriding wants_meta() must be an SKMetadataListener" contract goes away. Also skip copying meta deltas onto the receive queue unless a registered listener consumes them. With sendMeta=all on by default the server pushes metadata to every client; without a consumer those entries were copied into owned documents only to be dropped. The check reads the listener list under SKListener's semaphore before taking received_updates_semaphore_, so the global lock order (SKListener before received_updates) is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The rework addresses both defects from the last review — verified against the branch:
The producer shape is what I was after, and the On your open question about emit under the lock: keep it symmetric, which is what you already have. I pushed one commit (76143e0) with two small follow-ups so this doesn't need another round-trip:
Compiles clean on One thing worth confirming before this is fully wrapped: the earlier ~50ms end-to-end check was on the callback version, and the WS-task → queue → main-task → emit path is new. A quick run against a live SK server confirming an |
Summary
The signalk-server only pushes metadata deltas (units, zones, displayName, displayUnits) over the WebSocket when the client opts in via
?sendMeta=allon the stream URL. Without it, SensESP-based firmware that wants per-path metadata has to do a separate RESTGET /signalk/v1/api/<dot.path>/metaper path — extra round trips, fragile on flaky links, and no automatic refresh on metadata changes.Subscribe with
sendMeta=allby default so any SensESP-based firmware gets metadata in-stream alongside values.How
SKWSClient::connect_wsappends&sendMeta=allto the stream URL whensend_meta_enabled_is true (default).is_send_meta_enabled()/set_send_meta_enabled()for explicit opt-out.to_json/from_json) and surfaced in the admin UIConfigSchemaas a checkbox.Backwards compatibility
Existing
SKValueListenerconsumers are unaffected.on_receive_updatesalready iterates onlyupdate["values"], so meta-only deltas (which carry anupdate["meta"]array but noupdate["values"]) are silently ignored by current consumers. Firmware that wants metadata can now parse it directly from the incomingJsonDocumentwithout an additional HTTP round trip.Cost
Negligible: one meta-delta per subscribed path at subscribe time, plus on metadata changes (typically never during a session).
Why default-on
Most modern SK clients (KIP, freeboard-sk, instrumentpanel) already do this implicitly via
?sendMeta=all. Without it, every SensESP-based UI/HMI that wants units or zone-based coloring has to reinvent a REST polling path. Default-on is the least-surprise choice.Motivation
Surfaced while building a runtime-loadable HMI player on ESP32-P4 that wanted to color widgets per SignalK alarm-state zones. Realised the zones never showed up in the stream and the firmware had to do extra HTTP
/metapolling. Fix at the SensESP layer benefits every consumer in the same situation.