ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
sparkplug.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file sparkplug.h
6 * @brief Sparkplug B topic and payload codec (PROTOCORE_ENABLE_SPARKPLUG) - a zero-heap builder and
7 * reader for the Sparkplug B topic namespace and its Google Protocol Buffers payload.
8 *
9 * **The governing standard is not IETF.** Sparkplug is an Eclipse Foundation specification:
10 * Sparkplug 3.0.0, Eclipse Sparkplug Contributors. It rides MQTT, which is an OASIS standard -
11 * Sparkplug 3.0.0 sec 1.5 (Normative References) names MQTT Version 3.1.1 Plus Errata 01, OASIS
12 * Standard Incorporating Approved Errata 01, and MQTT Version 5.0, OASIS Standard. No RFC governs
13 * either one, and none is cited here.
14 *
15 * Sparkplug 3.0.0 sec 4.1 gives the topic:
16 *
17 * namespace/group_id/message_type/edge_node_id/[device_id]
18 *
19 * The namespace element is the UTF-8 constant `spBv1.0` (sec 4.1.1). group_id (sec 4.1.2),
20 * edge_node_id (sec 4.1.4) and device_id (sec 4.1.5) are UTF-8 excluding the reserved `+`, `/` and
21 * `#`. sec 4.1.3 defines nine message_type values. sec 4.1.5 states device_id MUST be included with
22 * DBIRTH, DDEATH, DDATA and DCMD, and MUST NOT be included with NBIRTH, NDEATH, NDATA, NCMD and
23 * STATE. The separator between elements is the MQTT topic level separator (MQTT Version 5.0
24 * sec 4.7.1.1).
25 *
26 * Sparkplug 3.0.0 sec 6.4.1 (Google Protocol Buffer Schema) gives the payload. Payload carries
27 * timestamp(1), metrics(2, repeated), seq(3), uuid(4) and body(5). Each Metric carries name(1),
28 * alias(2), timestamp(3), datatype(4), is_historical(5), is_transient(6), is_null(7), metadata(8),
29 * properties(9), and one member of the value oneof - int_value(10), long_value(11), float_value(12),
30 * double_value(13), boolean_value(14), string_value(15), bytes_value(16), dataset_value(17),
31 * template_value(18), extension_value(19).
32 *
33 * This codec covers name, alias, timestamp, datatype and the first six value members. is_historical,
34 * is_transient, is_null, metadata, properties, bytes_value, dataset_value, template_value and
35 * extension_value are neither written nor reported.
36 *
37 * Built on the Protocol Buffers codec (services/iot/protobuf) and published with the MQTT client.
38 *
39 * The module exports one symbol, @ref Sparkplug. Everything in sparkplug.c has internal linkage.
40 *
41 * @author Douglas Quigg (dstroy0)
42 * @date 2026
43 */
44
45#ifndef PROTOCORE_SPARKPLUG_H
46#define PROTOCORE_SPARKPLUG_H
47
48#include "protocore_config.h" // the entry point: protocore_types.h for the widths
49
50#if PROTOCORE_ENABLE_SPARKPLUG
51
53
54/** @brief Sparkplug 3.0.0 sec 4.1.1: the namespace element for the Sparkplug B payload definition. */
55#define SPB_NAMESPACE "spBv1.0"
56
57// Sparkplug 3.0.0 sec 4.1.3 message_type elements.
58#define SPB_MSG_NBIRTH "NBIRTH" ///< birth certificate for Sparkplug Edge Nodes
59#define SPB_MSG_NDEATH "NDEATH" ///< death certificate for Sparkplug Edge Nodes
60#define SPB_MSG_DBIRTH "DBIRTH" ///< birth certificate for Devices
61#define SPB_MSG_DDEATH "DDEATH" ///< death certificate for Devices
62#define SPB_MSG_NDATA "NDATA" ///< Edge Node data message
63#define SPB_MSG_DDATA "DDATA" ///< Device data message
64#define SPB_MSG_NCMD "NCMD" ///< Edge Node command message
65#define SPB_MSG_DCMD "DCMD" ///< Device command message
66#define SPB_MSG_STATE "STATE" ///< Sparkplug Host Application state message
67
68// Sparkplug 3.0.0 sec 6.4.16 DataType, the codes Metric.datatype takes.
69#define SPB_DT_UNKNOWN 0
70#define SPB_DT_INT8 1
71#define SPB_DT_INT16 2
72#define SPB_DT_INT32 3
73#define SPB_DT_INT64 4
74#define SPB_DT_UINT8 5
75#define SPB_DT_UINT16 6
76#define SPB_DT_UINT32 7
77#define SPB_DT_UINT64 8
78#define SPB_DT_FLOAT 9
79#define SPB_DT_DOUBLE 10
80#define SPB_DT_BOOLEAN 11
81#define SPB_DT_STRING 12
82#define SPB_DT_DATETIME 13
83#define SPB_DT_TEXT 14
84#define SPB_DT_UUID 15
85#define SPB_DT_DATASET 16
86#define SPB_DT_BYTES 17
87#define SPB_DT_FILE 18
88#define SPB_DT_TEMPLATE 19
89#define SPB_DT_PROPERTYSET 20
90#define SPB_DT_PROPERTYSETLIST 21
91#define SPB_DT_INT8_ARRAY 22
92#define SPB_DT_INT16_ARRAY 23
93#define SPB_DT_INT32_ARRAY 24
94#define SPB_DT_INT64_ARRAY 25
95#define SPB_DT_UINT8_ARRAY 26
96#define SPB_DT_UINT16_ARRAY 27
97#define SPB_DT_UINT32_ARRAY 28
98#define SPB_DT_UINT64_ARRAY 29
99#define SPB_DT_FLOAT_ARRAY 30
100#define SPB_DT_DOUBLE_ARRAY 31
101#define SPB_DT_BOOLEAN_ARRAY 32
102#define SPB_DT_STRING_ARRAY 33
103#define SPB_DT_DATETIME_ARRAY 34
104
105/** @brief Which member of the Metric value oneof carries the value (Sparkplug 3.0.0 sec 6.4.1). */
106typedef enum PROTO_ENUM_PACKED
107{
108 SPB_M_INT, ///< int_value, field 10, uint32
109 SPB_M_LONG, ///< long_value, field 11, uint64
110 SPB_M_FLOAT, ///< float_value, field 12
111 SPB_M_DOUBLE, ///< double_value, field 13
112 SPB_M_BOOL, ///< boolean_value, field 14
113 SPB_M_STRING, ///< string_value, field 15
114} SpbMetricKind;
115
116/** @brief One Metric to encode (Sparkplug 3.0.0 sec 6.4.6). A null name and a false has_* omit the field. */
117typedef struct
118{
119 const char *name; ///< name, field 1; omit on a DATA metric addressed by alias
120 proto_bool has_alias;
121 uint64_t alias; ///< alias, field 2
122 proto_bool has_timestamp;
123 uint64_t timestamp; ///< timestamp, field 3, milliseconds since epoch in UTC
124 uint32_t datatype; ///< datatype, field 4, an SPB_DT_* code
125 SpbMetricKind kind; ///< which value oneof member below is written
126 uint32_t int_value; ///< int_value, field 10
127 uint64_t long_value; ///< long_value, field 11
128 float float_value; ///< float_value, field 12
129 double double_value; ///< double_value, field 13
130 proto_bool bool_value; ///< boolean_value, field 14
131 const char *string_value; ///< string_value, field 15
132} SpbMetric;
133
134/** @brief A decoded Payload's top-level fields (Sparkplug 3.0.0 sec 6.4.5); metrics are iterated separately. */
135typedef struct
136{
137 proto_bool has_timestamp;
138 uint64_t timestamp; ///< timestamp, field 1, milliseconds since epoch in UTC
139 proto_bool has_seq;
140 uint64_t seq; ///< seq, field 3
141} SpbPayloadHeader;
142
143/** @brief A decoded Metric (Sparkplug 3.0.0 sec 6.4.6). name and string_value point INTO the source, un-terminated. */
144typedef struct
145{
146 const char *name; ///< name, field 1, or nullptr when the metric is addressed by alias
147 size_t name_len;
148 proto_bool has_alias;
149 uint64_t alias; ///< alias, field 2
150 proto_bool has_timestamp;
151 uint64_t timestamp; ///< timestamp, field 3
152 uint32_t datatype; ///< datatype, field 4, an SPB_DT_* code
153 proto_bool has_value; ///< false when no value oneof member was present
154 SpbMetricKind kind; ///< which value member is set, valid when @ref has_value
155 uint32_t int_value; ///< int_value, field 10
156 uint64_t long_value; ///< long_value, field 11
157 float float_value; ///< float_value, field 12
158 double double_value; ///< double_value, field 13
159 proto_bool bool_value; ///< boolean_value, field 14
160 const char *string_value; ///< string_value bytes, field 15, un-terminated, or nullptr
161 size_t string_value_len;
162} SpbMetricDecoded;
163
164/** @brief Sparkplug 3.0.0 sec 4.1 topic namespace elements, less the fixed namespace element. */
165typedef struct
166{
167 const char *group_id; ///< group_id (sec 4.1.2)
168 const char *message_type; ///< message_type (sec 4.1.3), one of the SPB_MSG_* constants
169 const char *edge_node_id; ///< edge_node_id (sec 4.1.4)
170 const char *device_id; ///< device_id (sec 4.1.5); NULL for an Edge Node topic
171} SpbTopicArgs;
172
173/** @brief Where a built topic string lands. */
174typedef struct
175{
176 char *out; ///< the buffer a topic build writes into
177 size_t cap; ///< how much room it has, the NUL included
178} SpbTopicOutArgs;
179
180/** @brief Where encoded Protocol Buffers octets land. */
181typedef struct
182{
183 uint8_t *buf; ///< the buffer a Payload or Metric build writes into
184 size_t cap; ///< how many octets it holds
185} SpbOutArgs;
186
187/** @brief The Payload header fields a build stamps (Sparkplug 3.0.0 sec 6.4.5). */
188typedef struct
189{
190 uint64_t timestamp; ///< timestamp, field 1, milliseconds since epoch; MUST be UTC
191 uint64_t seq; ///< seq, field 3; 0..255, incrementing by one and wrapping to zero
192} SpbPayloadArgs;
193
194/** @brief The Metrics a build serializes (Sparkplug 3.0.0 sec 6.4.6). */
195typedef struct
196{
197 const SpbMetric *list; ///< the Metric array; a Metric build serializes list[0]
198 size_t count; ///< how many of them a Payload build writes
199} SpbMetricsArgs;
200
201/** @brief The octets a decode reads, and the cursor an iteration carries across calls. */
202typedef struct
203{
204 const uint8_t *buf; ///< the encoded Payload or Metric being read
205 size_t len; ///< how many octets it holds
206 size_t cursor; ///< the metrics iteration position; set 0 to start, advanced by each call
207} SpbSourceArgs;
208
209/**
210 * @brief The Sparkplug B codec: the sec 4.1 topic namespace and the sec 6.4.1 payload schema.
211 *
212 * A caller sets the members a call takes, invokes it through ::Sparkplug, and reads the outcome off
213 * the same handle.
214 *
215 * No slot member: the codec holds no rows, so no call names one.
216 *
217 * @var SparkplugNs::topic the topic namespace elements a topic build joins (sec 4.1)
218 * @var SparkplugNs::topic_out the buffer a topic build writes into
219 * @var SparkplugNs::out the buffer a Payload or Metric build writes into
220 * @var SparkplugNs::payload the Payload header fields a build stamps (sec 6.4.5)
221 * @var SparkplugNs::metrics the Metrics a build serializes (sec 6.4.6)
222 * @var SparkplugNs::source the octets a decode reads, and its metrics cursor
223 * @var SparkplugNs::ok a call's true/false outcome; a build reports whether it wrote anything
224 * @var SparkplugNs::n the length a build wrote, 0 if it did not fit
225 * @var SparkplugNs::header the Payload header a parse decoded (sec 6.4.5)
226 * @var SparkplugNs::metric the Metric a parse decoded (sec 6.4.6)
227 * @var SparkplugNs::metric_bytes the next Metric sub-message an iteration reports, pointing into @c source
228 * @var SparkplugNs::metric_len how many octets that sub-message holds
229 * @var SparkplugNs::build_topic join `spBv1.0/group_id/message_type/edge_node_id[/device_id]` into @c topic_out
230 * @var SparkplugNs::build_metric serialize @c metrics.list[0] as one Metric message into @c out
231 * @var SparkplugNs::build_payload serialize @c payload plus @c metrics.count Metrics as one Payload into @c out
232 * @var SparkplugNs::parse_payload read a Payload's timestamp and seq from @c source into @c header
233 * @var SparkplugNs::next_metric report the next metrics(2) sub-message of a Payload and advance @c source.cursor
234 * @var SparkplugNs::parse_metric decode the Metric in @c source into @c metric
235 */
236typedef struct
237{
238 SpbTopicArgs topic; ///< what a topic says
239 SpbTopicOutArgs topic_out; ///< where that topic lands
240 SpbOutArgs out; ///< where encoded octets land
241 SpbPayloadArgs payload; ///< what a Payload header says
242 SpbMetricsArgs metrics; ///< what a build serializes
243 SpbSourceArgs source; ///< what a decode reads
244 proto_bool ok;
245 size_t n;
246 SpbPayloadHeader header;
247 SpbMetricDecoded metric;
248 const uint8_t *metric_bytes;
249 size_t metric_len;
250} SparkplugVars;
251
252/** @brief The operands and the outcome. */
253extern SparkplugVars SparkplugV;
254
255/** @brief The entries. */
256typedef struct
257{
258 void (*const build_topic)(uint8_t *work);
259 void (*const build_metric)(uint8_t *work);
260 void (*const build_payload)(uint8_t *work);
261 void (*const parse_payload)(uint8_t *work);
262 void (*const next_metric)(uint8_t *work);
263 void (*const parse_metric)(uint8_t *work);
264} SparkplugNs;
265
266// What the table binds, defined once in the .c and taking one parameter each: everything
267// else an entry needs is an operand in SparkplugV or a region of the borrow at a fixed offset.
268void protocore_sparkplug_build_topic(uint8_t *work);
269void protocore_sparkplug_build_metric(uint8_t *work);
270void protocore_sparkplug_build_payload(uint8_t *work);
271void protocore_sparkplug_parse_payload(uint8_t *work);
272void protocore_sparkplug_next_metric(uint8_t *work);
273void protocore_sparkplug_parse_metric(uint8_t *work);
274
275// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
276// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
277// `Sparkplug.build_topic(work)` resolves to a named function and becomes a DIRECT call. An extern table
278// leaves the call indirect and the symbol live at every level, -O2 -flto included.
279static const SparkplugNs Sparkplug __attribute__((unused)) = {
280 .build_topic = protocore_sparkplug_build_topic,
281 .build_metric = protocore_sparkplug_build_metric,
282 .build_payload = protocore_sparkplug_build_payload,
283 .parse_payload = protocore_sparkplug_parse_payload,
284 .next_metric = protocore_sparkplug_next_metric,
285 .parse_metric = protocore_sparkplug_parse_metric,
286};
287
288/**
289 * @brief The PROTOCORE_SPARKPLUG_BORROW bytes this module's state lives in.
290 *
291 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
292 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
293 * walks, so the state lasts the life of the program.
294 *
295 * @return the span.
296 */
297uint8_t *protocore_sparkplug_span(void);
298
300
301#endif // PROTOCORE_ENABLE_SPARKPLUG
302
303#endif // PROTOCORE_SPARKPLUG_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97