DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
gateway.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 gateway.cpp
6 * @brief Radio / wireless gateway bridge - implementation.
7 *
8 * A static port table; det_gw_uplink() envelopes a received frame and publishes it through
9 * the installed northbound callback (per-port rate-capped, fail-closed), det_gw_downlink()
10 * routes a command to a port's transmit callback, and det_gw_topic() formats a routing key.
11 * Zero heap.
12 */
13
15
16#if DETWS_ENABLE_GATEWAY
17
18#include <string.h>
19
20#ifdef ARDUINO
21#include "services/clock.h" // detws_millis()
22#endif
23
24namespace
25{
26struct port
27{
28 det_gw_tx_fn tx;
29 void *ctx;
30 uint32_t window_start; // ms of the current uplink rate window
31 uint16_t rate_cap; // uplink frames per second (0 = unlimited)
32 uint16_t count; // uplinks in the current window
33 uint8_t id;
34 det_gw_kind kind;
35 bool used;
36};
37
38// All gateway state, owned by one instance (internal linkage): the port table, the
39// northbound uplink callback + context, the topic prefix, the uplink sequence, and stats,
40// grouped so it is one named owner, unreachable from any other translation unit.
41struct GatewayCtx
42{
43 port ports[DETWS_GW_MAX_PORTS];
44 det_gw_uplink_fn uplink = nullptr;
45 void *uplink_ctx = nullptr;
46 const char *prefix = DETWS_GW_DEFAULT_PREFIX;
47 uint32_t seq = 0;
48 det_gw_stats stats;
49#ifndef ARDUINO
50 uint32_t now_ms = 0; // host test clock (real builds use detws_millis())
51#endif
52};
53GatewayCtx s_gw;
54
55#ifdef ARDUINO
56uint32_t gw_now()
57{
58 return detws_millis();
59}
60#else
61uint32_t gw_now()
62{
63 return s_gw.now_ms;
64}
65#endif
66
67// Returns a mutable port (callers mutate it), so it takes the owner by non-const reference.
68port *find_port(GatewayCtx &g, uint8_t id)
69{
70 for (uint8_t i = 0; i < DETWS_GW_MAX_PORTS; i++)
71 if (g.ports[i].used && g.ports[i].id == id)
72 return &g.ports[i];
73 return nullptr;
74}
75
76// Fixed 1-second window uplink rate cap; fail-closed (true = drop) once the cap is hit.
77bool rate_exceeded(port *p)
78{
79 if (p->rate_cap == 0)
80 return false;
81 uint32_t now = gw_now();
82 if ((uint32_t)(now - p->window_start) >= 1000)
83 {
84 p->window_start = now;
85 p->count = 0;
86 }
87 if (p->count >= p->rate_cap)
88 return true;
89 p->count++;
90 return false;
91}
92
93bool put_ch(char *buf, uint16_t *pos, uint16_t cap, char c)
94{
95 if ((uint16_t)(*pos + 1) >= cap) // keep room for the NUL
96 return false;
97 buf[(*pos)++] = c;
98 return true;
99}
100
101bool put_u32(char *buf, uint16_t *pos, uint16_t cap, uint32_t v)
102{
103 char tmp[10];
104 uint8_t n = 0;
105 if (v == 0)
106 tmp[n++] = '0';
107 while (v)
108 {
109 tmp[n++] = (char)('0' + (v % 10));
110 v /= 10;
111 }
112 while (n > 0)
113 if (!put_ch(buf, pos, cap, tmp[--n]))
114 return false;
115 return true;
116}
117} // namespace
118
119void det_gw_reset(void)
120{
121 memset(s_gw.ports, 0, sizeof(s_gw.ports));
122 s_gw.uplink = nullptr;
123 s_gw.uplink_ctx = nullptr;
124 s_gw.prefix = DETWS_GW_DEFAULT_PREFIX;
125 s_gw.seq = 0;
126 memset(&s_gw.stats, 0, sizeof(s_gw.stats));
127}
128
129bool det_gw_add_port(const det_gw_port_config *cfg)
130{
131 if (!cfg || find_port(s_gw, cfg->port_id))
132 return false;
133 for (uint8_t i = 0; i < DETWS_GW_MAX_PORTS; i++)
134 {
135 if (s_gw.ports[i].used)
136 continue;
137 s_gw.ports[i].tx = cfg->tx;
138 s_gw.ports[i].ctx = cfg->ctx;
139 s_gw.ports[i].window_start = 0;
140 s_gw.ports[i].rate_cap = cfg->rate_cap;
141 s_gw.ports[i].count = 0;
142 s_gw.ports[i].id = cfg->port_id;
143 s_gw.ports[i].kind = cfg->kind;
144 s_gw.ports[i].used = true;
145 return true;
146 }
147 return false; // table full
148}
149
150void det_gw_set_uplink(det_gw_uplink_fn fn, void *ctx)
151{
152 s_gw.uplink = fn;
153 s_gw.uplink_ctx = ctx;
154}
155
156void det_gw_set_topic_prefix(const char *prefix)
157{
158 s_gw.prefix = prefix ? prefix : DETWS_GW_DEFAULT_PREFIX;
159}
160
161bool det_gw_uplink(uint8_t port_id, uint16_t src_addr, const uint8_t *payload, uint16_t len, int16_t rssi)
162{
163 s_gw.stats.up_in++;
164 port *p = find_port(s_gw, port_id);
165 if (!p || !s_gw.uplink || rate_exceeded(p))
166 {
167 s_gw.stats.up_dropped++;
168 return false;
169 }
170 det_gw_msg msg;
171 msg.payload = payload;
172 msg.seq = s_gw.seq++;
173 msg.len = len;
174 msg.src_addr = src_addr;
175 msg.rssi = rssi;
176 msg.port_id = port_id;
177 msg.kind = p->kind;
178 if (s_gw.uplink(&msg, s_gw.uplink_ctx))
179 {
180 s_gw.stats.up_published++;
181 return true;
182 }
183 s_gw.stats.up_dropped++;
184 return false;
185}
186
187bool det_gw_downlink(uint8_t port_id, uint16_t dst_addr, const uint8_t *payload, uint16_t len)
188{
189 s_gw.stats.down_in++;
190 port *p = find_port(s_gw, port_id);
191 if (!p || !p->tx || !p->tx(port_id, dst_addr, payload, len, p->ctx))
192 {
193 s_gw.stats.down_dropped++;
194 return false;
195 }
196 s_gw.stats.down_sent++;
197 return true;
198}
199
200uint16_t det_gw_topic(const det_gw_msg *msg, char *buf, uint16_t buflen)
201{
202 if (!msg || !buf || buflen == 0)
203 return 0;
204 uint16_t pos = 0;
205 for (const char *s = s_gw.prefix; *s; s++)
206 if (!put_ch(buf, &pos, buflen, *s))
207 return 0;
208 // Sequential, not one `||` chain: the two '/' separators are written at different
209 // positions (put_ch advances pos), so writing them as separate steps keeps each
210 // append distinct rather than repeating an identical-looking subexpression.
211 if (!put_ch(buf, &pos, buflen, '/'))
212 return 0;
213 if (!put_u32(buf, &pos, buflen, msg->port_id))
214 return 0;
215 if (!put_ch(buf, &pos, buflen, '/'))
216 return 0;
217 if (!put_u32(buf, &pos, buflen, msg->src_addr))
218 return 0;
219 buf[pos] = '\0';
220 return pos;
221}
222
223void det_gw_get_stats(det_gw_stats *out)
224{
225 if (out)
226 *out = s_gw.stats;
227}
228
229#if !defined(ARDUINO)
230void det_gw_test_set_now(uint32_t ms)
231{
232 s_gw.now_ms = ms;
233}
234#endif
235
236#endif // DETWS_ENABLE_GATEWAY
#define DETWS_GW_MAX_PORTS
Max southbound gateway ports (radios / buses; static-allocated).
#define DETWS_GW_DEFAULT_PREFIX
Default northbound topic prefix (overridable at runtime via det_gw_set_topic_prefix).
Pluggable monotonic clock for all library timing.
uint32_t detws_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
Radio / wireless gateway bridge (DETWS_ENABLE_GATEWAY) - the v5 southbound-to- northbound bridge.