DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
iface_bridge_hw.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 iface_bridge_hw.cpp
6 * @brief ESP32 glue for the interface bridge (see iface_bridge_hw.h): the PROTO_BRIDGE connection handler
7 * and the UART / SPI / I2C transfers. The rule table and frame codec live in the pure core.
8 */
9
11
12#if DETWS_ENABLE_IFACE_BRIDGE
13
16#include "services/clock.h" // detws_millis() pluggable monotonic clock
17
18// The Arduino bus headers MUST be included at global scope: pulling them into the anonymous namespace
19// below would make `SPI` / `Wire` anonymous-namespace symbols with no definition (link failure).
20#if defined(ARDUINO)
21#include "services/i2c.h" // detws_i2c_begin (the shared I2C bus owner)
22#include <Arduino.h>
23#include <SPI.h>
24#include <Wire.h>
25#endif
26
27namespace
28{
29
30// One published listener -> hardware rule. Dispatch is by the listener id the transport stamps on each
31// accepted slot (identical to services/relay); the rule pointer is stable for the life of the binding
32// because rules live in the pure table's static storage.
33struct BridgeBind
34{
35 bool active;
36 uint8_t listener_id;
37 const BridgeRule *rule;
38};
39
40// All of the glue's mutable state in one owned, feature-gated context (the owner-context guard requires
41// the single file-scope mutable to be a `*Ctx` instance).
42struct BridgeGlueCtx
43{
44 BridgeBind binds[DETWS_BRIDGE_MAX_RULES];
45 bool registered; ///< the PROTO_BRIDGE handler is installed
46 bool spi_begun; ///< SPI.begin() has run (once, shared bus)
47};
48BridgeGlueCtx s_ctx;
49
50const BridgeRule *rule_for_slot(uint8_t slot)
51{
52 uint8_t lid = det_conn_listener_id(slot);
53 for (int i = 0; i < DETWS_BRIDGE_MAX_RULES; i++)
54 if (s_ctx.binds[i].active && s_ctx.binds[i].listener_id == lid)
55 return s_ctx.binds[i].rule;
56 return nullptr;
57}
58
59// ---------------------------------------------------------------------------------------------
60// Bus I/O (ESP32 only). Host builds stub these out - the codec + rule table are host-tested.
61// ---------------------------------------------------------------------------------------------
62
63#if defined(ARDUINO)
64
65// unit -> the matching HardwareSerial, or nullptr if this SoC has no such UART. SOC_UART_NUM mirrors the
66// core's own guards on Serial1 / Serial2 (an S3 has fewer UARTs than a classic ESP32).
67HardwareSerial *uart_for(uint8_t unit)
68{
69 switch (unit)
70 {
71 case 0:
72 return &Serial;
73#if SOC_UART_NUM > 1
74 case 1:
75 return &Serial1;
76#endif
77#if SOC_UART_NUM > 2
78 case 2:
79 return &Serial2;
80#endif
81 default:
82 return nullptr;
83 }
84}
85
86// Bring the target's bus up once at publish. UART begins at its baud; SPI drives the CS gpio high (idle)
87// and starts the shared SPI bus once; I2C uses the shared bus owner.
88void bus_begin(const BridgeTarget *t)
89{
90 switch (t->bus)
91 {
92 case BridgeBus::uart: {
93 HardwareSerial *s = uart_for(t->unit);
94 if (s)
95 s->begin(t->rate ? t->rate : 115200);
96 break;
97 }
98 case BridgeBus::spi:
99 pinMode(t->addr_cs, OUTPUT);
100 digitalWrite(t->addr_cs, HIGH); // CS idle-high
101 if (!s_ctx.spi_begun)
102 {
103 SPI.begin();
104 s_ctx.spi_begun = true;
105 }
106 break;
107 case BridgeBus::i2c:
109 break;
110 }
111}
112
113// One write-then-read transaction against the target's bus. Clocks @p wlen bytes out, reads @p rlen bytes
114// back into @p rbuf (short reads are zero-padded). Returns false only on a bus-level failure.
115bool bus_txn(const BridgeTarget *t, const uint8_t *wbuf, uint16_t wlen, uint8_t *rbuf, uint16_t rlen)
116{
117 switch (t->bus)
118 {
119 case BridgeBus::i2c:
120 if (wlen)
121 {
122 Wire.beginTransmission((uint8_t)t->addr_cs);
123 Wire.write(wbuf, wlen);
124 // repeated-start (no stop) when a read follows, so the device holds the register pointer
125 if (Wire.endTransmission(rlen == 0) != 0)
126 return false;
127 }
128 if (rlen)
129 {
130 uint16_t got = (uint16_t)Wire.requestFrom((int)(uint8_t)t->addr_cs, (int)rlen);
131 for (uint16_t i = 0; i < rlen; i++)
132 rbuf[i] = (i < got && Wire.available()) ? (uint8_t)Wire.read() : 0;
133 }
134 return true;
135
136 case BridgeBus::spi: {
137 uint8_t order = t->bit_order ? LSBFIRST : MSBFIRST;
138 SPI.beginTransaction(SPISettings(t->rate ? t->rate : 1000000, order, t->spi_mode & 0x3));
139 digitalWrite(t->addr_cs, LOW);
140 for (uint16_t i = 0; i < wlen; i++)
141 SPI.transfer(wbuf[i]);
142 for (uint16_t i = 0; i < rlen; i++)
143 rbuf[i] = SPI.transfer(0x00); // clock dummies to read
144 digitalWrite(t->addr_cs, HIGH);
145 SPI.endTransaction();
146 return true;
147 }
148
149 case BridgeBus::uart: {
150 HardwareSerial *s = uart_for(t->unit);
151 if (!s)
152 return false;
153 if (wlen)
154 s->write(wbuf, wlen);
155 uint16_t got = 0;
156 uint32_t deadline = detws_millis() + DETWS_BRIDGE_UART_TXN_MS;
157 while (got < rlen && (int32_t)(detws_millis() - deadline) < 0)
158 while (got < rlen && s->available())
159 rbuf[got++] = (uint8_t)s->read();
160 for (; got < rlen; got++)
161 rbuf[got] = 0; // zero-pad a short read
162 return true;
163 }
164 }
165 return false;
166}
167
168// STREAM: pipe socket RX -> UART (called from on_data).
169void stream_sock_to_uart(uint8_t slot, const BridgeTarget *t)
170{
171 HardwareSerial *s = uart_for(t->unit);
172 if (!s)
173 return;
174 uint8_t buf[DETWS_BRIDGE_STREAM_CHUNK];
175 size_t n = 0;
176 while ((n = det_conn_read(slot, buf, sizeof buf)) > 0)
177 s->write(buf, n);
178}
179
180// STREAM: pipe UART RX -> socket (called from on_poll).
181void stream_uart_to_sock(uint8_t slot, const BridgeTarget *t)
182{
183 HardwareSerial *s = uart_for(t->unit);
184 if (!s)
185 return;
186 uint8_t buf[DETWS_BRIDGE_STREAM_CHUNK];
187 while (s->available() > 0)
188 {
189 size_t n = 0;
190 while (n < sizeof buf && s->available())
191 buf[n++] = (uint8_t)s->read();
192 if (n && det_conn_active(slot))
193 det_conn_send(slot, buf, (u16_t)n);
194 }
195}
196
197#else // host build: no Serial / SPI / Wire. The codec + rule table are host-tested elsewhere.
198
199void bus_begin(const BridgeTarget *)
200{
201}
202bool bus_txn(const BridgeTarget *, const uint8_t *, uint16_t, uint8_t *, uint16_t)
203{
204 return false;
205}
206void stream_sock_to_uart(uint8_t, const BridgeTarget *)
207{
208}
209void stream_uart_to_sock(uint8_t, const BridgeTarget *)
210{
211}
212
213#endif // ARDUINO
214
215// TRANSACTION: drain complete write-then-read frames out of the slot's RX ring, run each against the bus,
216// and send the read bytes back. Peeks a whole frame into a linear scratch so the pure codec stays the one
217// owner of the frame format; consumes only once a frame is fully buffered (partial frames wait for more).
218void service_txn(uint8_t slot, const BridgeTarget *t)
219{
220 uint8_t frame[DETWS_BRIDGE_TXN_HDR + DETWS_BRIDGE_TXN_MAX];
221 uint8_t rbuf[DETWS_BRIDGE_TXN_MAX];
222 for (;;)
223 {
224 size_t avail = det_conn_available(slot);
225 if (avail < DETWS_BRIDGE_TXN_HDR)
226 return; // header not yet complete
227 uint8_t hdr[DETWS_BRIDGE_TXN_HDR];
228 det_conn_peek(slot, 0, hdr, DETWS_BRIDGE_TXN_HDR);
229 uint16_t wlen = (uint16_t)((hdr[0] << 8) | hdr[1]);
230 uint16_t rlen = (uint16_t)((hdr[2] << 8) | hdr[3]);
231 if (wlen > DETWS_BRIDGE_TXN_MAX || rlen > DETWS_BRIDGE_TXN_MAX)
232 {
233 det_conn_close(slot); // frame exceeds the configured cap - protocol error
234 return;
235 }
236 size_t need = (size_t)DETWS_BRIDGE_TXN_HDR + wlen;
237 if (avail < need)
238 return; // write payload not fully buffered yet
239 det_conn_peek(slot, 0, frame, need);
240 uint16_t pw = 0;
241 uint16_t pr = 0;
242 const uint8_t *wd = nullptr;
243 if (bridge_txn_parse(frame, need, &pw, &pr, &wd) != need)
244 {
245 det_conn_close(slot); // codec disagreed with the header - drop the connection
246 return;
247 }
248 det_conn_consume(slot, need);
249 if (!bus_txn(t, wd, pw, rbuf, pr))
250 {
251 det_conn_close(slot); // bus fault
252 return;
253 }
254 if (pr && det_conn_active(slot))
255 det_conn_send(slot, rbuf, pr);
256 }
257}
258
259// ---------------------------------------------------------------------------------------------
260// PROTO_BRIDGE connection handler.
261// ---------------------------------------------------------------------------------------------
262
263void bridge_on_accept(uint8_t slot)
264{
265 if (!rule_for_slot(slot))
266 det_conn_close(slot); // no rule published for this listener
267}
268
269void bridge_on_data(uint8_t slot)
270{
271 const BridgeRule *r = rule_for_slot(slot);
272 if (!r)
273 {
274 det_conn_close(slot);
275 return;
276 }
277 if (r->target.mode == BridgeMode::stream)
278 stream_sock_to_uart(slot, &r->target);
279 else
280 service_txn(slot, &r->target);
281}
282
283void bridge_on_poll(uint8_t slot)
284{
285 if (!det_conn_active(slot))
286 return;
287 const BridgeRule *r = rule_for_slot(slot);
288 if (!r || r->target.mode != BridgeMode::stream)
289 return; // transaction mode is request-driven; nothing to pump on poll
290 stream_uart_to_sock(slot, &r->target);
291}
292
293void bridge_on_close(uint8_t)
294{
295 // Per-connection is stateless (the rule is re-derived from the listener id each callback), so there is
296 // nothing to free; the transport owns the closing slot.
297}
298
299const ProtoHandler s_bridge_handler = {bridge_on_accept, bridge_on_data, bridge_on_close, bridge_on_poll};
300
301} // namespace
302
303bool det_bridge_publish(uint8_t listener_id, uint16_t port, BridgeProto proto, const BridgeTarget *target)
304{
305 if (!target)
306 return false;
307 if (!bridge_map(nullptr, port, proto, target)) // store + validate + dedupe in the pure table
308 return false;
309 const BridgeRule *rule = bridge_find(port, proto);
310 if (!rule)
311 return false;
312 int idx = -1;
313 for (int i = 0; i < DETWS_BRIDGE_MAX_RULES; i++)
314 if (!s_ctx.binds[i].active)
315 {
316 idx = i;
317 break;
318 }
319 if (idx < 0)
320 return false;
321 s_ctx.binds[idx].active = true;
322 s_ctx.binds[idx].listener_id = listener_id;
323 s_ctx.binds[idx].rule = rule;
324 bus_begin(&rule->target);
325 if (!s_ctx.registered)
326 {
327 proto_register(ConnProto::PROTO_BRIDGE, &s_bridge_handler);
328 s_ctx.registered = true;
329 }
330 return true;
331}
332
333void det_bridge_listener_reset(void)
334{
335 for (int i = 0; i < DETWS_BRIDGE_MAX_RULES; i++)
336 s_ctx.binds[i].active = false;
337 bridge_clear();
338}
339
340#endif // DETWS_ENABLE_IFACE_BRIDGE
#define DETWS_BRIDGE_MAX_RULES
Max concurrent address:port -> bus rules (services/iface_bridge).
@ PROTO_BRIDGE
address:port -> hardware bus (DETWS_ENABLE_IFACE_BRIDGE): UART/SPI/I2C device server.
#define DETWS_BRIDGE_UART_TXN_MS
UART TRANSACTION read window (ms): how long a write-then-read waits for the read_len reply.
#define DETWS_BRIDGE_STREAM_CHUNK
STREAM (UART) pipe chunk size (bytes) for services/iface_bridge - one socket<->UART hop.
#define DETWS_BRIDGE_TXN_MAX
Max write / read payload (bytes) per TRANSACTION frame (services/iface_bridge).
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
The one owner of the shared I2C bus bring-up for the peripheral drivers.
void detws_i2c_begin()
Bring up the shared I2C bus on DETWS_I2C_SDA_PIN / DETWS_I2C_SCL_PIN (-1 = default).
Definition i2c.h:33
ESP32 glue for the interface bridge (DETWS_ENABLE_IFACE_BRIDGE): the PROTO_BRIDGE listener that wires...
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.