ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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; pc_gateway_uplink() envelopes a received frame and publishes it through
9 * the installed northbound callback (per-port rate-capped, fail-closed), pc_gateway_downlink()
10 * routes a command to a port's transmit callback, and pc_gateway_topic() formats a routing key.
11 * Zero heap.
12 */
13
15
16#if PC_ENABLE_GATEWAY
17
18#include <string.h>
19
20#ifdef ARDUINO
21#include "services/system/clock.h" // pc_millis()
22#endif
23
24namespace
25{
26struct port
27{
28 pc_gateway_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 pc_gateway_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[PC_GW_MAX_PORTS];
44 pc_gateway_uplink_fn uplink = nullptr;
45 void *uplink_ctx = nullptr;
46 const char *prefix = PC_GW_DEFAULT_PREFIX;
47 uint32_t seq = 0;
48 pc_gateway_stats stats;
49#ifndef ARDUINO
50 uint32_t now_ms = 0; // host test clock (real builds use pc_millis())
51#endif
52};
53GatewayCtx s_gw;
54
55#ifdef ARDUINO
56uint32_t gw_now()
57{
58 return pc_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 < PC_GW_MAX_PORTS; i++)
71 {
72 if (g.ports[i].used && g.ports[i].id == id)
73 {
74 return &g.ports[i];
75 }
76 }
77 return nullptr;
78}
79
80// Fixed 1-second window uplink rate cap; fail-closed (true = drop) once the cap is hit.
81bool rate_exceeded(port *p)
82{
83 if (p->rate_cap == 0)
84 {
85 return false;
86 }
87 uint32_t now = gw_now();
88 if ((uint32_t)(now - p->window_start) >= 1000)
89 {
90 p->window_start = now;
91 p->count = 0;
92 }
93 if (p->count >= p->rate_cap)
94 {
95 return true;
96 }
97 p->count++;
98 return false;
99}
100
101bool put_ch(char *buf, uint16_t *pos, uint16_t cap, char c)
102{
103 if ((uint16_t)(*pos + 1) >= cap) // keep room for the NUL
104 {
105 return false;
106 }
107 buf[(*pos)++] = c;
108 return true;
109}
110
111bool put_u32(char *buf, uint16_t *pos, uint16_t cap, uint32_t v)
112{
113 char tmp[10];
114 uint8_t n = 0;
115 if (v == 0)
116 {
117 tmp[n++] = '0';
118 }
119 while (v)
120 {
121 tmp[n++] = (char)('0' + (v % 10));
122 v /= 10;
123 }
124 while (n > 0)
125 {
126 if (!put_ch(buf, pos, cap, tmp[--n]))
127 {
128 return false;
129 }
130 }
131 return true;
132}
133} // namespace
134
135void pc_gateway_reset(void)
136{
137 memset(s_gw.ports, 0, sizeof(s_gw.ports));
138 s_gw.uplink = nullptr;
139 s_gw.uplink_ctx = nullptr;
140 s_gw.prefix = PC_GW_DEFAULT_PREFIX;
141 s_gw.seq = 0;
142 memset(&s_gw.stats, 0, sizeof(s_gw.stats));
143}
144
145bool pc_gateway_add_port(const pc_gateway_port_config *cfg)
146{
147 if (!cfg || find_port(s_gw, cfg->port_id))
148 {
149 return false;
150 }
151 for (uint8_t i = 0; i < PC_GW_MAX_PORTS; i++)
152 {
153 if (s_gw.ports[i].used)
154 {
155 continue;
156 }
157 s_gw.ports[i].tx = cfg->tx;
158 s_gw.ports[i].ctx = cfg->ctx;
159 s_gw.ports[i].window_start = 0;
160 s_gw.ports[i].rate_cap = cfg->rate_cap;
161 s_gw.ports[i].count = 0;
162 s_gw.ports[i].id = cfg->port_id;
163 s_gw.ports[i].kind = cfg->kind;
164 s_gw.ports[i].used = true;
165 return true;
166 }
167 return false; // table full
168}
169
170void pc_gateway_set_uplink_cb(pc_gateway_uplink_fn fn, void *ctx)
171{
172 s_gw.uplink = fn;
173 s_gw.uplink_ctx = ctx;
174}
175
176void pc_gateway_set_topic_prefix(const char *prefix)
177{
178 s_gw.prefix = prefix ? prefix : PC_GW_DEFAULT_PREFIX;
179}
180
181bool pc_gateway_uplink(uint8_t port_id, uint16_t src_addr, const uint8_t *payload, uint16_t len, int16_t rssi)
182{
183 s_gw.stats.up_in++;
184 port *p = find_port(s_gw, port_id);
185 if (!p || !s_gw.uplink || rate_exceeded(p))
186 {
187 s_gw.stats.up_dropped++;
188 return false;
189 }
190 pc_gateway_msg msg;
191 msg.payload = payload;
192 msg.seq = s_gw.seq++;
193 msg.len = len;
194 msg.src_addr = src_addr;
195 msg.rssi = rssi;
196 msg.port_id = port_id;
197 msg.kind = p->kind;
198 if (s_gw.uplink(&msg, s_gw.uplink_ctx))
199 {
200 s_gw.stats.up_published++;
201 return true;
202 }
203 s_gw.stats.up_dropped++;
204 return false;
205}
206
207bool pc_gateway_downlink(uint8_t port_id, uint16_t dst_addr, const uint8_t *payload, uint16_t len)
208{
209 s_gw.stats.down_in++;
210 port *p = find_port(s_gw, port_id);
211 if (!p || !p->tx || !p->tx(port_id, dst_addr, payload, len, p->ctx))
212 {
213 s_gw.stats.down_dropped++;
214 return false;
215 }
216 s_gw.stats.down_sent++;
217 return true;
218}
219
220uint16_t pc_gateway_topic(const pc_gateway_msg *msg, char *buf, uint16_t buflen)
221{
222 if (!msg || !buf || buflen == 0)
223 {
224 return 0;
225 }
226 uint16_t pos = 0;
227 for (const char *s = s_gw.prefix; *s; s++)
228 {
229 if (!put_ch(buf, &pos, buflen, *s))
230 {
231 return 0;
232 }
233 }
234 // Sequential, not one `||` chain: the two '/' separators are written at different
235 // positions (put_ch advances pos), so writing them as separate steps keeps each
236 // append distinct rather than repeating an identical-looking subexpression.
237 if (!put_ch(buf, &pos, buflen, '/'))
238 {
239 return 0;
240 }
241 if (!put_u32(buf, &pos, buflen, msg->port_id))
242 {
243 return 0;
244 }
245 if (!put_ch(buf, &pos, buflen, '/'))
246 {
247 return 0;
248 }
249 if (!put_u32(buf, &pos, buflen, msg->src_addr))
250 {
251 return 0;
252 }
253 buf[pos] = '\0';
254 return pos;
255}
256
257void pc_gateway_get_stats(pc_gateway_stats *out)
258{
259 if (out)
260 {
261 *out = s_gw.stats;
262 }
263}
264
265#if !defined(ARDUINO)
266void pc_gateway_test_set_now(uint32_t ms)
267{
268 s_gw.now_ms = ms;
269}
270#endif
271
272#endif // PC_ENABLE_GATEWAY
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
Radio / wireless gateway bridge (PC_ENABLE_GATEWAY) - the v5 southbound-to- northbound bridge.
#define PC_GW_MAX_PORTS
Max southbound gateway ports (radios / buses; static-allocated).
#define PC_GW_DEFAULT_PREFIX
Default northbound topic prefix (overridable at runtime via pc_gateway_set_topic_prefix).