ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
sparkplug.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file sparkplug.cpp
6 * @brief Sparkplug B payload + topic builder over the Protobuf codec (pure, host-tested).
7 */
8
10
11#if PC_ENABLE_SPARKPLUG
12
14#include <string.h>
15
16// Tahu Payload field numbers.
17#define SPB_PL_TIMESTAMP 1
18#define SPB_PL_METRICS 2
19#define SPB_PL_SEQ 3
20
21// Tahu Metric field numbers.
22#define SPB_MET_NAME 1
23#define SPB_MET_ALIAS 2
24#define SPB_MET_TIMESTAMP 3
25#define SPB_MET_DATATYPE 4
26#define SPB_MET_INT 10
27#define SPB_MET_LONG 11
28#define SPB_MET_FLOAT 12
29#define SPB_MET_DOUBLE 13
30#define SPB_MET_BOOL 14
31#define SPB_MET_STRING 15
32
33size_t pc_spb_build_topic(char *buf, size_t cap, const char *group, const char *message_type, const char *edge_node,
34 const char *device)
35{
36 if (!buf || !group || !message_type || !edge_node)
37 {
38 return 0;
39 }
40 // spBv1.0/<group>/<message_type>/<edge_node>[/<device>]
41 size_t need = 8 /*"spBv1.0/"*/ + strnlen(group, cap) + 1 + strnlen(message_type, cap) + 1 + strnlen(edge_node, cap);
42 if (device)
43 {
44 need += 1 + strnlen(device, cap);
45 }
46 if (need + 1 > cap) // + NUL
47 {
48 return 0;
49 }
50 size_t p = 0;
51 const char *prefix = "spBv1.0/";
52 memcpy(buf + p, prefix, 8);
53 p += 8;
54 size_t n = strnlen(group, cap);
55 memcpy(buf + p, group, n);
56 p += n;
57 buf[p++] = '/';
58 n = strnlen(message_type, cap);
59 memcpy(buf + p, message_type, n);
60 p += n;
61 buf[p++] = '/';
62 n = strnlen(edge_node, cap);
63 memcpy(buf + p, edge_node, n);
64 p += n;
65 if (device)
66 {
67 buf[p++] = '/';
68 n = strnlen(device, cap);
69 memcpy(buf + p, device, n);
70 p += n;
71 }
72 buf[p] = '\0';
73 return p;
74}
75
76size_t pc_spb_build_metric(uint8_t *buf, size_t cap, const SpbMetric *m)
77{
78 if (!buf || !m)
79 {
80 return 0;
81 }
82 PbWriter w;
83 pc_pb_writer_init(&w, buf, cap);
84 if (m->name)
85 {
86 pc_pb_string(&w, SPB_MET_NAME, m->name);
87 }
88 if (m->has_alias)
89 {
90 pc_pb_uint64(&w, SPB_MET_ALIAS, m->alias);
91 }
92 if (m->has_timestamp)
93 {
94 pc_pb_uint64(&w, SPB_MET_TIMESTAMP, m->timestamp);
95 }
96 pc_pb_uint64(&w, SPB_MET_DATATYPE, m->datatype); // datatype is a uint32; the varint covers it
97 switch (m->kind)
98 {
99 case SpbMetricKind::SPB_M_INT:
100 pc_pb_uint64(&w, SPB_MET_INT, m->int_value);
101 break;
102 case SpbMetricKind::SPB_M_LONG:
103 pc_pb_uint64(&w, SPB_MET_LONG, m->long_value);
104 break;
105 case SpbMetricKind::SPB_M_FLOAT:
106 pc_pb_float(&w, SPB_MET_FLOAT, m->float_value);
107 break;
108 case SpbMetricKind::SPB_M_DOUBLE:
109 pc_pb_double(&w, SPB_MET_DOUBLE, m->double_value);
110 break;
111 case SpbMetricKind::SPB_M_BOOL:
112 pc_pb_bool(&w, SPB_MET_BOOL, m->bool_value);
113 break;
114 case SpbMetricKind::SPB_M_STRING:
115 if (m->string_value)
116 {
117 pc_pb_string(&w, SPB_MET_STRING, m->string_value);
118 }
119 break;
120 }
121 return pc_pb_writer_finish(&w);
122}
123
124size_t pc_spb_build_payload(uint8_t *buf, size_t cap, uint64_t timestamp, uint64_t seq, const SpbMetric *metrics,
125 size_t n)
126{
127 if (!buf || (n && !metrics))
128 {
129 return 0;
130 }
131 PbWriter w;
132 pc_pb_writer_init(&w, buf, cap);
133 pc_pb_uint64(&w, SPB_PL_TIMESTAMP, timestamp);
134 for (size_t i = 0; i < n; i++)
135 {
136 // Serialize each Metric submessage into a bounded temp, then add it as a
137 // length-delimited field (Payload.metrics). A metric stays well under this bound
138 // unless it carries a large string, in which case the build fails closed.
139 uint8_t metric[PC_SPB_METRIC_MAX];
140 size_t mlen = pc_spb_build_metric(metric, sizeof(metric), &metrics[i]);
141 if (!mlen)
142 {
143 return 0;
144 }
145 pc_pb_bytes(&w, SPB_PL_METRICS, metric, mlen);
146 }
147 pc_pb_uint64(&w, SPB_PL_SEQ, seq);
148 return pc_pb_writer_finish(&w);
149}
150
151bool pc_spb_parse_payload(const uint8_t *buf, size_t len, SpbPayloadHeader *out)
152{
153 if (!buf || !out)
154 {
155 return false;
156 }
157 memset(out, 0, sizeof(*out));
158 size_t pos = 0;
159 PbField f;
160 while (pos < len)
161 {
162 if (!pc_pb_read_field(buf, len, &pos, &f))
163 {
164 return false;
165 }
166 if (f.field_number == SPB_PL_TIMESTAMP && f.wire_type == PB_WT_VARINT)
167 {
168 out->has_timestamp = true;
169 out->timestamp = f.value;
170 }
171 else if (f.field_number == SPB_PL_SEQ && f.wire_type == PB_WT_VARINT)
172 {
173 out->has_seq = true;
174 out->seq = f.value;
175 }
176 // metrics (field 2), uuid, body are skipped here
177 }
178 return true;
179}
180
181bool pc_spb_payload_next_metric(const uint8_t *buf, size_t len, size_t *pos, const uint8_t **metric, size_t *metric_len)
182{
183 if (!buf || !pos || !metric || !metric_len)
184 {
185 return false;
186 }
187 PbField f;
188 while (*pos < len)
189 {
190 if (!pc_pb_read_field(buf, len, pos, &f))
191 {
192 return false;
193 }
194 if (f.field_number == SPB_PL_METRICS && f.wire_type == PB_WT_LEN)
195 {
196 *metric = f.data;
197 *metric_len = f.len;
198 return true;
199 }
200 }
201 return false;
202}
203
204// Apply a Metric metadata field (name / alias / timestamp / datatype) from @p f to @p out.
205static void spb_apply_meta_field(SpbMetricDecoded *out, const PbField *f)
206{
207 switch (f->field_number)
208 {
209 case SPB_MET_NAME:
210 if (f->wire_type == PB_WT_LEN)
211 {
212 out->name = (const char *)f->data;
213 out->name_len = f->len;
214 }
215 break;
216 case SPB_MET_ALIAS:
217 if (f->wire_type == PB_WT_VARINT)
218 {
219 out->has_alias = true;
220 out->alias = f->value;
221 }
222 break;
223 case SPB_MET_TIMESTAMP:
224 if (f->wire_type == PB_WT_VARINT)
225 {
226 out->has_timestamp = true;
227 out->timestamp = f->value;
228 }
229 break;
230 case SPB_MET_DATATYPE:
231 if (f->wire_type == PB_WT_VARINT)
232 {
233 out->datatype = (uint32_t)f->value;
234 }
235 break;
236 default:
237 break;
238 }
239}
240
241// Apply a Metric typed-value field (one of int / long / float / double / bool / string) from @p f.
242static void spb_apply_value_field(SpbMetricDecoded *out, const PbField *f)
243{
244 switch (f->field_number)
245 {
246 case SPB_MET_INT:
247 if (f->wire_type == PB_WT_VARINT)
248 {
249 out->has_value = true;
250 out->kind = SpbMetricKind::SPB_M_INT;
251 out->int_value = (uint32_t)f->value;
252 }
253 break;
254 case SPB_MET_LONG:
255 if (f->wire_type == PB_WT_VARINT)
256 {
257 out->has_value = true;
258 out->kind = SpbMetricKind::SPB_M_LONG;
259 out->long_value = f->value;
260 }
261 break;
262 case SPB_MET_FLOAT:
263 if (f->wire_type == PB_WT_I32)
264 {
265 out->has_value = true;
266 out->kind = SpbMetricKind::SPB_M_FLOAT;
267 out->float_value = pc_pb_float_bits((uint32_t)f->value);
268 }
269 break;
270 case SPB_MET_DOUBLE:
271 if (f->wire_type == PB_WT_I64)
272 {
273 out->has_value = true;
274 out->kind = SpbMetricKind::SPB_M_DOUBLE;
275 out->double_value = pc_pb_double_bits(f->value);
276 }
277 break;
278 case SPB_MET_BOOL:
279 if (f->wire_type == PB_WT_VARINT)
280 {
281 out->has_value = true;
282 out->kind = SpbMetricKind::SPB_M_BOOL;
283 out->bool_value = f->value != 0;
284 }
285 break;
286 case SPB_MET_STRING:
287 if (f->wire_type == PB_WT_LEN)
288 {
289 out->has_value = true;
290 out->kind = SpbMetricKind::SPB_M_STRING;
291 out->string_value = (const char *)f->data;
292 out->string_value_len = f->len;
293 }
294 break;
295 default:
296 break;
297 }
298}
299
300bool pc_spb_parse_metric(const uint8_t *buf, size_t len, SpbMetricDecoded *out)
301{
302 if (!buf || !out)
303 {
304 return false;
305 }
306 memset(out, 0, sizeof(*out));
307 size_t pos = 0;
308 PbField f;
309 while (pos < len)
310 {
311 if (!pc_pb_read_field(buf, len, &pos, &f))
312 {
313 return false;
314 }
315 spb_apply_meta_field(out, &f); // name / alias / timestamp / datatype
316 spb_apply_value_field(out, &f); // the typed value (int / long / float / double / bool / string)
317 }
318 return true;
319}
320
321#endif // PC_ENABLE_SPARKPLUG
Protocol Buffers wire codec (PC_ENABLE_PROTOBUF) - zero-heap streaming writer.
#define PC_SPB_METRIC_MAX
Max serialized size of one Sparkplug B metric submessage (stack temp, bytes).
Sparkplug B payload + topic codec (PC_ENABLE_SPARKPLUG) - zero-heap builder for the Eclipse Sparkplug...