ESPectre SDK 2.8.0-280-gac7af68
Wi-Fi CSI motion sensing for ESP32 firmware
Loading...
Searching...
No Matches
espectre_protocol.h
Go to the documentation of this file.
1/*
2 * ESPectre - ESPectre Protocol
3 *
4 * Shared device, command, and OTA protocol types used by frontend
5 * transports.
6 *
7 * Author: Francesco Pace <francesco.pace@gmail.com>
8 * SPDX-License-Identifier: GPL-3.0-only
9 * Commercial licensing available under separate agreement; see LICENSING.md.
10 */
11#pragma once
12
13#include <cstddef>
14#include <cstdint>
15#include <string>
16
17#include "runtime_snapshot.h"
18
19/**
20 * @file espectre_protocol.h
21 * @brief Wire types and payload builders for the ESPectre Protocol.
22 *
23 * The protocol is the contract between a device and whatever consumes it:
24 * MQTT topics, Direct WebSocket messages, JSON payloads, and the OTA status model.
25 * It is specified in `docs/ESPECTRE_PROTOCOL.md`; this header is the C++ view
26 * of that specification.
27 *
28 * Use it whenever your integration should stay interoperable with the shipped
29 * clients — the CLI, Home Assistant discovery, and the web portal all
30 * speak it. The builders take a `RuntimeSnapshot` and return a serialized
31 * payload, so your transport only moves bytes and never formats them.
32 *
33 * The parsers never throw: they validate and report failure through an out
34 * parameter. They do not all roll back cleanly on rejection, so parse into a
35 * copy of your live configuration and commit it only on success, which is what
36 * the shipped frontends do.
37 */
38
39namespace espectre {
40
42
43/** Protocol version reported in payloads. Bumped on a wire-format change. */
44inline constexpr const char *ESPECTRE_PROTOCOL_VERSION = "1.0";
45/** Default MQTT topic root. Override per device with `EspectreDeviceConfig::topic_prefix`. */
46inline constexpr const char *ESPECTRE_TOPIC_PREFIX = "espectre/v1/devices";
47/** Official tagged GitHub Release OTA channel. */
48inline constexpr const char *ESPECTRE_OTA_CHANNEL_RELEASE = "release";
49/** Rolling `main` OTA channel. Fetches GitHub Releases tag `snapshot`. */
50inline constexpr const char *ESPECTRE_OTA_CHANNEL_PREVIEW = "preview";
51/** Rolling `develop` OTA channel. Fetches GitHub Releases tag `snapshot-dev`. */
52inline constexpr const char *ESPECTRE_OTA_CHANNEL_DEVELOP = "develop";
53/** GitHub Releases tag for the `preview` OTA channel. Distinct from branch `main`. */
54inline constexpr const char *ESPECTRE_OTA_RELEASE_TAG_PREVIEW = "snapshot";
55/** GitHub Releases tag for the `develop` OTA channel. Distinct from branch `develop`. */
56inline constexpr const char *ESPECTRE_OTA_RELEASE_TAG_DEVELOP = "snapshot-dev";
57/** Sentinel meaning "use the runtime-generated device id". */
58inline constexpr uint64_t ESPECTRE_DEFAULT_DEVICE_ID = 0U;
59/** Empty label, meaning the device id is used as the display name. */
60inline constexpr const char *ESPECTRE_DEFAULT_DEVICE_LABEL = "";
61
62/**
63 * Device identity and broker settings.
64 *
65 * Frontends persist this so a device keeps its identity and connection across
66 * reboots and reprovisioning.
67 */
69 /** Stable device identity. Zero means use the runtime-generated value. */
71 /** Human-readable name. Empty falls back to the formatted device id. */
73 /** Broker hostname or IP. Empty disables MQTT: `IMqttTransport::setup()` fails. */
74 std::string mqtt_host;
75 uint16_t mqtt_port{1883};
76 /** Broker credentials. Leave empty for anonymous brokers. */
77 std::string mqtt_username;
78 std::string mqtt_password;
79 /** Topic root. Change it only if you also change every consumer. */
81};
82
83/** Link details available to frontends. Canonical MQTT info publishes only the channel. */
85 std::string ip_address;
86 std::string mac_address;
87 /** Wi-Fi channel in use. Zero when unknown. */
88 uint8_t channel{0U};
89};
90
91/**
92 * What the device advertises about itself.
93 *
94 * The `supports_*` flags are how a client learns which controls to offer.
95 * Derive them from `RuntimeCapabilities` rather than hardcoding, and let
96 * `normalize_protocol_device_info()` fill the gaps from a snapshot.
97 * MQTT clients that need command names should request `commands` rather than
98 * reconstructing the list from these flags.
99 */
101 /** Frontend name, for example `"native"`, `"matter"`, or your own. */
102 std::string frontend{"unknown"};
103 /** Application version, normally `espectre_firmware_version()`. */
104 std::string firmware_version{"unknown"};
105 /** Chip target, normally `CONFIG_IDF_TARGET`. */
106 std::string chip{"unknown"};
107 /** Active detector. Left empty, it is filled from the snapshot. */
108 std::string detector;
109 bool supports_info{true};
110 bool supports_stats{false};
111 /** MQTT `set_device_label` is honored and persists the user-facing label. */
118 bool supports_ota{false};
119 /**
120 * CSI traffic ownership mode: `"internal"`, `"external"`, or `"disabled"`.
121 *
122 * Omitted from `info` when empty. Sensing MQTT frontends that own traffic control fill it.
123 * `"pacing"` is Streamer collector mode only and is not a sensing MQTT value.
124 */
125 std::string csi_traffic_mode;
126 /**
127 * Internal traffic generator mode, such as `"ping"` or `"dns"`.
128 *
129 * Omitted from `info` when empty.
130 */
131 std::string traffic_mode;
132 /**
133 * Internal traffic generator and temporal-grid target rate, in packets per second.
134 *
135 * Omitted from `info` when zero.
136 */
137 uint32_t csi_target_pps{0U};
138 /**
139 * Detector evaluation cadence, in milliseconds.
140 *
141 * Omitted from `info` when zero. Canonical MQTT telemetry follows this interval.
142 */
144 /**
145 * Status-log and diagnostics heartbeat, in milliseconds.
146 *
147 * Omitted from `info` when zero. This is not the sensing telemetry interval.
148 */
151};
152
153/**
154 * A parsed control command.
155 *
156 * Fields are optional by design: each `has_*` flag says whether the peer
157 * actually sent that field, so an unset value is never confused with a zero
158 * the caller meant. Apply only the flagged fields.
159 */
161 /** Correlation id echoed in the result payload. May be empty. */
162 std::string command_id;
163 /** Command verb, for example `"set_threshold"` or `"recalibrate"`. */
164 std::string command;
165 /** User-facing label requested by `set_device_label`; empty clears it. */
166 std::string device_label;
167 /** Whether the command carried a valid string-valued `device_label`. */
168 bool has_device_label{false};
169 float threshold{0.0f};
170 bool has_threshold{false};
171 uint8_t motion_on_hits{0U};
172 uint8_t motion_off_hits{0U};
173 bool has_motion_hits{false};
174 std::string csi_traffic_mode;
178 std::string detector;
179 bool has_detector{false};
180 std::string wifi_ssid;
181 std::string wifi_password;
182 std::string wifi_bssid;
183 std::string wifi_band_policy;
184 uint8_t wifi_channel{0U};
185 bool has_wifi_ssid{false};
186 bool has_wifi_password{false};
187 bool has_wifi_bssid{false};
189 bool has_wifi_channel{false};
190 std::string mqtt_host;
191 std::string mqtt_username;
192 std::string mqtt_password;
193 std::string mqtt_topic_prefix;
194 uint16_t mqtt_port{0U};
195 bool has_mqtt_host{false};
196 bool has_mqtt_username{false};
197 bool has_mqtt_password{false};
199 bool has_mqtt_port{false};
200 /**
201 * OTA release channel for `ota_check` and `ota_start`: `"release"`, `"preview"`,
202 * or `"develop"`. Empty with `has_ota_channel` false means the firmware default.
203 */
204 std::string ota_channel;
205 bool has_ota_channel{false};
206};
207
208/**
209 * OTA progress, as reported to clients.
210 *
211 * A check runs `IDLE` -> `CHECKING` -> `UPDATE_AVAILABLE` or `UP_TO_DATE`.
212 * An update continues `DOWNLOADING` -> `APPLYING` -> `REBOOT_SCHEDULED`.
213 * `ERROR` is terminal for the attempt and carries the reason in
214 * `EspectreOtaStatus::message`.
215 */
226
227/** Full OTA status: state, the versions involved, and the resolved URLs. */
230 std::string current_version{"unknown"};
231 std::string target_version;
232 std::string manifest_url;
233 std::string image_url;
234 std::string message;
235 /** Build-time OTA channel used when a command omits its channel. */
236 std::string default_channel;
237 /** Resolved OTA channel for the current attempt. Empty when unused. */
238 std::string channel;
239 bool busy{false};
240 bool update_available{false};
241};
242
243/**
244 * @name Device identity
245 * Resolve, format, and parse the identity a device presents on the wire.
246 * @{
247 */
248
249/** Format a device id in its canonical wire form. */
250std::string format_espectre_device_id(uint64_t device_id);
251/**
252 * Parse a device id from its wire form.
253 *
254 * @param value Formatted device id, as produced by `format_espectre_device_id()`.
255 * @param device_id Written only when parsing succeeds.
256 * @return false on a malformed value, leaving the output untouched.
257 */
258bool parse_espectre_device_id(const std::string &value, uint64_t *device_id);
259/**
260 * Pack the first six MAC bytes into the historical numeric representation.
261 *
262 * @deprecated Runtime firmware uses the cached, domain-separated SHA-256
263 * pseudonym from `derive_runtime_device_id()` instead.
264 */
265[[deprecated("use the runtime-generated device identity")]] uint64_t espectre_device_id_from_mac(
266 const uint8_t *mac, size_t mac_len);
267/** Conventional device name derived from the immutable device identifier. */
268std::string espectre_device_name(uint64_t device_id, const char *chip = nullptr);
269/** The id actually in use. Frontend startup replaces the zero sentinel. */
271/** `espectre_effective_device_id_u64()` in wire form. */
273/** The configured label, or the effective device id when no label is set. */
275/**
276 * Fill in the parts of a device info block the frontend did not set.
277 *
278 * Takes the detector from `snapshot`, and `default_frontend` / `default_chip`
279 * where the caller left the field empty, so each frontend only states what is
280 * genuinely its own.
281 *
282 * @param info What the frontend knows about itself.
283 * @param snapshot Source of the detector name. May be `nullptr` when no
284 * snapshot exists yet.
285 * @param supports_ota Whether this frontend exposes firmware updates.
286 * @param default_frontend Frontend name used when `info.frontend` is empty.
287 * @param default_chip Chip name used when `info.chip` is empty.
288 * @return A copy of `info` with the gaps filled.
289 */
291 const RuntimeSnapshot *snapshot,
292 bool supports_ota,
293 const char *default_frontend,
294 const char *default_chip = nullptr);
295/** Erase broker settings while preserving identity, for a config reset. */
297
298/** @} */
299
300/**
301 * @name Topics and payloads
302 * Build the wire representation from runtime state. Each returns a complete
303 * payload ready to hand to a transport.
304 * @{
305 */
306
307/** Build a full topic from this device's prefix and a trailing segment. */
308std::string espectre_topic(const EspectreDeviceConfig &config, const char *suffix);
309/** Availability payload. Publish it retained so late subscribers see it. */
310std::string espectre_status_payload(const EspectreDeviceConfig &config, bool online, uint32_t timestamp_ms);
311/** Device description, supported controls, and optional CSI traffic settings. Publish retained on connect. */
312std::string espectre_info_payload(const EspectreDeviceConfig &config, const EspectreDeviceInfo &info);
313/**
314 * MQTT command catalog for the current frontend.
315 *
316 * Published on `commands/catalog` in response to `commands`. The list is
317 * derived from the same `supports_*` flags carried by `info`.
318 */
320/** Motion state, metric, and threshold. The payload behind every motion update. */
322 const RuntimeSnapshot &snapshot,
323 uint32_t timestamp_ms,
324 uint32_t uptime_s,
325 const char *frontend);
326/**
327 * Health counters plus optional rate and link diagnostics.
328 *
329 * `diagnostics` carries CSI and link rates from `RuntimeDiagnosticsSampler`.
330 * Pass `nullptr` only for a frontend that does not expose extended diagnostics.
331 */
333 const RuntimeSnapshot &snapshot,
334 uint32_t timestamp_ms,
335 uint32_t uptime_s,
336 float free_memory_kb,
337 float loop_time_ms,
338 const RuntimeDiagnosticsSample *diagnostics = nullptr);
339/**
340 * Acknowledge a command, echoing its `command_id`.
341 *
342 * Publish one for every command you parse, accepted or not; clients correlate
343 * on the id and otherwise cannot tell rejection from packet loss.
344 */
346 const EspectreCommand &command,
347 bool accepted,
348 const char *message);
349/** OTA progress payload, for each `IOtaService` status callback worth publishing. */
351 const EspectreOtaStatus &status,
352 uint32_t timestamp_ms);
353
354/** @} */
355
356/**
357 * @name Command parsing
358 * Turn received bytes into validated values.
359 * @{
360 */
361
362/**
363 * Parse a JSON command payload from the MQTT command topic.
364 *
365 * @param payload Raw message body as received.
366 * @param command Populated only on success. Check the `has_*` flags to see
367 * which fields the peer actually sent.
368 * @param error Receives a human-readable reason on failure. May be `nullptr`.
369 * @return false on malformed input or an unknown command.
370 */
371bool parse_espectre_command(const std::string &payload, EspectreCommand *command, std::string *error);
372/**
373 * Parse a transport-neutral command name plus a JSON parameter object.
374 *
375 * Direct WebSocket uses the request envelope id and method as the first two
376 * arguments. MQTT uses `parse_espectre_command()` for its flat payload.
377 */
378bool parse_espectre_command_request(const std::string &command_id,
379 const std::string &command_name,
380 const std::string &params_json,
381 EspectreCommand *command,
382 std::string *error);
383/**
384 * Whether `channel` is a published OTA channel name.
385 *
386 * Accepted values are `release`, `preview`, and `develop`. Empty is not
387 * accepted here; omit the field to keep the firmware default.
388 */
389bool espectre_ota_channel_accepted(const std::string &channel);
390/**
391 * Built-in GitHub Releases manifest URL for a frontend, chip, and channel.
392 *
393 * `release` uses `/releases/latest/download/`. `preview` uses tag
394 * `ESPECTRE_OTA_RELEASE_TAG_PREVIEW` (`snapshot`). `develop` uses tag
395 * `ESPECTRE_OTA_RELEASE_TAG_DEVELOP` (`snapshot-dev`).
396 *
397 * @return Empty when `frontend`, `chip`, or `channel` is not a published value.
398 */
399std::string espectre_ota_manifest_url(const char *frontend, const char *chip, const std::string &channel);
400/**
401 * Parse a legacy ASCII `SET_DEVICE_CONFIG:` command.
402 *
403 * Carries one `key=value` pair, applied in place. A rejected command writes
404 * nothing.
405 *
406 * @param command Full command string, including the `SET_DEVICE_CONFIG:` prefix.
407 * @param config Updated in place on success.
408 * @param error Receives a human-readable reason on failure. May be `nullptr`.
409 */
410bool parse_espectre_config_command(const std::string &command, EspectreDeviceConfig *config, std::string *error);
411/**
412 * Parse a `SET_MQTT_CONFIG:` command, carrying the broker settings.
413 *
414 * Fields are applied as they are read, so a command rejected part-way through
415 * leaves `config` partially updated. Pass a copy and commit only on success.
416 * `host` and `port` are required; the rest keep their previous values.
417 */
418bool parse_espectre_mqtt_config_command(const std::string &command, EspectreDeviceConfig *config, std::string *error);
419
420/** @} */
421
422} // namespace espectre
constexpr const char * ESPECTRE_DEFAULT_DEVICE_LABEL
Empty label, meaning the device id is used as the display name.
std::string espectre_status_payload(const EspectreDeviceConfig &config, bool online, uint32_t timestamp_ms)
Availability payload.
constexpr const char * ESPECTRE_TOPIC_PREFIX
Default MQTT topic root.
constexpr const char * ESPECTRE_PROTOCOL_VERSION
Protocol version reported in payloads.
std::string espectre_topic(const EspectreDeviceConfig &config, const char *suffix)
Build a full topic from this device's prefix and a trailing segment.
std::string espectre_commands_payload(const EspectreDeviceConfig &config, const EspectreDeviceInfo &info)
MQTT command catalog for the current frontend.
std::string espectre_command_result_payload(const EspectreDeviceConfig &config, const EspectreCommand &command, bool accepted, const char *message)
Acknowledge a command, echoing its command_id.
bool parse_espectre_device_id(const std::string &value, uint64_t *device_id)
Parse a device id from its wire form.
constexpr const char * ESPECTRE_OTA_CHANNEL_DEVELOP
Rolling develop OTA channel.
bool parse_espectre_config_command(const std::string &command, EspectreDeviceConfig *config, std::string *error)
Parse a legacy ASCII SET_DEVICE_CONFIG: command.
std::string espectre_stats_payload(const EspectreDeviceConfig &config, const RuntimeSnapshot &snapshot, uint32_t timestamp_ms, uint32_t uptime_s, float free_memory_kb, float loop_time_ms, const RuntimeDiagnosticsSample *diagnostics=nullptr)
Health counters plus optional rate and link diagnostics.
EspectreOtaState
OTA progress, as reported to clients.
std::string espectre_telemetry_payload(const EspectreDeviceConfig &config, const RuntimeSnapshot &snapshot, uint32_t timestamp_ms, uint32_t uptime_s, const char *frontend)
Motion state, metric, and threshold.
std::string espectre_effective_device_id(const EspectreDeviceConfig &config)
espectre_effective_device_id_u64() in wire form.
std::string espectre_info_payload(const EspectreDeviceConfig &config, const EspectreDeviceInfo &info)
Device description, supported controls, and optional CSI traffic settings.
constexpr uint64_t ESPECTRE_DEFAULT_DEVICE_ID
Sentinel meaning "use the runtime-generated device id".
bool parse_espectre_mqtt_config_command(const std::string &command, EspectreDeviceConfig *config, std::string *error)
Parse a SET_MQTT_CONFIG: command, carrying the broker settings.
uint64_t espectre_effective_device_id_u64(const EspectreDeviceConfig &config)
The id actually in use.
bool parse_espectre_command(const std::string &payload, EspectreCommand *command, std::string *error)
Parse a JSON command payload from the MQTT command topic.
constexpr const char * ESPECTRE_OTA_CHANNEL_PREVIEW
Rolling main OTA channel.
bool espectre_ota_channel_accepted(const std::string &channel)
Whether channel is a published OTA channel name.
constexpr const char * ESPECTRE_OTA_RELEASE_TAG_PREVIEW
GitHub Releases tag for the preview OTA channel.
bool parse_espectre_command_request(const std::string &command_id, const std::string &command_name, const std::string &params_json, EspectreCommand *command, std::string *error)
Parse a transport-neutral command name plus a JSON parameter object.
std::string espectre_ota_manifest_url(const char *frontend, const char *chip, const std::string &channel)
Built-in GitHub Releases manifest URL for a frontend, chip, and channel.
void clear_espectre_mqtt_config(EspectreDeviceConfig *config)
Erase broker settings while preserving identity, for a config reset.
std::string espectre_ota_status_payload(const EspectreDeviceConfig &config, const EspectreOtaStatus &status, uint32_t timestamp_ms)
OTA progress payload, for each IOtaService status callback worth publishing.
std::string espectre_effective_device_label(const EspectreDeviceConfig &config)
The configured label, or the effective device id when no label is set.
uint64_t espectre_device_id_from_mac(const uint8_t *mac, size_t mac_len)
Pack the first six MAC bytes into the historical numeric representation.
std::string espectre_device_name(uint64_t device_id, const char *chip=nullptr)
Conventional device name derived from the immutable device identifier.
std::string format_espectre_device_id(uint64_t device_id)
Format a device id in its canonical wire form.
constexpr const char * ESPECTRE_OTA_CHANNEL_RELEASE
Official tagged GitHub Release OTA channel.
constexpr const char * ESPECTRE_OTA_RELEASE_TAG_DEVELOP
GitHub Releases tag for the develop OTA channel.
EspectreDeviceInfo normalize_protocol_device_info(const EspectreDeviceInfo &info, const RuntimeSnapshot *snapshot, bool supports_ota, const char *default_frontend, const char *default_chip=nullptr)
Fill in the parts of a device info block the frontend did not set.
A parsed control command.
std::string device_label
User-facing label requested by set_device_label; empty clears it.
std::string ota_channel
OTA release channel for ota_check and ota_start: "release", "preview", or "develop".
bool has_device_label
Whether the command carried a valid string-valued device_label.
std::string command_id
Correlation id echoed in the result payload.
std::string command
Command verb, for example "set_threshold" or "recalibrate".
Device identity and broker settings.
std::string mqtt_host
Broker hostname or IP.
uint64_t device_id
Stable device identity.
std::string mqtt_username
Broker credentials.
std::string device_label
Human-readable name.
std::string topic_prefix
Topic root.
What the device advertises about itself.
std::string traffic_mode
Internal traffic generator mode, such as "ping" or "dns".
std::string firmware_version
Application version, normally espectre_firmware_version().
bool supports_device_config
MQTT set_device_label is honored and persists the user-facing label.
uint32_t evaluation_interval_ms
Detector evaluation cadence, in milliseconds.
uint32_t publish_interval_ms
Status-log and diagnostics heartbeat, in milliseconds.
std::string frontend
Frontend name, for example "native", "matter", or your own.
uint32_t csi_target_pps
Internal traffic generator and temporal-grid target rate, in packets per second.
std::string chip
Chip target, normally CONFIG_IDF_TARGET.
std::string csi_traffic_mode
CSI traffic ownership mode: "internal", "external", or "disabled".
std::string detector
Active detector.
Link details available to frontends.
uint8_t channel
Wi-Fi channel in use.
Full OTA status: state, the versions involved, and the resolved URLs.
std::string channel
Resolved OTA channel for the current attempt.
std::string default_channel
Build-time OTA channel used when a command omits its channel.
Rate and link diagnostics derived from cumulative runtime counters.
A consistent view of the sensing state at one instant.