Telemetry
The telemetry component pairs a self-contained browser data plotter
web app with an optional firmware-side binary telemetry emitter, so you can
plot live data from a device two complementary ways:
Text / CSV over Web Serial — point the web app at any device that prints columnar data (a header line plus numeric rows). It auto-detects the delimiter and schema, discards non-matching lines (including ESP-IDF log lines), and re-evaluates when a new header arrives. No firmware component is required.
Binary telemetry over WebUSB — the Telemetry class streams typed float channels directly, for higher rate, lower overhead, and device-accurate timestamps. The same web app decodes and plots them.
The hosted app is at
esp-cpp.github.io/espp/apps/telemetry.html (Chromium, secure
context). It plots a high number of points efficiently (uPlot) with drag
zoom, a per-series filter, and CSV save / load, plus optional 2D X–Y and 3D
X–Y–Z modes over the same parsed columns.
Telemetry service
The Telemetry class is a small device→host protocol carried on the espp
stream_frame framing (dispatcher module
id 3), so it can share one USB vendor / CDC stream with other modules via
dispatcher. Firmware declares a fixed set of
named float channels (the SCHEMA) and pushes SAMPLE frames — a
device timestamp plus one float per channel (batchable) — with emit(...).
Host requests are GET_SCHEMA and SET_STREAM (enable/disable + rate).
Construct it with the channel names and a send function, register
handle() on a dispatcher module (or feed
raw bytes to feed()), and call emit() from your producer. emit() and
request handling are safe to call concurrently: every outbound frame is
serialized on an internal send mutex that is held across the send callback,
so frames never interleave and a SAMPLE always carries the channel set it was
built from. Because that mutex is held while send runs, the callback must
not re-enter the emitter (emit(), send_schema(), set_channels(),
handle() / feed()) — keep it to writing the bytes to the transport. See
the example for USB vendor (WebUSB) wiring and capability discovery that lists
the app in the browser Device Hub. (The framing is transport-agnostic — CDC /
UART / a socket work too — but the web app’s binary path consumes WebUSB.)
API Reference
Header File
Classes
-
class Telemetry : public espp::BaseComponent
Binary telemetry emitter for the Serial Plotter web app.
A tiny device->host protocol carried over the espp `stream_frame` framing (so it can share one USB vendor / CDC stream with other modules via `espp::Dispatcher`). Firmware declares a fixed set of named float channels (the SCHEMA) and pushes SAMPLE frames — a device timestamp plus one float per channel — which the hosted `telemetry.html` web app decodes and plots, exactly like the columnar Web-Serial path but binary, higher rate, and with device-accurate timestamps.
This is the purpose-built counterpart to the app’s text (CSV-style) Web-Serial transport: instead of parsing printed columns, the device sends typed samples directly.
## Wire protocol (dispatcher module id 3)
Every message is a `stream_frame` frame with `module == kModule`. The frame `type` byte’s high bit distinguishes direction: host->device requests are `0x0X`, device->host frames are `0x8X` (which sets the frame reply flag).
**GET_SCHEMA** (host->device, no payload): request the current SCHEMA.
**SET_STREAM** (host->device, `[enabled u8][period_ms u16]`): enable or disable streaming and request a sample period (informational — the firmware’s emit cadence is authoritative; the requested period is exposed via period_ms() so an app can honor it). Answered with OK.
**SCHEMA** (device->host): `[version u8][flags u8][nchannels u8]` then, per channel, `[type u8][name_len u8][name bytes]`. `version == kSchemaVersion`, `flags == 0` (reserved), channel `type == 0` (f32). Sent on GET_SCHEMA, on set_channels(), and on demand via send_schema().
**SAMPLE** (device->host): one or more packed records, each `[timestamp u32 microseconds][f32 × nchannels]` little-endian. A frame may batch several records (payload size is an exact multiple of the record size) for higher throughput.
**OK** (device->host, `[request_type u8]`): acknowledges a request.
**ERROR** (device->host, `[request_type u8][code u32][utf8 message]`).
## Threading
emit() is typically called from a producer task while requests are handled on a transport RX task; both are safe to call concurrently. Every outbound frame is serialized on an internal send mutex that is held across the user `send` callback, so the callback (a) never runs concurrently with itself — a `send` that is not itself thread-safe cannot interleave the bytes of two frames — and (b) always delivers a SAMPLE together with the channel set it was built from, so the host never decodes a sample against a stale SCHEMA.
Because that send mutex is held while `send` runs, the callback MUST NOT re-enter this object — do not call emit(), send_schema(), set_channels(), handle() or feed() from inside `send`, or it will deadlock on the same (non-recursive) mutex. Keep `send` to writing the bytes to the transport.
Public Types
-
enum class Type : uint8_t
Frame `type` values within the telemetry module.
Values:
-
enumerator GetSchema
request the current SCHEMA
-
enumerator SetStream
[enabled u8][period_ms u16]: enable/disable + rate
-
enumerator Schema
channel schema (see class docs)
-
enumerator Sample
one or more [timestamp u32 us][f32 x nchannels] records
-
enumerator Ok
[request_type u8]: request acknowledged
-
enumerator Error
[request_type u8][code u32][utf8 message]
-
enumerator GetSchema
-
enum class ChannelType : uint8_t
Channel value type (only 32-bit float today; reserved for future widening).
Values:
-
enumerator F32
-
enumerator F32
-
using send_fn = std::function<void(std::span<const uint8_t> frame)>
Function used to transmit one encoded frame to the host.
- Param frame:
The complete encoded frame bytes (header + payload + CRC).
Public Functions
-
inline explicit Telemetry(const Config &config)
Construct the emitter.
- Parameters:
config – Channel names, the (optional) send function, and defaults.
-
inline void emit(std::span<const float> values, uint32_t timestamp_us)
Push one sample with an explicit device timestamp.
Note
No-op while streaming is disabled or no send function is configured.
- Parameters:
values – One value per channel, in schema order (size must match the channel count, else the sample is dropped with a rate-limited warning).
timestamp_us – Device timestamp in microseconds (u32; wraps ~71 min).
-
inline void emit(std::span<const float> values)
Push one sample timestamped with the current device time.
-
inline void set_channels(std::vector<std::string> channels)
Redefine the channel set at runtime and send a fresh SCHEMA.
Note
The channel-set update and the outbound SCHEMA are performed together under send_mutex_, so a concurrent emit() cannot deliver a sample built from the new channels before (or the old channels after) the matching SCHEMA reaches the host.
-
inline std::vector<std::string> channels() const
The current channel names (schema order).
-
inline bool streaming() const
Whether streaming is currently enabled.
-
inline void set_streaming(bool on)
Enable or disable streaming (SAMPLE emission).
-
inline uint16_t period_ms() const
The host-requested sample period in milliseconds (informational).
-
inline void send_schema() const
Send the current SCHEMA frame now (device->host).
Note
Uses the same send_mutex_-outer / mutex_-inner order as emit() so the SCHEMA and any concurrent SAMPLE stay consistently ordered.
-
inline void handle(const espp::stream_frame::Frame &frame)
Dispatcher handler: process one frame addressed to this module.
Register with `dispatcher.register_module(Telemetry::kModule, …)`. Ignores reply-flagged frames (device->host pushes are never host requests).
-
inline void feed(std::span<const uint8_t> data)
Feed raw transport bytes through an internal frame parser (for use without a Dispatcher). Processes every complete frame for this module.
-
inline void reset_parser()
Discard any partially-buffered bytes in the internal parser (e.g. on transport reconnect). Only relevant when using feed().
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
Public Static Attributes
-
static constexpr uint8_t kModule = 3
Dispatcher module id owned by the telemetry protocol (the frame `module` byte). Device->host Type values keep the high bit set, which the framing maps to the reply flag.
-
static constexpr uint8_t kSchemaVersion = 1
Version byte at the head of a SCHEMA payload, so the wire format can evolve.
-
static constexpr size_t kMaxChannels = 255
Maximum channel count (the SCHEMA encodes it as a u8).