ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
udp_telemetry.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 udp_telemetry.cpp
6 * @brief InfluxDB line-protocol builder (pure) + UDP cast to a collector.
7 *
8 * The builder is host-tested; the cast uses pc_udp_sendto on ESP32 and is a
9 * no-op on host builds (no transport dependency pulled into the unit test).
10 */
11
13#include "shared_primitives/strbuf.h" // pc_sb frame builder
14
15#if PC_ENABLE_UDP_TELEMETRY
16
17#include <stdio.h>
18#include <string.h>
19
20// ---------------------------------------------------------------------------
21// Line builder (pure)
22// ---------------------------------------------------------------------------
23
24#if defined(ARDUINO)
26#endif
27static void line_append(pc_line *l, const char *s)
28{
29 if (l->overflow)
30 {
31 return;
32 }
33 size_t n = strnlen(s, l->cap + 1);
34 if (l->pos + n >= l->cap) // keep room for the null terminator
35 {
36 l->overflow = true;
37 return;
38 }
39 memcpy(l->buf + l->pos, s, n);
40 l->pos += n;
41 l->buf[l->pos] = '\0';
42}
43
44// Separator before a field: a space before the first, a comma after.
45static void line_sep(pc_line *l)
46{
47 line_append(l, l->have_fields ? "," : " ");
48 l->have_fields = true;
49}
50
51void pc_line_init(pc_line *l, char *buf, size_t cap, const char *measurement)
52{
53 l->buf = buf;
54 l->cap = cap;
55 l->pos = 0;
56 l->overflow = false;
57 l->have_fields = false;
58 if (cap)
59 {
60 buf[0] = '\0';
61 }
62 line_append(l, measurement ? measurement : "");
63}
64
65// Append a string with InfluxDB tag/key escaping: comma, equals and space are
66// backslash-escaped (line protocol, "Special characters").
67static void line_append_escaped(pc_line *l, const char *s)
68{
69 if (!s)
70 {
71 return;
72 }
73 for (const char *p = s; *p; p++)
74 {
75 if (*p == ',' || *p == '=' || *p == ' ')
76 {
77 char esc[3] = {'\\', *p, '\0'};
78 line_append(l, esc);
79 }
80 else
81 {
82 char one[2] = {*p, '\0'};
83 line_append(l, one);
84 }
85 }
86}
87
88void pc_line_add_tag(pc_line *l, const char *key, const char *val)
89{
90 // Tags are part of the series key: they come right after the measurement,
91 // comma-separated, BEFORE the space-separated fields. Adding one after a field
92 // is a misuse -> fail the line closed.
93 if (l->have_fields)
94 {
95 l->overflow = true;
96 return;
97 }
98 line_append(l, ",");
99 line_append_escaped(l, key);
100 line_append(l, "=");
101 line_append_escaped(l, val);
102}
103
104void pc_line_set_timestamp(pc_line *l, int64_t timestamp)
105{
106 if (!l->have_fields) // a line needs at least one field before the timestamp
107 {
108 l->overflow = true;
109 return;
110 }
111 char num[24];
112 pc_sb sb_num = {num, sizeof(num), 0, true};
113 pc_sb_put(&sb_num, " ");
114 pc_sb_i64(&sb_num, (int64_t)((long long)timestamp));
115 if (pc_sb_finish(&sb_num) == 0)
116 {
117 num[0] = '\0'; // space-separated trailing timestamp
118 }
119 line_append(l, num);
120}
121
122void pc_line_add_int(pc_line *l, const char *field, int64_t v)
123{
124 char num[24];
125 pc_sb sb_num2 = {num, sizeof(num), 0, true};
126 pc_sb_i64(&sb_num2, (int64_t)((long long)v));
127 pc_sb_put(&sb_num2, "i");
128 if (pc_sb_finish(&sb_num2) == 0)
129 {
130 num[0] = '\0'; // InfluxDB integer suffix
131 }
132 line_sep(l);
133 line_append(l, field);
134 line_append(l, "=");
135 line_append(l, num);
136}
137
138void pc_line_add_uint(pc_line *l, const char *field, uint64_t v)
139{
140 char num[24];
141 pc_sb sb_num3 = {num, sizeof(num), 0, true};
142 pc_sb_u64(&sb_num3, (uint64_t)((unsigned long long)v));
143 pc_sb_put(&sb_num3, "u");
144 if (pc_sb_finish(&sb_num3) == 0)
145 {
146 num[0] = '\0'; // InfluxDB UInteger suffix 'u' (not signed 'i')
147 }
148 line_sep(l);
149 line_append(l, field);
150 line_append(l, "=");
151 line_append(l, num);
152}
153
154void pc_line_add_float(pc_line *l, const char *field, float v, uint8_t decimals)
155{
156 char num[32];
157 pc_sb sb_num4 = {num, sizeof(num), 0, true};
158 pc_sb_fixed(&sb_num4, (double)((double)v), (unsigned)((int)decimals));
159 if (pc_sb_finish(&sb_num4) == 0)
160 {
161 num[0] = '\0';
162 }
163 line_sep(l);
164 line_append(l, field);
165 line_append(l, "=");
166 line_append(l, num);
167}
168
169size_t pc_line_len(const pc_line *l)
170{
171 return l->pos;
172}
173
174bool pc_line_ok(const pc_line *l)
175{
176 return !l->overflow && l->have_fields;
177}
178
179// ---------------------------------------------------------------------------
180// Cast
181// ---------------------------------------------------------------------------
182
183#ifdef ARDUINO
184
185namespace
186{
187// All UDP-telemetry cast state, owned by one instance (internal linkage): the collector
188// endpoint and the begun flag, grouped so it is one named owner, unreachable cross-TU.
189struct UdpTelemetryCtx
190{
191 char ip[16] = {0};
192 uint16_t port = 0;
193 bool begun = false;
194};
195UdpTelemetryCtx s_ut;
196} // namespace
197
198void pc_udp_telemetry_begin(const char *collector_ip, uint16_t port)
199{
200 strncpy(s_ut.ip, collector_ip ? collector_ip : "", sizeof(s_ut.ip) - 1);
201 s_ut.ip[sizeof(s_ut.ip) - 1] = '\0';
202 s_ut.port = port;
203 s_ut.begun = true;
204}
205
206bool pc_udp_telemetry_send(const char *data, size_t len)
207{
208 if (!s_ut.begun || !data)
209 {
210 return false;
211 }
212 return pc_udp_sendto(s_ut.ip, s_ut.port, (const uint8_t *)data, len);
213}
214
215#else // host build - no network
216
217void pc_udp_telemetry_begin(const char *, uint16_t)
218{
219}
220
221bool pc_udp_telemetry_send(const char *, size_t)
222{
223 return false;
224}
225
226#endif // ARDUINO
227
228bool pc_udp_telemetry_cast(const pc_line *l)
229{
230 if (!pc_line_ok(l))
231 {
232 return false;
233 }
234 return pc_udp_telemetry_send(l->buf, l->pos);
235}
236
237#endif // PC_ENABLE_UDP_TELEMETRY
Bounded no-heap string builder that fails closed on overflow (one shared copy).
void pc_sb_fixed(pc_sb *b, double v, unsigned decimals)
Append v with exactly decimals digits after the point (printf "%.<decimals>f").
Definition strbuf.h:565
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_u64(pc_sb *b, uint64_t v)
Append v as decimal (64-bit).
Definition strbuf.h:309
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
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
bool pc_udp_sendto(const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
Send a UDP datagram to an arbitrary destination (outbound client).
Definition udp.cpp:358
Layer 4 (Transport) - connectionless UDP datagram service.
Fire-and-forget UDP telemetry cast (PC_ENABLE_UDP_TELEMETRY).