DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
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 det_udp_sendto on ESP32 and is a
9 * no-op on host builds (no transport dependency pulled into the unit test).
10 */
11
13
14#if DETWS_ENABLE_UDP_TELEMETRY
15
16#include <stdio.h>
17#include <string.h>
18
19// ---------------------------------------------------------------------------
20// Line builder (pure)
21// ---------------------------------------------------------------------------
22
23static void line_append(DetwsLine *l, const char *s)
24{
25 if (l->overflow)
26 return;
27 size_t n = strnlen(s, l->cap + 1);
28 if (l->pos + n >= l->cap) // keep room for the null terminator
29 {
30 l->overflow = true;
31 return;
32 }
33 memcpy(l->buf + l->pos, s, n);
34 l->pos += n;
35 l->buf[l->pos] = '\0';
36}
37
38// Separator before a field: a space before the first, a comma after.
39static void line_sep(DetwsLine *l)
40{
41 line_append(l, l->have_fields ? "," : " ");
42 l->have_fields = true;
43}
44
45void detws_line_init(DetwsLine *l, char *buf, size_t cap, const char *measurement)
46{
47 l->buf = buf;
48 l->cap = cap;
49 l->pos = 0;
50 l->overflow = false;
51 l->have_fields = false;
52 if (cap)
53 buf[0] = '\0';
54 line_append(l, measurement ? measurement : "");
55}
56
57// Append a string with InfluxDB tag/key escaping: comma, equals and space are
58// backslash-escaped (line protocol, "Special characters").
59static void line_append_escaped(DetwsLine *l, const char *s)
60{
61 if (!s)
62 return;
63 for (const char *p = s; *p; p++)
64 {
65 if (*p == ',' || *p == '=' || *p == ' ')
66 {
67 char esc[3] = {'\\', *p, '\0'};
68 line_append(l, esc);
69 }
70 else
71 {
72 char one[2] = {*p, '\0'};
73 line_append(l, one);
74 }
75 }
76}
77
78void detws_line_add_tag(DetwsLine *l, const char *key, const char *val)
79{
80 // Tags are part of the series key: they come right after the measurement,
81 // comma-separated, BEFORE the space-separated fields. Adding one after a field
82 // is a misuse -> fail the line closed.
83 if (l->have_fields)
84 {
85 l->overflow = true;
86 return;
87 }
88 line_append(l, ",");
89 line_append_escaped(l, key);
90 line_append(l, "=");
91 line_append_escaped(l, val);
92}
93
94void detws_line_set_timestamp(DetwsLine *l, int64_t timestamp)
95{
96 if (!l->have_fields) // a line needs at least one field before the timestamp
97 {
98 l->overflow = true;
99 return;
100 }
101 char num[24];
102 snprintf(num, sizeof(num), " %lld", (long long)timestamp); // space-separated trailing timestamp
103 line_append(l, num);
104}
105
106void detws_line_add_int(DetwsLine *l, const char *field, int64_t v)
107{
108 char num[24];
109 snprintf(num, sizeof(num), "%lldi", (long long)v); // InfluxDB integer suffix
110 line_sep(l);
111 line_append(l, field);
112 line_append(l, "=");
113 line_append(l, num);
114}
115
116void detws_line_add_uint(DetwsLine *l, const char *field, uint64_t v)
117{
118 char num[24];
119 snprintf(num, sizeof(num), "%lluu", (unsigned long long)v); // InfluxDB UInteger suffix 'u' (not signed 'i')
120 line_sep(l);
121 line_append(l, field);
122 line_append(l, "=");
123 line_append(l, num);
124}
125
126void detws_line_add_float(DetwsLine *l, const char *field, float v, uint8_t decimals)
127{
128 char num[32];
129 snprintf(num, sizeof(num), "%.*f", (int)decimals, (double)v);
130 line_sep(l);
131 line_append(l, field);
132 line_append(l, "=");
133 line_append(l, num);
134}
135
136size_t detws_line_len(const DetwsLine *l)
137{
138 return l->pos;
139}
140
141bool detws_line_ok(const DetwsLine *l)
142{
143 return !l->overflow && l->have_fields;
144}
145
146// ---------------------------------------------------------------------------
147// Cast
148// ---------------------------------------------------------------------------
149
150#ifdef ARDUINO
151
153
154namespace
155{
156// All UDP-telemetry cast state, owned by one instance (internal linkage): the collector
157// endpoint and the begun flag, grouped so it is one named owner, unreachable cross-TU.
158struct UdpTelemetryCtx
159{
160 char ip[16] = {0};
161 uint16_t port = 0;
162 bool begun = false;
163};
164UdpTelemetryCtx s_ut;
165} // namespace
166
167void detws_udp_telemetry_begin(const char *collector_ip, uint16_t port)
168{
169 strncpy(s_ut.ip, collector_ip ? collector_ip : "", sizeof(s_ut.ip) - 1);
170 s_ut.ip[sizeof(s_ut.ip) - 1] = '\0';
171 s_ut.port = port;
172 s_ut.begun = true;
173}
174
175bool detws_udp_telemetry_send(const char *data, size_t len)
176{
177 if (!s_ut.begun || !data)
178 return false;
179 return det_udp_sendto(s_ut.ip, s_ut.port, (const uint8_t *)data, len);
180}
181
182#else // host build - no network
183
184void detws_udp_telemetry_begin(const char *, uint16_t)
185{
186}
187
188bool detws_udp_telemetry_send(const char *, size_t)
189{
190 return false;
191}
192
193#endif // ARDUINO
194
195bool detws_udp_telemetry_cast(const DetwsLine *l)
196{
197 if (!detws_line_ok(l))
198 return false;
199 return detws_udp_telemetry_send(l->buf, l->pos);
200}
201
202#endif // DETWS_ENABLE_UDP_TELEMETRY
bool det_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:196
Layer 4 (Transport) - connectionless UDP datagram service.
Fire-and-forget UDP telemetry cast (DETWS_ENABLE_UDP_TELEMETRY).