DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ntrip_caster_listener.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 ntrip_caster_listener.cpp
6 * @brief Server-side NTRIP caster listener (see ntrip_caster_listener.h). Answers PROTO_NTRIP_CASTER rover
7 * requests via the pure ntrip_caster codec and fans RTCM corrections out to subscribed rovers.
8 */
9
11
12#if DETWS_ENABLE_NTRIP_CASTER
13
16#include <string.h>
17
18namespace
19{
20// One published mountpoint on a listener.
21struct CasterMount
22{
23 bool active;
24 uint8_t listener_id;
25 char name[DETWS_NTRIP_MOUNT_MAX];
26 NtripMount cfg; // source-table description (string fields referenced from the caller)
27 const char *auth_b64; // required HTTP Basic credentials, or null for open access
28};
29
30// One rover connection: reading its request, then streaming a mountpoint.
31struct CasterRover
32{
33 bool active;
34 bool streaming;
35 uint8_t conn_slot;
36 int mount_idx; // index into s_ctx.mounts once streaming, else -1
37 char req[DETWS_NTRIP_REQ_MAX];
38 uint16_t req_len;
39};
40
41struct NtripCasterCtx
42{
43 CasterMount mounts[DETWS_NTRIP_MAX_MOUNTS];
44 CasterRover rovers[DETWS_NTRIP_MAX_ROVERS];
45 bool registered;
46};
47NtripCasterCtx s_ctx;
48
49CasterRover *rover_by_conn(uint8_t slot)
50{
51 for (int i = 0; i < DETWS_NTRIP_MAX_ROVERS; i++)
52 if (s_ctx.rovers[i].active && s_ctx.rovers[i].conn_slot == slot)
53 return &s_ctx.rovers[i];
54 return nullptr;
55}
56
57int rover_find_free()
58{
59 for (int i = 0; i < DETWS_NTRIP_MAX_ROVERS; i++)
60 if (!s_ctx.rovers[i].active)
61 return i;
62 return -1;
63}
64
65// Find a mount by name, optionally constrained to a given listener (-1 = any).
66int mount_index(const char *name, int listener_id)
67{
68 for (int i = 0; i < DETWS_NTRIP_MAX_MOUNTS; i++)
69 {
70 if (!s_ctx.mounts[i].active)
71 continue;
72 if (listener_id >= 0 && s_ctx.mounts[i].listener_id != (uint8_t)listener_id)
73 continue;
74 if (strcmp(s_ctx.mounts[i].name, name) == 0)
75 return i;
76 }
77 return -1;
78}
79
80// Count published mounts on a listener and write the matching NtripMount descriptors into out (for the
81// source table). Returns the count (bounded by cap).
82size_t mounts_on_listener(uint8_t listener_id, NtripMount *out, size_t cap)
83{
84 size_t k = 0;
85 for (int i = 0; i < DETWS_NTRIP_MAX_MOUNTS && k < cap; i++)
86 if (s_ctx.mounts[i].active && s_ctx.mounts[i].listener_id == listener_id)
87 out[k++] = s_ctx.mounts[i].cfg;
88 return k;
89}
90
91// Send a response and close the rover (a control reply is small; a single send is fine).
92void reply_and_close(CasterRover *r, const char *resp, size_t len)
93{
94 if (len && det_conn_active(r->conn_slot))
95 det_conn_send(r->conn_slot, resp, (u16_t)len);
96 r->active = false;
97 det_conn_close(r->conn_slot);
98}
99
100// Constant-length credential compare (auth strings are short and app-configured).
101bool auth_ok(const NtripRequest *req, const char *expect)
102{
103 if (!expect)
104 return true; // open access
105 if (!req->auth_b64)
106 return false;
107 size_t el = strnlen(expect, req->auth_b64_len + 1);
108 if (el != req->auth_b64_len)
109 return false;
110 return memcmp(req->auth_b64, expect, el) == 0;
111}
112
113void serve_sourcetable(CasterRover *r, NtripVersion version)
114{
115 NtripMount list[DETWS_NTRIP_MAX_MOUNTS];
116 size_t nm = mounts_on_listener(det_conn_listener_id(r->conn_slot), list, DETWS_NTRIP_MAX_MOUNTS);
117 char buf[DETWS_NTRIP_REQ_MAX + 256];
118 size_t n = ntrip_build_sourcetable(buf, sizeof(buf), version, list, nm);
119 reply_and_close(r, buf, n);
120}
121
122// A completed request has been parsed; dispatch it.
123void dispatch(CasterRover *r, const NtripRequest *req)
124{
125 char buf[192];
126 if (!req->is_get)
127 {
128 size_t n = ntrip_build_error_response(buf, sizeof(buf), req->version);
129 reply_and_close(r, buf, n);
130 return;
131 }
132 if (req->want_sourcetable)
133 {
134 serve_sourcetable(r, req->version);
135 return;
136 }
137 int mi = mount_index(req->mountpoint, (int)det_conn_listener_id(r->conn_slot));
138 if (mi < 0)
139 {
140 serve_sourcetable(r, req->version); // unknown mount -> advertise the available ones
141 return;
142 }
143 if (!auth_ok(req, s_ctx.mounts[mi].auth_b64))
144 {
145 size_t n = ntrip_build_unauthorized_response(buf, sizeof(buf), req->version);
146 reply_and_close(r, buf, n);
147 return;
148 }
149 size_t n = ntrip_build_stream_response(buf, sizeof(buf), req->version);
150 if (n == 0 || !det_conn_active(r->conn_slot) || !det_conn_send(r->conn_slot, buf, (u16_t)n))
151 {
152 r->active = false;
153 det_conn_close(r->conn_slot);
154 return;
155 }
156 r->streaming = true;
157 r->mount_idx = mi; // corrections for this mount now fan out to this rover
158}
159
160void caster_on_accept(uint8_t slot)
161{
162 int idx = rover_find_free();
163 if (idx < 0)
164 {
165 det_conn_close(slot); // rover table full
166 return;
167 }
168 CasterRover *r = &s_ctx.rovers[idx];
169 r->active = true;
170 r->streaming = false;
171 r->conn_slot = slot;
172 r->mount_idx = -1;
173 r->req_len = 0;
174}
175
176void caster_on_data(uint8_t slot)
177{
178 CasterRover *r = rover_by_conn(slot);
179 if (!r)
180 {
181 det_conn_close(slot);
182 return;
183 }
184 if (r->streaming)
185 {
186 // A streaming rover may send periodic GGA; the base already knows its position, so drain & ignore.
187 uint8_t sink[64];
188 while (det_conn_available(slot))
189 if (det_conn_read(slot, sink, sizeof(sink)) == 0)
190 break;
191 return;
192 }
193 // Still reading the request: append what is available, then try to parse.
194 while (det_conn_available(slot) && r->req_len < sizeof(r->req) - 1)
195 {
196 size_t got = det_conn_read(slot, (uint8_t *)r->req + r->req_len, sizeof(r->req) - 1 - r->req_len);
197 if (got == 0)
198 break;
199 r->req_len += (uint16_t)got;
200 }
201 NtripRequest req;
202 if (ntrip_request_parse(r->req, r->req_len, &req))
203 {
204 dispatch(r, &req);
205 return;
206 }
207 if (r->req_len >= sizeof(r->req) - 1) // request too large without a header terminator
208 {
209 char buf[64];
210 size_t n = ntrip_build_error_response(buf, sizeof(buf), NtripVersion::NTRIP_V1);
211 reply_and_close(r, buf, n);
212 }
213}
214
215void caster_on_close(uint8_t slot)
216{
217 CasterRover *r = rover_by_conn(slot);
218 if (r)
219 r->active = false; // the transport owns the closing slot
220}
221
222const ProtoHandler s_caster_handler = {caster_on_accept, caster_on_data, caster_on_close, nullptr};
223
224} // namespace
225
226bool det_ntrip_caster_add_mount(uint8_t listener_id, const NtripMount *mount, const char *auth_b64)
227{
228 if (!mount || !mount->mountpoint)
229 return false;
230 size_t nl = strnlen(mount->mountpoint, DETWS_NTRIP_MOUNT_MAX + 1);
231 if (nl == 0 || nl >= DETWS_NTRIP_MOUNT_MAX)
232 return false;
233 int idx = -1;
234 for (int i = 0; i < DETWS_NTRIP_MAX_MOUNTS; i++)
235 if (!s_ctx.mounts[i].active)
236 {
237 idx = i;
238 break;
239 }
240 if (idx < 0)
241 return false;
242 CasterMount *m = &s_ctx.mounts[idx];
243 m->active = true;
244 m->listener_id = listener_id;
245 memcpy(m->name, mount->mountpoint, nl + 1);
246 m->cfg = *mount;
247 m->cfg.mountpoint = m->name; // point the copied cfg at our owned name
248 m->auth_b64 = auth_b64;
249 if (!s_ctx.registered)
250 {
252 s_ctx.registered = true;
253 }
254 return true;
255}
256
257int det_ntrip_caster_broadcast(const char *mountpoint, const uint8_t *data, size_t len)
258{
259 if (!mountpoint || !data || len == 0)
260 return 0;
261 int mi = mount_index(mountpoint, -1);
262 if (mi < 0)
263 return 0;
264 int sent = 0;
265 for (int i = 0; i < DETWS_NTRIP_MAX_ROVERS; i++)
266 {
267 CasterRover *r = &s_ctx.rovers[i];
268 if (!r->active || !r->streaming || r->mount_idx != mi)
269 continue;
270 if (!det_conn_active(r->conn_slot))
271 continue;
272 if (det_conn_send(r->conn_slot, data, (u16_t)len))
273 sent++;
274 }
275 return sent;
276}
277
278int det_ntrip_caster_subscriber_count(const char *mountpoint)
279{
280 if (!mountpoint)
281 return 0;
282 int mi = mount_index(mountpoint, -1);
283 if (mi < 0)
284 return 0;
285 int n = 0;
286 for (int i = 0; i < DETWS_NTRIP_MAX_ROVERS; i++)
287 if (s_ctx.rovers[i].active && s_ctx.rovers[i].streaming && s_ctx.rovers[i].mount_idx == mi)
288 n++;
289 return n;
290}
291
292void det_ntrip_caster_reset(void)
293{
294 for (int i = 0; i < DETWS_NTRIP_MAX_MOUNTS; i++)
295 s_ctx.mounts[i].active = false;
296 for (int i = 0; i < DETWS_NTRIP_MAX_ROVERS; i++)
297 s_ctx.rovers[i].active = false;
298}
299
300#endif // DETWS_ENABLE_NTRIP_CASTER
@ PROTO_NTRIP_CASTER
NTRIP caster (DETWS_ENABLE_NTRIP_CASTER): serves RTCM3 corrections to rovers.
#define DETWS_NTRIP_REQ_MAX
Max NTRIP client request size (bytes) the caster buffers while reading the request headers.
#define DETWS_NTRIP_MAX_ROVERS
Max concurrent rover connections a caster serves corrections to (services/gnss).
#define DETWS_NTRIP_MOUNT_MAX
Max length (incl. NUL) of an NTRIP mountpoint name the caster serves.
#define DETWS_NTRIP_MAX_MOUNTS
Max distinct mountpoints a single caster serves (each = one RTCM stream).
Server-side NTRIP caster listener (DETWS_ENABLE_NTRIP_CASTER): the ConnProto::PROTO_NTRIP_CASTER hand...
Layer 5 (Session) - per-protocol connection handler dispatch table.
void proto_register(ConnProto proto, const ProtoHandler *h)
Register h for protocol proto (replaces any previous handler).
Definition session.cpp:38
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:362
void det_conn_close(uint8_t slot)
Close connection slot gracefully (tcp_close), aborting if the FIN cannot be queued....
Definition tcp.cpp:467
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.