DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
tcp.h
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 tcp.h
6 * @brief Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
7 *
8 * Defines the static connection pool and the per-connection event plumbing.
9 * Each listener port owns its own FreeRTOS queue (see listener.h); the
10 * session layer drains all active queues each tick via server_tick().
11 *
12 * **Concurrency model**
13 * | Context | Reads | Writes |
14 * |------------------|------------------------|-------------------------|
15 * | lwIP callbacks | rx_head (to check full)| rx_buffer[], rx_head |
16 * | Arduino loop | rx_buffer[], rx_tail | rx_tail |
17 *
18 * `state`, `rx_head`, and `rx_tail` are `DetAtomic` (acquire/release): the
19 * single-producer / single-consumer ring buffer is correct without a mutex
20 * because the release store of an index publishes the preceding buffer writes
21 * and the acquire load observes them, on either core.
22 *
23 * **Backpressure (lossless)**
24 * When a whole inbound segment will not fit the free ring space, the recv
25 * callback refuses it (returns ERR_MEM without freeing the pbuf); lwIP retains
26 * it as refused_data and redelivers once the main loop has drained the ring, so
27 * no received byte is dropped. Requires RX_BUF_SIZE > one TCP segment (TCP_MSS).
28 *
29 * @author Douglas Quigg (dstroy0)
30 * @date 2026
31 */
32
33#ifndef DETERMINISTICESPASYNCWEBSERVER_TRANSPORT_H
34#define DETERMINISTICESPASYNCWEBSERVER_TRANSPORT_H
35
36#include "ServerConfig.h"
37#include "freertos/FreeRTOS.h"
38#include "freertos/queue.h"
39#include "lwip/tcp.h"
40#include "network_drivers/network/ip.h" // DetIp (family-tagged peer address)
41#include "shared_primitives/ring.h" // DetAtomic + the shared SPSC ring drain primitive
42#include <Arduino.h>
43#include <atomic>
44
45// ---------------------------------------------------------------------------
46// Connection state
47// ---------------------------------------------------------------------------
48
49/**
50 * @brief Lifecycle state of a connection pool slot.
51 *
52 * Transitions:
53 * - `ConnState::CONN_FREE → ConnState::CONN_ACTIVE` : accept callback fires.
54 * - `ConnState::CONN_ACTIVE → ConnState::CONN_FREE` : graceful close, error, or timeout.
55 * - `ConnState::CONN_ACTIVE → ConnState::CONN_CLOSING` : (reserved for future half-close support).
56 */
57enum class ConnState : uint8_t
58{
59 CONN_FREE, ///< Slot is available; no PCB is attached.
60 CONN_ACTIVE, ///< Live connection; PCB is valid.
61 CONN_CLOSING ///< FIN sent; waiting for final ACK (reserved).
62};
63
64/**
65 * @brief A single TCP connection context.
66 *
67 * Sized so that `MAX_CONNS` instances fit in a static array without
68 * fragmentation. All fields except the volatile ring-buffer indices may
69 * only be accessed from the Arduino main-loop task.
70 */
71struct TcpConn
72{
73 uint8_t id; ///< Fixed slot index (0 … MAX_CONNS-1).
74 DetAtomic<ConnState> state; ///< Lifecycle state; acquire/release for inter-task visibility.
75 struct tcp_pcb *pcb; ///< lwIP PCB; null when slot is free.
76 uint32_t last_activity_ms; ///< `millis()` timestamp of last TX/RX event.
77
78 uint8_t rx_buffer[RX_BUF_SIZE]; ///< Ring buffer storage.
79 DetAtomic<size_t> rx_head; ///< Producer write index (lwIP/tcpip context).
80 DetAtomic<size_t> rx_tail; ///< Consumer read index (worker context).
81 size_t rx_acked; ///< rx_tail position last ACKed to lwIP (tcp_recved). Worker-only:
82 ///< the window is reopened by exactly the bytes drained since, so it
83 ///< tracks ring occupancy (ack-on-consume) rather than copy.
84
85 uint8_t listener_id; ///< Index into listener_pool[]; set at accept time.
86 uint8_t owner; ///< Worker that owns this slot (round-robin at accept). Always 0 at N=1.
87 ConnProto proto; ///< Application protocol for this connection.
88 uint8_t
89 proto_slot; ///< Per-protocol session/pool index (0xFF = none): the SSH session, an MQTT/Modbus session, etc.
90 DetIface iface; ///< Interface this connection arrived on; set at accept time.
91 uint8_t tls; ///< Non-zero when this connection is TLS (set at accept time).
92#if DETWS_ENABLE_HTTP2 || DETWS_ENABLE_HTTP3
93 /// Self-framing protocol response sink (Layer 5 TX seam): HTTP/2 installs it at ALPN, HTTP/3 at
94 /// dispatch, so the response methods route through it instead of building an HTTP/1.1 message.
95 /// Null means plain HTTP/1.1 (the default builder). Extends the ProtoHandler seam to the TX side.
96 bool (*resp_sink)(uint8_t slot, int code, const char *content_type, const char *body, size_t len);
97#endif
98#if DETWS_ENABLE_HTTP2
99 uint8_t h2; ///< Non-zero once this connection negotiated HTTP/2 (ALPN "h2").
100 uint8_t h2_checked; ///< The post-handshake ALPN check ran (once per connection).
101 uint32_t h2_stream; ///< Stream id of the request currently being dispatched (for the response).
102#endif
103#if DETWS_ENABLE_HTTP3
104 uint8_t h3; ///< Non-zero when this is the reserved HTTP/3 dispatch slot (no TCP pcb).
105 uint32_t h3_conn_id; ///< quic_server connection id the response routes back to.
106 uint64_t h3_stream; ///< HTTP/3 request stream id the response is written on.
107#endif
108};
109
110/** @brief Sentinel for TcpConn::proto_slot meaning "no per-protocol session bound". */
111#define DETWS_PROTO_SLOT_NONE 0xFFu
112
113/**
114 * @brief softAP IPv4 address (network byte order) for STA/AP interface tagging.
115 *
116 * Zero when no softAP is configured. Set via DetWebServer::set_ap_ip(); the
117 * accept callback tags each connection DetIface::DETIFACE_AP when its local IP equals
118 * this, else DetIface::DETIFACE_STA. Used by per-route interface filters.
119 */
120extern uint32_t detws_ap_ip;
121
122/** @brief Static pool of connection contexts. Defined in tcp.cpp.
123 * Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots plus any reserved internal dispatch slot(s)
124 * (HTTP/3); the TCP accept path only ever uses [0, MAX_CONNS). */
126
127// ---------------------------------------------------------------------------
128// Event queue
129// ---------------------------------------------------------------------------
130
131/**
132 * @brief Type of connection event posted to a listener's FreeRTOS queue.
133 */
134enum class EvtType : uint8_t
135{
136 EVT_CONNECT, ///< New connection accepted.
137 EVT_DATA, ///< Data received; bytes are already in the ring buffer.
138 EVT_DISCONNECT, ///< Remote peer closed the connection gracefully.
139 EVT_ERROR ///< lwIP reported an error (PCB may already be freed).
140};
141
142/**
143 * @brief Event record posted from lwIP callbacks to the session layer.
144 *
145 * Small enough (≤12 bytes on 32-bit) that the FreeRTOS queue copies it by
146 * value - no pointer lifetime issues.
147 */
148struct TcpEvt
149{
150 EvtType type; ///< What happened.
151 uint8_t slot_id; ///< Which connection slot is affected.
152 size_t data_len; ///< Bytes copied (EvtType::EVT_DATA only); 0 for other types.
153};
154
155// ---------------------------------------------------------------------------
156// DeterministicAsyncTCP
157// ---------------------------------------------------------------------------
158
159/**
160 * @class DeterministicAsyncTCP
161 * @brief Static-only facade managing the shared TCP connection pool.
162 *
163 * All state is class-level (no instances). pool_init() initializes the
164 * connection pool and the runtime timeout config once per boot (or per
165 * restart cycle). Listening sockets and per-listener queues are owned by
166 * the listener layer (see listener.h); this class only manages the shared
167 * conn_pool[] and the idle-timeout sweep.
168 */
170{
171 public:
172 /**
173 * @brief Initialize the connection pool and store the runtime config.
174 *
175 * Zeroes all connection slots and sets conn_timeout_ms from @p cfg.
176 * Call this before calling listener_add() for each port.
177 *
178 * @param cfg Optional runtime config. Pass nullptr to use compile-time
179 * defaults (CONN_TIMEOUT_MS).
180 */
181 static void pool_init(const WebServerConfig *cfg = nullptr);
182
183 /**
184 * @brief Abort all active connections and reset the pool to ConnState::CONN_FREE.
185 *
186 * Does not touch listener PCBs or listener queues - call listener_stop_all()
187 * before this if you also want to close the listening sockets.
188 * Safe to call from the main-loop task.
189 */
190 static void stop();
191
192 /**
193 * @brief Scan the pool and force-close connections idle for > conn_timeout_ms.
194 *
195 * Called at the start of every server_tick() call, before any event queue
196 * is drained. Uses `tcp_abort()` (not `tcp_close()`) because the
197 * connection has already timed out and a graceful FIN is not warranted.
198 *
199 * A timed-out slot has its state set to `ConnState::CONN_FREE` and `pcb` cleared
200 * *before* `tcp_abort()` is called, so any in-flight lwIP callback for
201 * that PCB will see `slot->state != ConnState::CONN_ACTIVE` and exit without
202 * touching the slot.
203 *
204 * Only sweeps slots owned by @p worker_id, so each worker reaps just its own
205 * connections (no cross-worker writes). At DETWS_WORKER_COUNT=1 every slot is
206 * owned by worker 0, so it sweeps the whole pool as before.
207 */
208 static void check_timeouts(int worker_id = 0);
209
210 /**
211 * @brief Runtime connection-idle timeout in milliseconds.
212 *
213 * Loaded from WebServerConfig::conn_timeout_ms at pool_init() time.
214 * Defaults to CONN_TIMEOUT_MS if no config is supplied.
215 */
216 static uint32_t conn_timeout_ms;
217};
218
219// ---------------------------------------------------------------------------
220// Connection output API (defined in tcp.cpp)
221// ---------------------------------------------------------------------------
222// The one send/flush/close path for all higher layers. Presentation (WebSocket,
223// SSE, SSH) and the HTTP application call these instead of touching lwIP, so the
224// transport layer stays the sole owner of TCP I/O. det_conn_send/flush are
225// TLS-aware (route through the TLS record layer when the slot is a TLS conn);
226// with DETWS_ENABLE_TLS off they are byte-identical to tcp_write/tcp_output.
227
228/**
229 * @brief Send @p len bytes on connection @p slot (copies @p data; TLS-aware).
230 * @return true if the bytes were queued; false if the send buffer was full and
231 * the write was refused. A streaming producer should pace with
232 * det_conn_sndbuf() and resume on a later loop; existing fixed-size
233 * senders may ignore the result.
234 */
235bool det_conn_send(uint8_t slot, const void *data, u16_t len);
236
237/**
238 * @brief Send @p len bytes on @p slot and flush in a single tcpip_thread round-trip.
239 *
240 * The terminal-write analogue of det_conn_send(): the tcp_write and its tcp_output() run inside
241 * one marshaled op, so a small single-shot response costs one ~23 us on-device marshal instead of
242 * the det_conn_send()+det_conn_flush() pair (two). Use for the LAST write of a response whose body
243 * is already fully buffered (send / send_empty / redirect); a streaming producer that pages across
244 * loops must keep using det_conn_send() + a single trailing det_conn_flush(). TLS-identical to
245 * det_conn_send (the record BIO already outputs per record). Same return contract as det_conn_send.
246 */
247bool det_conn_send_flush(uint8_t slot, const void *data, u16_t len);
248
249/**
250 * @brief Bytes that can currently be queued for sending on @p slot.
251 *
252 * Advisory free space in the TCP send buffer: a producer can send at most this
253 * many bytes per handle() loop and resume on the next loop as the window drains
254 * (the on_poll hook is the natural resume point). For a TLS slot the usable
255 * plaintext is somewhat less (TLS record + cipher overhead). Returns 0 when
256 * the slot has no pcb.
257 */
258u16_t det_conn_sndbuf(uint8_t slot);
259
260/** @brief Flush queued bytes / finish the send on @p slot (TLS-aware). */
261void det_conn_flush(uint8_t slot);
262
263/**
264 * @brief Refresh @p slot's idle-timeout timestamp while a response body is in flight.
265 *
266 * The file/chunk send pumps call this each poll they run: a slot still paging out a body is
267 * actively streaming (or briefly blocked on a full window / a transient link stall), not idle,
268 * so the CONN_TIMEOUT_MS idle sweep must not reap it mid-transfer - that truncates any body
269 * larger than one TCP window. A genuinely dead peer is still reclaimed by lwIP's retransmission
270 * timers (err callback), not this sweep.
271 */
272void det_conn_touch_active(uint8_t slot);
273
274/**
275 * @brief Reopen the TCP receive window by however much @p slot has drained.
276 *
277 * Ack-on-consume, owned entirely by the transport layer: recv_cb no longer
278 * ACKs on copy. Instead the window tracks how much the application has actually
279 * drained from the ring (rx_tail) since the last ACK, so it never advertises more
280 * than the ring can hold and the peer is paced to the consumer; a slow sink (e.g.
281 * flash writes during a streamed upload) can never overflow the ring and deadlock.
282 *
283 * Other layers do not touch the ring indices to manage flow control - the worker
284 * just calls this once per owned slot per loop and transport does the rest
285 * (computes the delta vs rx_acked, marshals tcp_recved to tcpip_thread, advances
286 * rx_acked). A no-op when nothing was drained or @p slot is not active.
287 */
288void det_conn_ack_consumed(uint8_t slot);
289
290// ---------------------------------------------------------------------------
291// RX ring read API - the single way any layer drains received bytes.
292//
293// Transport owns the ring; consumers (HTTP/WS/Telnet/SSH/TLS and the framed
294// services) must never index rx_buffer or advance rx_tail themselves - they call
295// these. Consuming functions advance rx_tail only; the window is reopened by the
296// worker's det_conn_ack_consumed() once per loop (one owner, no per-byte ACK).
297// Single-consumer per slot (the owning worker), so no locking here. These are
298// inline because the byte path is hot and the ring internals live in this header.
299// ---------------------------------------------------------------------------
300
301// All five delegate to the shared SPSC ring primitive (ring.h) over the slot's
302// rx_buffer - the server transport never reimplements the ring math.
303
304/** @brief Bytes currently available to read from @p slot's ring. */
305static inline size_t det_conn_available(uint8_t slot)
306{
307 const TcpConn *c = &conn_pool[slot];
308 return det_ring_available(c->rx_head, c->rx_tail, RX_BUF_SIZE);
309}
310
311/** @brief Pop one byte into @p out; false if the ring is empty. */
312static inline bool det_conn_read_byte(uint8_t slot, uint8_t *out)
313{
314 TcpConn *c = &conn_pool[slot];
315 return det_ring_read_byte(c->rx_buffer, RX_BUF_SIZE, c->rx_head, c->rx_tail, out);
316}
317
318/** @brief Copy @p n bytes at @p off from the tail into @p dst WITHOUT consuming (lookahead). */
319static inline void det_conn_peek(uint8_t slot, size_t off, uint8_t *dst, size_t n)
320{
321 const TcpConn *c = &conn_pool[slot];
322 det_ring_peek(c->rx_buffer, RX_BUF_SIZE, c->rx_tail, off, dst, n);
323}
324
325/** @brief Drop @p n bytes from the tail (advance past already-peeked data). */
326static inline void det_conn_consume(uint8_t slot, size_t n)
327{
328 det_ring_consume(conn_pool[slot].rx_tail, RX_BUF_SIZE, n);
329}
330
331/** @brief Pop up to @p cap bytes into @p buf; returns the count read. */
332static inline size_t det_conn_read(uint8_t slot, uint8_t *buf, size_t cap)
333{
334 TcpConn *c = &conn_pool[slot];
335 return det_ring_read(c->rx_buffer, RX_BUF_SIZE, c->rx_head, c->rx_tail, buf, cap);
336}
337
338/**
339 * @brief True if @p slot holds a live connection that can accept a send or close.
340 *
341 * The single predicate every layer uses to ask "is this slot sendable": it folds the
342 * CONN_ACTIVE state check and the non-null pcb check the send / flush / close paths
343 * require. Callers outside transport/ + tls/ must NOT test conn_pool[slot].state or
344 * .pcb themselves - .pcb is a raw lwIP pointer, so poking it couples a higher layer to
345 * the transport's internals. Guard a send with `if (!det_conn_active(slot)) return;`.
346 */
347static inline bool det_conn_active(uint8_t slot)
348{
349 const TcpConn *c = &conn_pool[slot];
350 return c->state == ConnState::CONN_ACTIVE && c->pcb != nullptr;
351}
352
353/** @brief The network interface (STA / AP / ANY) @p slot's connection arrived on. */
354static inline DetIface det_conn_iface(uint8_t slot)
355{
356 return conn_pool[slot].iface;
357}
358
359/** @brief The id of the listener @p slot's connection was accepted on. */
360static inline uint8_t det_conn_listener_id(uint8_t slot)
361{
362 return conn_pool[slot].listener_id;
363}
364
365/**
366 * @brief Number of server connection slots currently in the CONN_ACTIVE state.
367 *
368 * The connection pool owns this aggregate: callers (stats / metrics) ask transport for the
369 * count instead of sweeping conn_pool[] and testing .state themselves.
370 */
371uint8_t det_conn_active_count();
372
373/**
374 * @brief Write raw bytes straight to @p pcb (no TLS), context-safe.
375 *
376 * This is the one safe path for the TLS engine's BIO to emit ciphertext: it
377 * writes directly when already running inside the lwIP thread (the marshaled
378 * app-data send path) and marshals a raw write when called from the main-loop
379 * task (the handshake / read pump), so a TLS handshake never does an
380 * unsynchronized tcp_write from the main loop. Calls tcp_output() on success.
381 * @return true if the bytes were queued; false on a full send buffer (ERR_MEM).
382 */
383bool det_conn_raw_send(struct tcp_pcb *pcb, const void *data, u16_t len);
384
385/**
386 * @brief Close connection @p slot gracefully (tcp_close), aborting if the FIN
387 * cannot be queued. The transport owns the whole teardown: it detaches the
388 * pcb from its lwIP callbacks, frees the slot, and (TLS slot) emits
389 * close_notify + frees the per-connection TLS context - so callers pass
390 * only the slot and never touch the raw tcp_pcb. A no-op if the slot has
391 * no live pcb.
392 */
393void det_conn_close(uint8_t slot);
394
395/**
396 * @brief Begin a graceful close that dwells in ConnState::CONN_CLOSING until the peer ACKs.
397 *
398 * Unlike det_conn_close() (immediate teardown), this leaves the slot's PCB and
399 * callbacks live and moves it ACTIVE -> ConnState::CONN_CLOSING. The slot finalizes (PCB
400 * closed, slot freed) from the sent callback once the response has fully drained,
401 * or from the idle sweep after DETWS_CLOSING_TIMEOUT_MS if the peer never ACKs.
402 * The caller must already have queued (det_conn_send) + flushed the response.
403 * A no-op if the slot is not ConnState::CONN_ACTIVE (e.g. an error freed it mid-write).
404 */
405void det_conn_begin_close(uint8_t slot_id);
406
407/** @brief Detach @p pcb from its slot's lwIP callbacks before the slot is freed. */
408void det_conn_detach(struct tcp_pcb *pcb);
409
410/** @brief Hard-abort @p pcb (RST) for a fatal condition; no graceful FIN. */
411void det_conn_abort(struct tcp_pcb *pcb);
412
413/**
414 * @brief Hard-abort connection @p slot (RST) for a fatal condition. The transport
415 * owns the teardown order: free the per-connection TLS context (abrupt, no
416 * close_notify), detach the pcb from its callbacks, reset the slot, then
417 * abort - so callers pass only the slot and never touch the raw tcp_pcb. A
418 * no-op if the slot has no live pcb.
419 */
420void det_conn_abort_slot(uint8_t slot);
421
422/**
423 * @brief Raw source IPv4 of the connection in @p slot, or 0 if the slot has no
424 * active pcb (or on host builds). Byte order is irrelevant: this is an
425 * identity key (e.g. for the auth lockout), not for display. Keeps the
426 * lwIP pcb access inside L4 so callers never reach into the pcb directly.
427 */
428uint32_t det_conn_remote_ip(uint8_t slot);
429
430/**
431 * @brief The connected peer's address as a family-tagged ::DetIp (IPv4 or IPv6).
432 *
433 * Unlike det_conn_remote_ip() (which flattens to a v4 uint32 and cannot represent a v6 peer),
434 * this reports the real address for a dual-stack build (DETWS_ENABLE_IPV6). Format it with
435 * det_ip_format() or classify it with det_ip_classify().
436 * @return true if @p slot has an active connection whose address was written to @p out.
437 */
438bool det_conn_remote_addr(uint8_t slot, DetIp *out);
439
440/**
441 * @brief A stable per-peer 32-bit identity key for @p slot (the v4 address, or an FNV-1a hash of a
442 * v6 address). For rate-limit / auth-lockout buckets, where a v6 peer must not silently
443 * share the all-zero v4 bucket. Returns 0 if the slot has no active connection.
444 */
445#ifdef ARDUINO
446/**
447 * @brief Convert a raw lwIP address to the portable family-tagged DetIp - for the accept callback,
448 * which has the pcb but no connection slot yet. ESP32 only (the parameter is an lwIP type).
449 */
450void det_lwip_to_detip(const ip_addr_t *ra, DetIp *out);
451#endif
452
453// ---------------------------------------------------------------------------
454// Observability (DETWS_ENABLE_OBSERVABILITY) - connection event hook + counters
455// ---------------------------------------------------------------------------
456#if DETWS_ENABLE_OBSERVABILITY
457
458/** @brief Why a connection event fired (the reason for a transition or notice). */
459enum class DetConnReason : uint8_t
460{
461 DET_CONN_R_ACCEPT, ///< New connection accepted (ConnState::CONN_FREE -> ConnState::CONN_ACTIVE).
462 DET_CONN_R_CLOSE_REMOTE, ///< Peer closed gracefully (FIN received).
463 DET_CONN_R_CLOSE_LOCAL, ///< Application initiated the close.
464 DET_CONN_R_ERROR, ///< lwIP reported a fatal error on the connection.
465 DET_CONN_R_TIMEOUT, ///< Idle-timeout sweep reaped the slot.
466 DET_CONN_R_ABORT, ///< Forced abort (server stop / pool reset).
467 DET_CONN_R_DRAINED, ///< ConnState::CONN_CLOSING slot finished draining -> closed.
468 DET_CONN_R_BACKPRESSURE, ///< RX segment refused (ring full); no state change.
469 DET_CONN_R_DEFER_DROP ///< Event queue full; an event was dropped (no state change).
470};
471
472/** @brief Snapshot of the transport's lifetime counters (plus a live gauge). */
473struct DetConnCounters
474{
475 uint32_t accepts; ///< Connections accepted.
476 uint32_t closes_remote; ///< Closed by peer FIN.
477 uint32_t closes_local; ///< Closed by the application.
478 uint32_t closes_error; ///< Closed by an lwIP error.
479 uint32_t closes_timeout; ///< Reaped by the idle-timeout sweep.
480 uint32_t closes_abort; ///< Force-aborted (stop / reset).
481 uint32_t backpressure; ///< RX segments refused for lack of ring space.
482 uint32_t defer_drops; ///< Deferred events dropped because the queue was full.
483 uint32_t closing_gauge; ///< Slots currently in ConnState::CONN_CLOSING (live, not cumulative).
484};
485
486/**
487 * @brief Callback fired on every connection state transition.
488 *
489 * Runs in whichever task drove the transition (tcpip_thread for accept / recv /
490 * error, a worker for close / timeout), so keep it short and non-blocking and do
491 * not call back into the server from it. @p old_state == @p new_state for the
492 * non-transition notices (backpressure, defer-drop).
493 */
494typedef void (*DetConnEventCb)(uint8_t slot, ConnState old_state, ConnState new_state, DetConnReason reason);
495
496/** @brief Register (or clear, with nullptr) the connection event callback. */
497void det_conn_on_event(DetConnEventCb cb);
498
499/** @brief Read a consistent snapshot of the transport counters. */
500DetConnCounters det_conn_counters();
501
502/** @brief Zero the cumulative counters (the live ConnState::CONN_CLOSING gauge is untouched). */
503void det_conn_counters_reset();
504
505// Internal notify points (tcp.cpp), reached via the macros below so both
506// tcp.cpp and listener.cpp (accept) record through one path.
507void detws_obs_transition(uint8_t slot, ConnState olds, ConnState news, DetConnReason reason);
508void detws_obs_notice(uint8_t slot, ConnState st, DetConnReason reason);
509#define DETWS_OBS_TRANSITION(slot, olds, news, reason) detws_obs_transition((slot), (olds), (news), (reason))
510#define DETWS_OBS_NOTICE(slot, st, reason) detws_obs_notice((slot), (st), (reason))
511
512#else // !DETWS_ENABLE_OBSERVABILITY
513
514// Compile to nothing; the arguments (incl. DetConnReason names, only declared
515// when the feature is on) are dropped unparsed by the preprocessor.
516#define DETWS_OBS_TRANSITION(slot, olds, news, reason) ((void)0)
517#define DETWS_OBS_NOTICE(slot, st, reason) ((void)0)
518
519#endif // DETWS_ENABLE_OBSERVABILITY
520
521// ---------------------------------------------------------------------------
522// Per-connection lwIP callbacks (defined in tcp.cpp, used in listener.cpp)
523// ---------------------------------------------------------------------------
524
525/**
526 * @brief lwIP receive callback - wired to each new connection by listener_accept_cb.
527 * @see tcp.cpp
528 */
529err_t lowlevel_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
530
531/**
532 * @brief lwIP sent callback - refreshes the idle-timeout timestamp.
533 * @see tcp.cpp
534 */
535err_t lowlevel_sent_cb(void *arg, struct tcp_pcb *tpcb, u16_t len);
536
537/**
538 * @brief lwIP error callback - fires when the stack detects a fatal error.
539 * @see tcp.cpp
540 */
541void lowlevel_err_cb(void *arg, err_t err);
542
543// ---------------------------------------------------------------------------
544// Event enqueue (defined in listener.cpp, called from tcp.cpp)
545// ---------------------------------------------------------------------------
546
547/*
548 * Forward declaration of listener_enqueue() to break the circular include.
549 * See listener.h for the full documentation of this function.
550 * Returns true if the event was queued, false if it was dropped (queue full or
551 * inactive listener) - the transport observes drops as DetConnReason::DET_CONN_R_DEFER_DROP.
552 */
553bool listener_enqueue(uint8_t listener_id, const TcpEvt *evt);
554
555#endif
User-facing configuration for DeterministicESPAsyncWebServer.
#define CONN_POOL_SLOTS
DetIface
Network interface a connection arrived on (for per-route filtering).
ConnProto
Application protocol spoken on a listener port or connection slot.
#define RX_BUF_SIZE
Ring-buffer capacity in bytes per connection slot.
Static-only facade managing the shared TCP connection pool.
Definition tcp.h:170
static void check_timeouts(int worker_id=0)
Scan the pool and force-close connections idle for > conn_timeout_ms.
Definition tcp.cpp:727
static void pool_init(const WebServerConfig *cfg=nullptr)
Initialize the connection pool and store the runtime config.
Definition tcp.cpp:608
static uint32_t conn_timeout_ms
Runtime connection-idle timeout in milliseconds.
Definition tcp.h:216
static void stop()
Abort all active connections and reset the pool to ConnState::CONN_FREE.
Definition tcp.cpp:625
Layer 3 (Network) - a family-tagged IP address (IPv4 or IPv6) with RFC-faithful text parsing,...
Shared single-producer / single-consumer byte-ring primitive.
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52
A single TCP connection context.
Definition tcp.h:72
DetIface iface
Interface this connection arrived on; set at accept time.
Definition tcp.h:90
struct tcp_pcb * pcb
lwIP PCB; null when slot is free.
Definition tcp.h:75
uint32_t last_activity_ms
millis() timestamp of last TX/RX event.
Definition tcp.h:76
uint8_t id
Fixed slot index (0 … MAX_CONNS-1).
Definition tcp.h:73
uint8_t rx_buffer[RX_BUF_SIZE]
Ring buffer storage.
Definition tcp.h:78
uint8_t owner
Worker that owns this slot (round-robin at accept). Always 0 at N=1.
Definition tcp.h:86
uint8_t listener_id
Index into listener_pool[]; set at accept time.
Definition tcp.h:85
DetAtomic< size_t > rx_tail
Consumer read index (worker context).
Definition tcp.h:80
DetAtomic< size_t > rx_head
Producer write index (lwIP/tcpip context).
Definition tcp.h:79
DetAtomic< ConnState > state
Lifecycle state; acquire/release for inter-task visibility.
Definition tcp.h:74
uint8_t tls
Non-zero when this connection is TLS (set at accept time).
Definition tcp.h:91
uint8_t proto_slot
Per-protocol session/pool index (0xFF = none): the SSH session, an MQTT/Modbus session,...
Definition tcp.h:89
size_t rx_acked
Definition tcp.h:81
ConnProto proto
Application protocol for this connection.
Definition tcp.h:87
Event record posted from lwIP callbacks to the session layer.
Definition tcp.h:149
EvtType type
What happened.
Definition tcp.h:150
size_t data_len
Bytes copied (EvtType::EVT_DATA only); 0 for other types.
Definition tcp.h:152
uint8_t slot_id
Which connection slot is affected.
Definition tcp.h:151
Runtime-tunable server parameters.
uint32_t det_conn_remote_ip(uint8_t slot)
Raw source IPv4 of the connection in slot, or 0 if the slot has no active pcb (or on host builds)....
Definition tcp.cpp:655
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_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:413
void det_conn_begin_close(uint8_t slot_id)
Begin a graceful close that dwells in ConnState::CONN_CLOSING until the peer ACKs.
Definition tcp.cpp:572
uint32_t detws_ap_ip
softAP IPv4 address (network byte order) for STA/AP interface tagging.
Definition tcp.cpp:349
u16_t det_conn_sndbuf(uint8_t slot)
Bytes that can currently be queued for sending on slot.
Definition tcp.cpp:398
void det_conn_close(uint8_t slot)
Close connection slot gracefully (tcp_close), aborting if the FIN cannot be queued....
Definition tcp.cpp:467
void det_conn_ack_consumed(uint8_t slot)
Reopen the TCP receive window by however much slot has drained.
Definition tcp.cpp:427
TcpConn conn_pool[CONN_POOL_SLOTS]
Static pool of connection contexts. Defined in tcp.cpp. Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots pl...
Definition tcp.cpp:347
bool listener_enqueue(uint8_t listener_id, const TcpEvt *evt)
Post evt to the queue owned by listener listener_id.
Definition listener.cpp:291
bool det_conn_send_flush(uint8_t slot, const void *data, u16_t len)
Send len bytes on slot and flush in a single tcpip_thread round-trip.
Definition tcp.cpp:378
err_t lowlevel_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
lwIP receive callback - wired to each new connection by listener_accept_cb.
Definition tcp.cpp:795
void det_lwip_to_detip(const ip_addr_t *ra, DetIp *out)
A stable per-peer 32-bit identity key for slot (the v4 address, or an FNV-1a hash of a v6 address)....
Definition tcp.cpp:673
bool det_conn_raw_send(struct tcp_pcb *pcb, const void *data, u16_t len)
Write raw bytes straight to pcb (no TLS), context-safe.
Definition tcp.cpp:449
err_t lowlevel_sent_cb(void *arg, struct tcp_pcb *tpcb, u16_t len)
lwIP sent callback - refreshes the idle-timeout timestamp.
Definition tcp.cpp:896
void lowlevel_err_cb(void *arg, err_t err)
lwIP error callback - fires when the stack detects a fatal error.
Definition tcp.cpp:922
void det_conn_touch_active(uint8_t slot)
Refresh slot's idle-timeout timestamp while a response body is in flight.
Definition tcp.cpp:718
bool det_conn_remote_addr(uint8_t slot, DetIp *out)
The connected peer's address as a family-tagged DetIp (IPv4 or IPv6).
Definition tcp.cpp:691
void det_conn_detach(struct tcp_pcb *pcb)
Detach pcb from its slot's lwIP callbacks before the slot is freed.
Definition tcp.cpp:516
void det_conn_abort_slot(uint8_t slot)
Hard-abort connection slot (RST) for a fatal condition. The transport owns the teardown order: free t...
Definition tcp.cpp:496
EvtType
Type of connection event posted to a listener's FreeRTOS queue.
Definition tcp.h:135
@ EVT_DATA
Data received; bytes are already in the ring buffer.
@ EVT_CONNECT
New connection accepted.
@ EVT_DISCONNECT
Remote peer closed the connection gracefully.
@ EVT_ERROR
lwIP reported an error (PCB may already be freed).
ConnState
Lifecycle state of a connection pool slot.
Definition tcp.h:58
@ CONN_CLOSING
FIN sent; waiting for final ACK (reserved).
@ CONN_ACTIVE
Live connection; PCB is valid.
@ CONN_FREE
Slot is available; no PCB is attached.
void det_conn_abort(struct tcp_pcb *pcb)
Hard-abort pcb (RST) for a fatal condition; no graceful FIN.
Definition tcp.cpp:527
uint8_t det_conn_active_count()
Number of server connection slots currently in the CONN_ACTIVE state.
Definition tcp.cpp:646