ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 PC_ENABLE_IFACE_BRIDGE
13
16#include "services/system/clock.h" // pc_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/peripherals/i2c.h" // pc_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/net/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[PC_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 = pc_conn_listener_id(slot);
53 for (int i = 0; i < PC_BRIDGE_MAX_RULES; i++)
54 {
55 if (s_ctx.binds[i].active && s_ctx.binds[i].listener_id == lid)
56 {
57 return s_ctx.binds[i].rule;
58 }
59 }
60 return nullptr;
61}
62
63// ---------------------------------------------------------------------------------------------
64// Bus I/O (ESP32 only). Host builds stub these out - the codec + rule table are host-tested.
65// ---------------------------------------------------------------------------------------------
66
67#if defined(ARDUINO)
68
69// unit -> the matching HardwareSerial, or nullptr if this SoC has no such UART. SOC_UART_NUM mirrors the
70// core's own guards on Serial1 / Serial2 (an S3 has fewer UARTs than a classic ESP32).
71HardwareSerial *uart_for(uint8_t unit)
72{
73 switch (unit)
74 {
75 case 0:
76 // With USB-CDC-on-boot, `Serial` is the USB CDC (HWCDC) and UART0 is exposed as `Serial0`
77 // (a HardwareSerial); otherwise `Serial` itself is the UART0 HardwareSerial. Mirror the core's
78 // own guard (cores/esp32/HardwareSerial.h) so unit 0 always resolves to a real HardwareSerial.
79#if ARDUINO_USB_CDC_ON_BOOT
80 return &Serial0;
81#else
82 return &Serial;
83#endif
84#if SOC_UART_NUM > 1
85 case 1:
86 return &Serial1;
87#endif
88#if SOC_UART_NUM > 2
89 case 2:
90 return &Serial2;
91#endif
92 default:
93 return nullptr;
94 }
95}
96
97// Bring the target's bus up once at publish. UART begins at its baud; SPI drives the CS gpio high (idle)
98// and starts the shared SPI bus once; I2C uses the shared bus owner.
99void bus_begin(const BridgeTarget *t)
100{
101 switch (t->bus)
102 {
103 case BridgeBus::uart: {
104 HardwareSerial *s = uart_for(t->unit);
105 if (s)
106 {
107 s->begin(t->rate ? t->rate : 115200);
108 }
109 break;
110 }
111 case BridgeBus::spi:
112 pinMode(t->addr_cs, OUTPUT);
113 digitalWrite(t->addr_cs, HIGH); // CS idle-high
114 if (!s_ctx.spi_begun)
115 {
116 SPI.begin();
117 s_ctx.spi_begun = true;
118 }
119 break;
120 case BridgeBus::i2c:
121 pc_i2c_begin();
122 break;
123 }
124}
125
126// One write-then-read transaction against the target's bus. Clocks @p wlen bytes out, reads @p rlen bytes
127// back into @p rbuf (short reads are zero-padded). Returns false only on a bus-level failure.
128bool bus_txn(const BridgeTarget *t, const uint8_t *wbuf, uint16_t wlen, uint8_t *rbuf, uint16_t rlen)
129{
130 switch (t->bus)
131 {
132 case BridgeBus::i2c:
133 if (wlen)
134 {
135 Wire.beginTransmission((uint8_t)t->addr_cs);
136 Wire.write(wbuf, wlen);
137 // repeated-start (no stop) when a read follows, so the device holds the register pointer
138 if (Wire.endTransmission(rlen == 0) != 0)
139 {
140 return false;
141 }
142 }
143 if (rlen)
144 {
145 uint16_t got = (uint16_t)Wire.requestFrom((int)(uint8_t)t->addr_cs, (int)rlen);
146 for (uint16_t i = 0; i < rlen; i++)
147 {
148 rbuf[i] = (i < got && Wire.available()) ? (uint8_t)Wire.read() : 0;
149 }
150 }
151 return true;
152
153 case BridgeBus::spi: {
154 uint8_t order = t->bit_order ? LSBFIRST : MSBFIRST;
155 SPI.beginTransaction(SPISettings(t->rate ? t->rate : 1000000, order, t->spi_mode & 0x3));
156 digitalWrite(t->addr_cs, LOW);
157 for (uint16_t i = 0; i < wlen; i++)
158 {
159 SPI.transfer(wbuf[i]);
160 }
161 for (uint16_t i = 0; i < rlen; i++)
162 {
163 rbuf[i] = SPI.transfer(0x00); // clock dummies to read
164 }
165 digitalWrite(t->addr_cs, HIGH);
166 SPI.endTransaction();
167 return true;
168 }
169
170 case BridgeBus::uart: {
171 HardwareSerial *s = uart_for(t->unit);
172 if (!s)
173 {
174 return false;
175 }
176 if (wlen)
177 {
178 s->write(wbuf, wlen);
179 }
180 uint16_t got = 0;
181 uint32_t deadline = pc_millis() + PC_BRIDGE_UART_TXN_MS;
182 while (got < rlen && (int32_t)(pc_millis() - deadline) < 0)
183 {
184 while (got < rlen && s->available())
185 {
186 rbuf[got++] = (uint8_t)s->read();
187 }
188 }
189 for (; got < rlen; got++)
190 {
191 rbuf[got] = 0; // zero-pad a short read
192 }
193 return true;
194 }
195 }
196 return false;
197}
198
199// STREAM: pipe socket RX -> UART (called from on_data).
200void stream_sock_to_uart(uint8_t slot, const BridgeTarget *t)
201{
202 HardwareSerial *s = uart_for(t->unit);
203 if (!s)
204 {
205 return;
206 }
207 uint8_t buf[PC_BRIDGE_STREAM_CHUNK];
208 size_t n = 0;
209 while ((n = pc_conn_read(slot, buf, sizeof buf)) > 0)
210 {
211 s->write(buf, n);
212 }
213}
214
215// STREAM: pipe UART RX -> socket (called from on_poll).
216void stream_uart_to_sock(uint8_t slot, const BridgeTarget *t)
217{
218 HardwareSerial *s = uart_for(t->unit);
219 if (!s)
220 {
221 return;
222 }
223 uint8_t buf[PC_BRIDGE_STREAM_CHUNK];
224 while (s->available() > 0)
225 {
226 size_t n = 0;
227 while (n < sizeof buf && s->available())
228 {
229 buf[n++] = (uint8_t)s->read();
230 }
231 if (n && pc_conn_active(slot))
232 {
233 pc_conn_send(slot, buf, (u16_t)n);
234 }
235 }
236}
237
238#else // host build: no Serial / SPI / Wire. The codec + rule table are host-tested elsewhere.
239
240void bus_begin(const BridgeTarget *)
241{
242}
243bool bus_txn(const BridgeTarget *, const uint8_t *, uint16_t, uint8_t *, uint16_t)
244{
245 return false;
246}
247void stream_sock_to_uart(uint8_t, const BridgeTarget *)
248{
249}
250void stream_uart_to_sock(uint8_t, const BridgeTarget *)
251{
252}
253
254#endif // ARDUINO
255
256// TRANSACTION: drain complete write-then-read frames out of the slot's RX ring, run each against the bus,
257// and send the read bytes back. Peeks a whole frame into a linear scratch so the pure codec stays the one
258// owner of the frame format; consumes only once a frame is fully buffered (partial frames wait for more).
259void service_txn(uint8_t slot, const BridgeTarget *t)
260{
261 uint8_t frame[PC_BRIDGE_TXN_HDR + PC_BRIDGE_TXN_MAX];
262 uint8_t rbuf[PC_BRIDGE_TXN_MAX];
263 for (;;)
264 {
265 size_t avail = pc_conn_available(slot);
266 if (avail < PC_BRIDGE_TXN_HDR)
267 {
268 return; // header not yet complete
269 }
270 uint8_t hdr[PC_BRIDGE_TXN_HDR];
271 pc_conn_peek(slot, 0, hdr, PC_BRIDGE_TXN_HDR);
272 uint16_t wlen = (uint16_t)((hdr[0] << 8) | hdr[1]);
273 uint16_t rlen = (uint16_t)((hdr[2] << 8) | hdr[3]);
274 if (wlen > PC_BRIDGE_TXN_MAX || rlen > PC_BRIDGE_TXN_MAX)
275 {
276 pc_conn_close(slot); // frame exceeds the configured cap - protocol error
277 return;
278 }
279 size_t need = (size_t)PC_BRIDGE_TXN_HDR + wlen;
280 if (avail < need)
281 {
282 return; // write payload not fully buffered yet
283 }
284 pc_conn_peek(slot, 0, frame, need);
285 uint16_t pw = 0;
286 uint16_t pr = 0;
287 const uint8_t *wd = nullptr;
288 if (pc_iface_bridge_txn_parse(frame, need, &pw, &pr, &wd) != need)
289 {
290 pc_conn_close(slot); // codec disagreed with the header - drop the connection
291 return;
292 }
293 pc_conn_consume(slot, need);
294 if (!bus_txn(t, wd, pw, rbuf, pr))
295 {
296 pc_conn_close(slot); // bus fault
297 return;
298 }
299 if (pr && pc_conn_active(slot))
300 {
301 pc_conn_send(slot, rbuf, pr);
302 }
303 }
304}
305
306// ---------------------------------------------------------------------------------------------
307// PROTO_BRIDGE connection handler.
308// ---------------------------------------------------------------------------------------------
309
310void bridge_on_accept(uint8_t slot)
311{
312 if (!rule_for_slot(slot))
313 {
314 pc_conn_close(slot); // no rule published for this listener
315 }
316}
317
318void bridge_on_data(uint8_t slot)
319{
320 const BridgeRule *r = rule_for_slot(slot);
321 if (!r)
322 {
323 pc_conn_close(slot);
324 return;
325 }
326 if (r->target.mode == BridgeMode::stream)
327 {
328 stream_sock_to_uart(slot, &r->target);
329 }
330 else
331 {
332 service_txn(slot, &r->target);
333 }
334}
335
336void bridge_on_poll(uint8_t slot)
337{
338 if (!pc_conn_active(slot))
339 {
340 return;
341 }
342 const BridgeRule *r = rule_for_slot(slot);
343 if (!r || r->target.mode != BridgeMode::stream)
344 {
345 return; // transaction mode is request-driven; nothing to pump on poll
346 }
347 stream_uart_to_sock(slot, &r->target);
348}
349
350void bridge_on_close(uint8_t)
351{
352 // Per-connection is stateless (the rule is re-derived from the listener id each callback), so there is
353 // nothing to free; the transport owns the closing slot.
354}
355
356const ProtoHandler s_bridge_handler = {bridge_on_accept, bridge_on_data, bridge_on_close, bridge_on_poll};
357
358} // namespace
359
360bool pc_iface_bridge_publish(uint8_t listener_id, uint16_t port, BridgeProto proto, const BridgeTarget *target)
361{
362 if (!target)
363 {
364 return false;
365 }
366 if (!pc_iface_bridge_map(nullptr, port, proto, target)) // store + validate + dedupe in the pure table
367 {
368 return false;
369 }
370 const BridgeRule *rule = pc_iface_bridge_find(port, proto);
371 if (!rule)
372 {
373 return false;
374 }
375 int idx = -1;
376 for (int i = 0; i < PC_BRIDGE_MAX_RULES; i++)
377 {
378 if (!s_ctx.binds[i].active)
379 {
380 idx = i;
381 break;
382 }
383 }
384 if (idx < 0)
385 {
386 return false;
387 }
388 s_ctx.binds[idx].active = true;
389 s_ctx.binds[idx].listener_id = listener_id;
390 s_ctx.binds[idx].rule = rule;
391 bus_begin(&rule->target);
392 if (!s_ctx.registered)
393 {
394 proto_register(ConnProto::PROTO_BRIDGE, &s_bridge_handler);
395 s_ctx.registered = true;
396 }
397 return true;
398}
399
400void pc_iface_bridge_listener_reset(void)
401{
402 for (int i = 0; i < PC_BRIDGE_MAX_RULES; i++)
403 {
404 s_ctx.binds[i].active = false;
405 }
406 pc_iface_bridge_clear();
407}
408
409#endif // PC_ENABLE_IFACE_BRIDGE
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
The one owner of the shared I2C bus bring-up for the peripheral drivers.
void pc_i2c_begin()
Bring up the shared I2C bus on PC_I2C_SDA_PIN / PC_I2C_SCL_PIN (-1 = default).
Definition i2c.h:33
ESP32 glue for the interface bridge (PC_ENABLE_IFACE_BRIDGE): the PROTO_BRIDGE listener that wires an...
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
#define PC_BRIDGE_MAX_RULES
Max concurrent address:port -> bus rules (services/net/iface_bridge).
@ PROTO_BRIDGE
address:port -> hardware bus (PC_ENABLE_IFACE_BRIDGE): UART/SPI/I2C device server.
#define PC_BRIDGE_UART_TXN_MS
UART TRANSACTION read window (ms): how long a write-then-read waits for the read_len reply.
#define PC_BRIDGE_STREAM_CHUNK
STREAM (UART) pipe chunk size (bytes) for services/net/iface_bridge - one socket<->UART hop.
#define PC_BRIDGE_TXN_MAX
Max write / read payload (bytes) per TRANSACTION frame (services/net/iface_bridge).
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
void pc_conn_close(uint8_t slot)
Close connection slot gracefully (tcp_close), aborting if the FIN cannot be queued....
Definition tcp.cpp:645
bool pc_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:500
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.