ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
client.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 client.cpp
6 * @brief Layer 4 outbound TCP client transport (pooled). See client.h.
7 *
8 * Mirrors the server transport's cross-thread rule: every raw lwIP call runs in
9 * tcpip_thread via tcpip_api_call(). Each slot owns its pcb and an SPSC wire ring
10 * (producer = the lwIP recv callback in tcpip_thread; consumer = the caller's
11 * loop/blocking task). The rings use `volatile` indices, matching the shipped
12 * per-client implementations this consolidates.
13 */
14
15#include "client.h"
16
17// Compiles only on Arduino AND only when a client transport is actually enabled
18// (HTTP client / MQTT / WS client). A server-only build leaves DNS_RESOLVER off,
19// so the resolver symbols this unit calls would not be declared - see
20// PC_NEED_CLIENT in protocore_config.h.
21#if defined(ARDUINO) && PC_NEED_CLIENT
22
23#include "diffserv.h" // DiffServ DSCP marking for outbound client connections (compiles out when off)
24#include "lwip/priv/tcpip_priv.h"
25#include "lwip/tcp.h"
26#include "services/net/dns_resolver/dns_resolver.h" // shared host->IP resolve (one DNS owner)
27#include "services/system/clock.h" // pc_millis()
28#include "shared_primitives/ring.h" // shared pc_atomic + SPSC ring drain (same primitive as the server)
29#include <Arduino.h> // millis()
30#include <string.h>
31
32struct ClientConn
33{
34 struct tcp_pcb *pcb;
35 volatile bool in_use;
36 volatile bool connected;
37 volatile bool closed; // peer FIN or error
38 uint8_t rx[PC_CLIENT_RX_BUF];
39 pc_atomic<size_t> head; // producer (lwIP recv cb); acquire/release SPSC, same as the server ring
40 pc_atomic<size_t> tail; // consumer (caller)
41};
42
43// Outbound client connection pool, owned by one instance (internal linkage): the per-slot
44// ClientConn state. One named owner, unreachable from any other translation unit.
45struct pc_client_ctx
46{
47 ClientConn cc[PC_CLIENT_CONNS];
48};
49static pc_client_ctx s_client;
50
51// Hostname resolution is delegated to the shared DNS resolver (pc_dns_resolver_resolve,
52// services/net/dns_resolver) so there is one owner of the gethostbyname-marshal +
53// deadline-poll pattern instead of a private copy here.
54
55// --- lwIP callbacks (tcpip_thread); arg = the owning ClientConn* -------------
56
57static err_t cc_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
58{
59 (void)err;
60 ClientConn *c = (ClientConn *)arg;
61 if (!c)
62 {
63 return ERR_OK;
64 }
65 if (p == nullptr)
66 {
67 c->closed = true; // peer closed
68 return ERR_OK;
69 }
70 // Wire bytes -> ring via the shared producer primitive (same as the server): if
71 // the whole segment will not fit, refuse it (lwIP retains + redelivers); else
72 // bulk-memcpy each pbuf span and publish head once.
73 (void)tpcb;
74 if (p->tot_len > pc_ring_free(c->head, c->tail, PC_CLIENT_RX_BUF))
75 {
76 return ERR_MEM;
77 }
78 size_t h = c->head; // sole producer of head; advance a local and publish once
79 for (struct pbuf *q = p; q; q = q->next)
80 {
81 h = pc_ring_write_span(c->rx, PC_CLIENT_RX_BUF, h, (const uint8_t *)q->payload, q->len);
82 }
83 c->head = h; // single release store publishes the whole segment
84 // Do NOT tcp_recved() here. The window is reopened by pc_client_read() as the
85 // caller drains (ack-on-consume), so it tracks ring occupancy and the peer can
86 // never overflow the ring - same model as the server transport. ACKing on copy
87 // would decouple the window from drainage and deadlock a large inbound transfer
88 // once PC_CLIENT_RX_BUF < TCP_WND.
89 pbuf_free(p);
90 return ERR_OK;
91}
92
93static err_t cc_connected(void *arg, struct tcp_pcb *tpcb, err_t err)
94{
95 (void)tpcb;
96 ClientConn *c = (ClientConn *)arg;
97 if (c)
98 {
99 if (err == ERR_OK)
100 {
101 c->connected = true;
102 }
103 else
104 {
105 c->closed = true;
106 }
107 }
108 return ERR_OK;
109}
110
111static void cc_err(void *arg, err_t err)
112{
113 (void)err;
114 ClientConn *c = (ClientConn *)arg;
115 if (c)
116 {
117 c->pcb = nullptr; // lwIP already freed it
118 c->closed = true;
119 }
120}
121
122// --- tcpip_thread-marshaled ops ---------------------------------------------
123
124struct CcConnCall
125{
126 struct tcpip_api_call_data base;
127 ClientConn *c;
128 ip_addr_t addr;
129 uint16_t port;
130 err_t result;
131};
132struct CcSendCall
133{
134 struct tcpip_api_call_data base;
135 ClientConn *c;
136 const void *data;
137 u16_t len;
138 err_t result;
139};
140struct CcRecvedCall
141{
142 struct tcpip_api_call_data base;
143 ClientConn *c;
144 u16_t len;
145};
146
147static err_t cc_do_connect(struct tcpip_api_call_data *cd)
148{
149 CcConnCall *k = (CcConnCall *)cd;
150 ClientConn *c = k->c;
151 c->pcb = tcp_new_ip_type(IPADDR_TYPE_V4);
152 if (!c->pcb)
153 {
154 k->result = ERR_MEM;
155 return ERR_OK;
156 }
157 tcp_arg(c->pcb, c);
158 tcp_recv(c->pcb, cc_recv);
159 tcp_err(c->pcb, cc_err);
160#if PC_ENABLE_DIFFSERV
161 {
162 // Mark the outbound connection with the server-wide default DSCP (the SYN onward). Runs in
163 // tcpip_thread (this is the marshalled connect op), so touching the pcb is race-free.
164 uint8_t dscp = pc_diffserv_default_dscp();
165 if (dscp)
166 {
167 c->pcb->tos = pc_dscp_to_tos(dscp);
168 }
169 }
170#endif
171 k->result = tcp_connect(c->pcb, &k->addr, k->port, cc_connected);
172 return ERR_OK;
173}
174
175static err_t cc_do_send(struct tcpip_api_call_data *cd)
176{
177 CcSendCall *k = (CcSendCall *)cd;
178 ClientConn *c = k->c;
179 if (!c->pcb)
180 {
181 k->result = ERR_CONN;
182 return ERR_OK;
183 }
184 k->result = tcp_write(c->pcb, k->data, k->len, TCP_WRITE_FLAG_COPY);
185 if (k->result == ERR_OK)
186 {
187 tcp_output(c->pcb);
188 }
189 return ERR_OK;
190}
191
192static err_t cc_do_close(struct tcpip_api_call_data *cd)
193{
194 CcSendCall *k = (CcSendCall *)cd;
195 ClientConn *c = k->c;
196 if (c->pcb)
197 {
198 tcp_arg(c->pcb, nullptr);
199 tcp_recv(c->pcb, nullptr);
200 tcp_err(c->pcb, nullptr);
201 if (tcp_close(c->pcb) != ERR_OK)
202 {
203 tcp_abort(c->pcb);
204 }
205 c->pcb = nullptr;
206 }
207 return ERR_OK;
208}
209
210static err_t cc_do_recved(struct tcpip_api_call_data *cd)
211{
212 CcRecvedCall *k = (CcRecvedCall *)cd;
213 if (k->c->pcb)
214 {
215 tcp_recved(k->c->pcb, k->len); // reopen the window by the consumed bytes
216 }
217 return ERR_OK;
218}
219
220// --- public API --------------------------------------------------------------
221
222int pc_client_open(const char *host, uint16_t port, uint32_t timeout_ms)
223{
224 int cid = -1;
225 for (int i = 0; i < PC_CLIENT_CONNS; i++)
226 {
227 if (!s_client.cc[i].in_use)
228 {
229 cid = i;
230 break;
231 }
232 }
233 if (cid < 0)
234 {
235 return -1; // pool full
236 }
237
238 ClientConn *c = &s_client.cc[cid];
239 c->pcb = nullptr;
240 c->connected = false;
241 c->closed = false;
242 c->head = 0;
243 c->tail = 0;
244 c->in_use = true;
245
246 // Resolve through the shared DNS owner (its own PC_DNS_TIMEOUT_MS budget),
247 // then give the connect its full timeout_ms.
248 uint32_t ip = 0;
249 if (!pc_dns_resolver_resolve(host, &ip))
250 {
251 c->in_use = false;
252 return -2; // DNS failure
253 }
254 uint32_t deadline = pc_millis() + timeout_ms;
255
256 CcConnCall k;
257 memset(&k, 0, sizeof(k));
258 k.c = c;
259 IP_ADDR4(&k.addr, (uint8_t)(ip >> 24), (uint8_t)(ip >> 16), (uint8_t)(ip >> 8), (uint8_t)ip);
260 k.port = port;
261 tcpip_api_call(cc_do_connect, &k.base);
262 if (k.result != ERR_OK)
263 {
264 pc_client_close(cid);
265 return -3; // connect issue
266 }
267 while (!c->connected && !c->closed && (int32_t)(deadline - pc_millis()) > 0)
268 {
269 pcdelay(5);
270 }
271 if (!c->connected)
272 {
273 pc_client_close(cid);
274 return -4; // connect timeout / refused
275 }
276 return cid;
277}
278
279bool pc_client_connected(int cid)
280{
281 return cid >= 0 && cid < PC_CLIENT_CONNS && s_client.cc[cid].in_use && s_client.cc[cid].connected &&
282 !s_client.cc[cid].closed;
283}
284
285bool pc_client_is_closed(int cid)
286{
287 if (cid < 0 || cid >= PC_CLIENT_CONNS)
288 {
289 return true;
290 }
291 return s_client.cc[cid].closed;
292}
293
294bool pc_client_send(int cid, const void *data, size_t len)
295{
296 if (cid < 0 || cid >= PC_CLIENT_CONNS || !s_client.cc[cid].in_use)
297 {
298 return false;
299 }
300 CcSendCall k;
301 memset(&k, 0, sizeof(k));
302 k.c = &s_client.cc[cid];
303 k.data = data;
304 k.len = (u16_t)(len > 0xFFFF ? 0xFFFF : len);
305 tcpip_api_call(cc_do_send, &k.base);
306 return k.result == ERR_OK;
307}
308
309size_t pc_client_available(int cid)
310{
311 if (cid < 0 || cid >= PC_CLIENT_CONNS)
312 {
313 return 0;
314 }
315 ClientConn *c = &s_client.cc[cid];
316 return pc_ring_available(c->head, c->tail, PC_CLIENT_RX_BUF);
317}
318
319size_t pc_client_read(int cid, uint8_t *buf, size_t cap)
320{
321 if (cid < 0 || cid >= PC_CLIENT_CONNS)
322 {
323 return 0;
324 }
325 ClientConn *c = &s_client.cc[cid];
326 size_t n = pc_ring_read(c->rx, PC_CLIENT_RX_BUF, c->head, c->tail, buf, cap);
327 if (n > 0 && c->pcb)
328 {
329 // Ack-on-consume: reopen the receive window by exactly what we just drained.
330 CcRecvedCall k;
331 memset(&k, 0, sizeof(k));
332 k.c = c;
333 k.len = (u16_t)n;
334 tcpip_api_call(cc_do_recved, &k.base);
335 }
336 return n;
337}
338
339void pc_client_close(int cid)
340{
341 if (cid < 0 || cid >= PC_CLIENT_CONNS || !s_client.cc[cid].in_use)
342 {
343 return;
344 }
345 CcSendCall k;
346 memset(&k, 0, sizeof(k));
347 k.c = &s_client.cc[cid];
348 tcpip_api_call(cc_do_close, &k.base);
349 s_client.cc[cid].in_use = false;
350}
351
352#else // !ARDUINO - host stub (the clients are ARDUINO-only; host builds no-op)
353
354int pc_client_open(const char *, uint16_t, uint32_t)
355{
356 return -1;
357}
359{
360 return false;
361}
363{
364 return true;
365}
366bool pc_client_send(int, const void *, size_t)
367{
368 return false;
369}
371{
372 return 0;
373}
374size_t pc_client_read(int, uint8_t *, size_t)
375{
376 return 0;
377}
379{
380 // no-op: the native stub owns no socket to close
381}
382
383#endif // ARDUINO
#define PC_CLIENT_RX_BUF
Definition c2_defaults.h:56
bool pc_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:366
size_t pc_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:374
bool pc_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:362
int pc_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
Definition client.cpp:354
size_t pc_client_available(int)
Wire bytes currently buffered and ready to read.
Definition client.cpp:370
bool pc_client_connected(int)
True once the TCP handshake has completed for cid.
Definition client.cpp:358
void pc_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:378
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Pluggable monotonic clock for all library timing.
void pcdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Definition clock.h:89
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
Layer 4 (Transport) - DiffServ QoS marking (RFC 2474) for outbound traffic.
DNS resolver with answer verification (PC_ENABLE_DNS_RESOLVER).
#define PC_CLIENT_CONNS
Reverse-SSH tunnel: max concurrent forwarded-tcpip channels bridged at once. A relay that forwards to...
Shared single-producer / single-consumer byte-ring primitive.