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]

enum class ChannelType : uint8_t

Channel value type (only 32-bit float today; reserved for future widening).

Values:

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 set_send(send_fn fn)

Set (or replace) the transmit function, e.g. after USB init.

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

Returns:

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

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

set_log_level

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

get_log_level

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

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).

struct Config

Configuration for the Telemetry emitter.

Public Members

std::vector<std::string> channels

Channel names, in sample order (>= 1).

send_fn send = {nullptr}

Transmits an encoded frame (may be set later).

bool stream_on_start = {true}

Start with streaming enabled.

uint16_t period_ms = {20}

Default requested sample period (informational).

espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}

Logger verbosity.