ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
senml.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 senml.cpp
6 * @brief SenML pack builders: JSON, plus any binary codec via pc_codec (pure, host-tested).
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_SENML
13
17#include <stdio.h> // snprintf for the JSON number formatting
18
19// SenML-CBOR integer labels (RFC 8428).
20#define SENML_LBL_BN (-2)
21#define SENML_LBL_BT (-3)
22#define SENML_LBL_N 0
23#define SENML_LBL_U 1
24#define SENML_LBL_V 2
25#define SENML_LBL_VS 3
26#define SENML_LBL_VB 4
27#define SENML_LBL_T 6
28
29// True when @p d is an integer that fits in int64 (so it can be emitted losslessly as one).
30static bool is_integral(double d)
31{
32 return d >= -9.2e18 && d <= 9.2e18 && d == (double)(int64_t)d;
33}
34
35// Emit a SenML number into a JSON value position: an integer when integral (keeps timestamp
36// precision), otherwise a %g float.
37static void json_num(JsonWriter &w, double d)
38{
39 char tmp[32];
40 if (is_integral(d))
41 {
42 pc_sb sb_tmp = {tmp, sizeof(tmp), 0, true};
43 pc_sb_i64(&sb_tmp, (int64_t)((long long)d));
44 if (pc_sb_finish(&sb_tmp) == 0)
45 {
46 tmp[0] = '\0';
47 }
48 }
49 else
50 {
51 pc_sb sb_tmp2 = {tmp, sizeof(tmp), 0, true};
52 pc_sb_g(&sb_tmp2, (double)(d), 6);
53 if (pc_sb_finish(&sb_tmp2) == 0)
54 {
55 tmp[0] = '\0';
56 }
57 }
58 w.raw(tmp);
59}
60
61size_t pc_senml_json_build(char *buf, size_t cap, const SenmlRecord *records, size_t count)
62{
63 if (!buf || (count && !records))
64 {
65 return 0;
66 }
67 JsonWriter w(buf, cap);
68 w.begin_array();
69 for (size_t i = 0; i < count; i++)
70 {
71 const SenmlRecord &r = records[i];
72 w.begin_object();
73 if (r.base_name)
74 {
75 w.kv_str("bn", r.base_name);
76 }
77 if (r.has_base_time)
78 {
79 w.key("bt");
80 json_num(w, r.base_time);
81 }
82 if (r.name)
83 {
84 w.kv_str("n", r.name);
85 }
86 if (r.unit)
87 {
88 w.kv_str("u", r.unit);
89 }
90 // Every SenmlValueKind enumerator has a case below, so the default edge the compiler
91 // emits for the uint8_t-backed enum is unreachable for any value the API admits.
92 switch (r.value_kind) // GCOVR_EXCL_LINE exhaustive enum switch; the default edge is dead
93 {
94 case SenmlValueKind::SENML_V_FLOAT:
95 w.key("v");
96 json_num(w, r.value);
97 break;
98 case SenmlValueKind::SENML_V_STRING:
99 if (r.value_str)
100 {
101 w.kv_str("vs", r.value_str);
102 }
103 break;
104 case SenmlValueKind::SENML_V_BOOL:
105 w.kv_bool("vb", r.value_bool);
106 break;
107 case SenmlValueKind::SENML_V_NONE:
108 break;
109 }
110 if (r.has_time)
111 {
112 w.key("t");
113 json_num(w, r.time);
114 }
115 w.end_object();
116 }
117 w.end_array();
118 return w.ok() ? w.length() : 0;
119}
120
121// Emit a SenML number: an integer when integral (keeps timestamp precision), else a float.
122static void codec_num(const pc_codec *c, pc_span *w, double d)
123{
124 if (is_integral(d))
125 {
126 c->put_int(w, (int64_t)d);
127 }
128 else
129 {
130 c->put_float(w, (float)d);
131 }
132}
133
134static size_t record_fields(const SenmlRecord &r)
135{
136 size_t n = 0;
137 if (r.base_name)
138 {
139 n++;
140 }
141 if (r.has_base_time)
142 {
143 n++;
144 }
145 if (r.name)
146 {
147 n++;
148 }
149 if (r.unit)
150 {
151 n++;
152 }
153 if (r.value_kind != SenmlValueKind::SENML_V_NONE &&
154 !(r.value_kind == SenmlValueKind::SENML_V_STRING && !r.value_str))
155 {
156 n++;
157 }
158 if (r.has_time)
159 {
160 n++;
161 }
162 return n;
163}
164
165size_t pc_senml_build(const pc_codec *c, uint8_t *buf, size_t cap, const SenmlRecord *records, size_t count)
166{
167 if (!c || !buf || (count && !records))
168 {
169 return 0;
170 }
171 pc_span w{pc_span_from(buf, cap)};
172 c->put_array(&w, count);
173 for (size_t i = 0; i < count; i++)
174 {
175 const SenmlRecord &r = records[i];
176 c->put_map(&w, record_fields(r));
177 if (r.base_name)
178 {
179 c->put_label(&w, "bn", SENML_LBL_BN);
180 c->put_str(&w, r.base_name);
181 }
182 if (r.has_base_time)
183 {
184 c->put_label(&w, "bt", SENML_LBL_BT);
185 codec_num(c, &w, r.base_time);
186 }
187 if (r.name)
188 {
189 c->put_label(&w, "n", SENML_LBL_N);
190 c->put_str(&w, r.name);
191 }
192 if (r.unit)
193 {
194 c->put_label(&w, "u", SENML_LBL_U);
195 c->put_str(&w, r.unit);
196 }
197 // Exhaustive over SenmlValueKind, as in the JSON builder above: the compiler's default
198 // edge for the uint8_t-backed enum is unreachable, and record_fields() above is written
199 // against the same four kinds so the declared field count always matches what is emitted.
200 switch (r.value_kind) // GCOVR_EXCL_LINE exhaustive enum switch; the default edge is dead
201 {
202 case SenmlValueKind::SENML_V_FLOAT:
203 c->put_label(&w, "v", SENML_LBL_V);
204 codec_num(c, &w, r.value);
205 break;
206 case SenmlValueKind::SENML_V_STRING:
207 if (r.value_str)
208 {
209 c->put_label(&w, "vs", SENML_LBL_VS);
210 c->put_str(&w, r.value_str);
211 }
212 break;
213 case SenmlValueKind::SENML_V_BOOL:
214 c->put_label(&w, "vb", SENML_LBL_VB);
215 c->put_bool(&w, r.value_bool);
216 break;
217 case SenmlValueKind::SENML_V_NONE:
218 break;
219 }
220 if (r.has_time)
221 {
222 c->put_label(&w, "t", SENML_LBL_T);
223 codec_num(c, &w, r.time);
224 }
225 }
226 return pc_span_ok(w) ? pc_span_len(w) : 0;
227}
228
229// --- resolution (RFC 8428 ยง4.6) ---
230
231size_t pc_senml_resolve(const SenmlRecord *in, size_t n, SenmlResolved *out, size_t max)
232{
233 if (!in || !out)
234 {
235 return 0;
236 }
237 const char *base_name = nullptr; // the active base name (bn), carried forward
238 bool base_time_set = false;
239 double base_time = 0.0; // the active base time (bt)
240
241 size_t count = n < max ? n : max;
242 for (size_t i = 0; i < count; i++)
243 {
244 const SenmlRecord *r = &in[i];
245 if (r->base_name) // a base field becomes active for this record and the ones after it
246 {
247 base_name = r->base_name;
248 }
249 if (r->has_base_time)
250 {
251 base_time = r->base_time;
252 base_time_set = true;
253 }
254
255 SenmlResolved *o = &out[i];
256 // Resolved name = active base name + record name (either part may be absent).
257 pc_sb sb_name = {o->name, sizeof(o->name), 0, true};
258 pc_sb_put(&sb_name, base_name ? base_name : "");
259 pc_sb_put(&sb_name, r->name ? r->name : "");
260 int w = (int)pc_sb_finish(&sb_name);
261 if (!sb_name.ok)
262 {
263 o->name[0] = '\0'; // GCOVR_EXCL_LINE snprintf on "%s%s" cannot encode-error
264 }
265
266 // Resolved time = base time + record time (each defaults to 0); absent only if neither is present.
267 o->has_time = base_time_set || r->has_time;
268 o->time = (base_time_set ? base_time : 0.0) + (r->has_time ? r->time : 0.0);
269
270 o->unit = r->unit;
271 o->value_kind = r->value_kind;
272 o->value = r->value;
273 o->value_str = r->value_str;
274 o->value_bool = r->value_bool;
275 }
276 return count;
277}
278
279#endif // PC_ENABLE_SENML
Layer 6 (Presentation) - zero-heap CBOR (RFC 8949) encoder.
Builds a JSON document into a fixed caller buffer, no heap.
Definition json.h:59
void begin_object()
Open { (as a value/element where applicable).
Definition json.cpp:161
void key(const char *k)
Emit an object member name ("k":); follow with one value.
Definition json.cpp:178
void raw(const char *literal)
Emit a pre-formatted literal verbatim.
Definition json.cpp:234
void kv_bool(const char *k, bool v)
"k":true|false.
Definition json.cpp:255
void end_array()
Close ].
Definition json.cpp:173
void kv_str(const char *k, const char *v)
"k":"v" (escaped).
Definition json.cpp:240
bool ok() const
< False after any overflow / structural error.
Definition json.h:90
void end_object()
Close }.
Definition json.cpp:165
size_t length() const
< Bytes written so far (excludes the NUL).
Definition json.h:94
void begin_array()
Open [.
Definition json.cpp:169
One binary codec interface; a wire encoding is an instance of it.
Layer 6 (Presentation) - zero-heap JSON: a bounded writer and top-level reader.
SenML (RFC 8428) sensor-measurement pack builder (PC_ENABLE_SENML) - zero-heap SenML-JSON and SenML-C...
pc_span pc_span_from(uint8_t *p, size_t cap)
Bind a span to memory whose extent is only known at run time.
Definition span.h:92
bool pc_span_ok(const pc_span &s)
True when the span refers to real storage and every write so far has fit.
Definition span.h:114
size_t pc_span_len(const pc_span &s)
Bytes the payload needs - the backward direction.
Definition span.h:137
Bounded no-heap string builder that fails closed on overflow (one shared copy).
void pc_sb_g(pc_sb *b, double v, unsigned sig)
Append v with sig significant digits, choosing fixed or scientific form - the printf "%....
Definition strbuf.h:437
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_i64(pc_sb *b, int64_t v)
Append v as signed decimal (64-bit), with a leading '-' when negative.
Definition strbuf.h:315
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
A wire encoding: the ten writes, the peek, and the nine reads.
Definition codec.h:66
void(* put_label)(pc_span *w, const char *name, int64_t num)
Emit a map key, given both spellings of it.
Definition codec.h:87
void(* put_float)(pc_span *w, float f)
Definition codec.h:75
void(* put_array)(pc_span *w, size_t count)
Definition codec.h:76
void(* put_int)(pc_span *w, int64_t v)
Definition codec.h:69
void(* put_map)(pc_span *w, size_t count)
Definition codec.h:77
void(* put_bool)(pc_span *w, bool b)
Definition codec.h:73
void(* put_str)(pc_span *w, const char *s)
Definition codec.h:71
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
bool ok
Definition strbuf.h:34
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65