ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
physical_esp.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 physical_esp.cpp
6 * @brief Layer 1 (Physical) - ESP backend: 802.11 radio + wired Ethernet (RMII / W5500 SPI) bring-up and
7 * live egress readout, straight off the Arduino-ESP32 WiFi/ETH wrappers and lwIP's default netif.
8 *
9 * The vendor-specific half of the physical layer. The common API is in network_drivers/physical/physical.h and the
10 * vendor is chosen by the PC_VENDOR_* selector (board_drivers/board_profiles/pc_platform.h); this whole TU compiles to
11 * nothing on any non-ESP vendor, where physical.cpp's fallback stubs stand in (PC_PHYSICAL_HAS_BACKEND == 0). WiFi
12 * station bring-up is asynchronous (poll wifi_ready()); pc_net_egress() reads the live default-route netif and hands it
13 * to the pure pc_net_classify_ip() that lives in physical.cpp. Adding STM/RP/TI = a sibling
14 * board_drivers/physical/<vendor>/ TU guarded by that vendor's macro, no change here.
15 */
16
18
19#if PC_VENDOR_ESP
20
21#include "lwip/ip_addr.h"
22#include "lwip/netif.h"
23#include <WiFi.h>
24#include <esp_wifi.h> // esp_wifi_set_channel / esp_wifi_sta_get_ap_info (raw-radio bring-up + SSID readout)
25#include <string.h> // strnlen / memcpy
26#if PC_ENABLE_ETHERNET
27#include <ETH.h>
28#endif
29#if PC_ENABLE_IPV6
30#include "lwip/ip6_addr.h"
31#endif
32
33bool init_wifi_physical(const char *ssid, const char *password)
34{
35 WiFi.begin(ssid, password);
36 return true;
37}
38
39bool wifi_ready()
40{
41 return WiFi.isConnected();
42}
43
44bool init_wifi_radio_physical(uint8_t channel)
45{
46 // Radio up but not associated: ESP-NOW and promiscuous capture want the PHY without an IP link.
47 WiFi.mode(WIFI_STA);
48 WiFi.disconnect();
49 if (channel)
50 {
51 esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
52 }
53 return true;
54}
55
56bool init_wifi_ap_physical(const char *ssid, const char *password)
57{
58 WiFi.mode(WIFI_AP_STA); // coexist so a station link can run alongside the softAP
59 return WiFi.softAP(ssid, password);
60}
61
62#if PC_ENABLE_ETHERNET
63bool init_eth_physical(void)
64{
65#if defined(PC_ETH_W5500) && PC_ETH_W5500 && ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
66 // W5500 SPI Ethernet (arduino-esp32 3.x ETH SPI API): the HSPI host (SPI3) clocks the W5500 on the
67 // PC_ETH_W5500_* pins (CS/INT/RST + SCK/MISO/MOSI). Needs CONFIG_ETH_SPI_ETHERNET_W5500 in the SDK
68 // (default on for the S3). W5500 SPI is arduino-esp32 3.x only - the 2.x ETH library has no W5500.
69 return ETH.begin(ETH_PHY_W5500, 1 /*phy addr*/, PC_ETH_W5500_CS, PC_ETH_W5500_INT, PC_ETH_W5500_RST, SPI3_HOST,
71#else
72 // RMII PHY: pins / type / clock come from the ETH_PHY_* build flags (ETH.begin() defaults).
73 return ETH.begin();
74#endif
75}
76bool eth_ready(void)
77{
78 return ETH.linkUp() && (uint32_t)ETH.localIP() != 0;
79}
80#else
81bool init_eth_physical(void)
82{
83 return false; // Ethernet not enabled (PC_ENABLE_ETHERNET)
84}
85bool eth_ready(void)
86{
87 return false;
88}
89#endif
90
91#if PC_ENABLE_IPV6
92
93bool init_ipv6_physical(void)
94{
95 // The WiFi wrapper's enable call was renamed in Arduino-ESP32 3.0; the address readout
96 // below goes straight to lwIP, which is stable across both cores.
97#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
98 return WiFi.enableIPv6(true);
99#else
100 return WiFi.enableIpV6();
101#endif
102}
103
104bool net_global_ipv6(pc_ip *out)
105{
106 if (!out || !netif_default)
107 {
108 return false;
109 }
110 for (int8_t i = 0; i < LWIP_IPV6_NUM_ADDRESSES; i++)
111 {
112 if (!ip6_addr_isvalid(netif_ip6_addr_state(netif_default, i)))
113 {
114 continue;
115 }
116 const ip6_addr_t *a6 = netif_ip6_addr(netif_default, i);
117 if (!ip6_addr_isglobal(a6))
118 {
119 continue;
120 }
122 memcpy(out->bytes, a6->addr, 16); // lwIP holds the 16 bytes in network order
123 return true;
124 }
125 return false;
126}
127
128bool pc_ipv6_ready(void)
129{
130 pc_ip tmp;
131 return net_global_ipv6(&tmp);
132}
133#else
134bool init_ipv6_physical(void)
135{
136 return false; // IPv6 not enabled (PC_ENABLE_IPV6)
137}
139{
140 return false;
141}
142bool pc_ipv6_ready(void)
143{
144 return false;
145}
146#endif
147
148uint32_t pc_net_egress_ip(void)
149{
150 // netif_default is the current default-route interface (the egress).
151 return netif_default ? ip4_addr_get_u32(ip_2_ip4(&netif_default->ip_addr)) : 0;
152}
153
155{
156 uint32_t egress = pc_net_egress_ip();
157 if (egress == 0)
158 {
160 }
161 uint32_t sta = WiFi.isConnected() ? (uint32_t)WiFi.localIP() : 0;
162 uint32_t ap = pc_net_ap_ip();
163 return pc_net_classify_ip(egress, sta, ap);
164}
165
166uint32_t pc_net_ap_ip(void)
167{
168 return (WiFi.getMode() & WIFI_AP) ? (uint32_t)WiFi.softAPIP() : 0;
169}
170
171int8_t pc_net_rssi(void)
172{
173 return WiFi.isConnected() ? (int8_t)WiFi.RSSI() : 0;
174}
175
176bool pc_net_mac(uint8_t out[6])
177{
178 if (!out)
179 {
180 return false;
181 }
182 WiFi.macAddress(out);
183 return true;
184}
185
186bool pc_net_egress_mac(uint8_t out[6])
187{
188 // The egress interface's own link-layer address, straight off the live default netif - the Ethernet PHY's
189 // MAC on a wired link, the WiFi STA MAC on a wireless one. Independent of which driver started.
190 if (!out || !netif_default || netif_default->hwaddr_len < 6)
191 {
192 return false;
193 }
194 memcpy(out, netif_default->hwaddr, 6);
195 return true;
196}
197
198size_t pc_net_ssid(char *out, size_t cap)
199{
200 if (!out || cap == 0)
201 {
202 return 0;
203 }
204 wifi_ap_record_t info; // heap-free SSID readout (WiFi.SSID() would allocate an Arduino String)
205 if (esp_wifi_sta_get_ap_info(&info) != ESP_OK)
206 {
207 out[0] = '\0';
208 return 0;
209 }
210 size_t n = strnlen((const char *)info.ssid, sizeof(info.ssid));
211 if (n >= cap)
212 {
213 n = cap - 1;
214 }
215 memcpy(out, info.ssid, n);
216 out[n] = '\0';
217 return n;
218}
219
220uint8_t pc_net_channel(void)
221{
222 return (uint8_t)WiFi.channel();
223}
224
225/* ------------------------------------------------------------------ radio control (L1 contract)
226 * The vendor half of pc_phy_*: mode translation, the quarter-dBm transmit unit, and the
227 * received-packet struct all stay on this side of the boundary. The core sees whole dBm, its own
228 * power-save vocabulary, and a plain frame pointer.
229 */
230
231namespace
232{
233wifi_ps_type_t to_esp_ps(pc_phy_ps mode)
234{
236 {
237 return WIFI_PS_MIN_MODEM;
238 }
240 {
241 return WIFI_PS_MAX_MODEM;
242 }
243 return WIFI_PS_NONE;
244}
245
246// One named owner for the monitor-mode sink (owner-context guard).
247struct PhyMonitorCtx
248{
249 pc_phy_frame_fn sink;
250};
251PhyMonitorCtx s_phy_monitor = {nullptr};
252
253// Vendor shape in, neutral shape out. sig_len includes the 4-byte FCS, which no caller wants.
254void phy_monitor_trampoline(void *buf, wifi_promiscuous_pkt_type_t)
255{
256 if (!s_phy_monitor.sink || !buf)
257 {
258 return;
259 }
260 const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf;
261 uint16_t len = (uint16_t)pkt->rx_ctrl.sig_len;
262 if (len < 4)
263 {
264 return;
265 }
266 s_phy_monitor.sink(pkt->payload, (uint16_t)(len - 4), (int8_t)pkt->rx_ctrl.rssi, (uint8_t)pkt->rx_ctrl.channel);
267}
268} // namespace
269
270bool pc_phy_ps_set(pc_phy_ps mode)
271{
272 return esp_wifi_set_ps(to_esp_ps(mode)) == ESP_OK;
273}
274
276{
277 wifi_ps_type_t m = WIFI_PS_NONE;
278 if (esp_wifi_get_ps(&m) != ESP_OK)
279 {
281 }
282 if (m == WIFI_PS_MIN_MODEM)
283 {
285 }
286 if (m == WIFI_PS_MAX_MODEM)
287 {
289 }
291}
292
293bool pc_phy_tx_power_set(int8_t dbm)
294{
295 // The vendor API takes quarter-dBm; that unit is vendor detail, so it converts here.
296 return esp_wifi_set_max_tx_power((int8_t)(dbm * 4)) == ESP_OK;
297}
298
299bool pc_phy_monitor_begin(uint8_t channel, pc_phy_frame_fn cb)
300{
301 if (!cb)
302 {
303 return false;
304 }
305 s_phy_monitor.sink = cb;
306 esp_wifi_set_promiscuous(true);
307 esp_wifi_set_promiscuous_rx_cb(&phy_monitor_trampoline);
308 esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
309 return true;
310}
311
312void pc_phy_monitor_set_channel(uint8_t channel)
313{
314 esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
315}
316
317void pc_phy_monitor_end(void)
318{
319 esp_wifi_set_promiscuous(false);
320 s_phy_monitor.sink = nullptr;
321}
322
323#endif // PC_VENDOR_ESP
@ PC_IP_V6
IPv6 (bytes[0..15])
bool init_wifi_physical(const char *, const char *)
Connect to a WiFi access point.
Definition physical.cpp:41
bool pc_ipv6_ready(void)
True once the interface has a global IPv6 address (see net_global_ipv6()).
Definition physical.cpp:73
pc_iface pc_net_classify_ip(uint32_t egress_ip, uint32_t sta_ip, uint32_t ap_ip)
Classify an egress IPv4 against the WiFi station / softAP IPs (pure helper, exposed for unit testing)...
Definition physical.cpp:20
uint32_t pc_net_egress_ip(void)
IPv4 (network byte order) of the current egress interface, or 0 if none.
Definition physical.cpp:77
pc_phy_ps pc_phy_ps_get(void)
Read the active power-save mode (PC_PHY_PS_NONE when unsupported).
Definition physical.cpp:108
bool eth_ready(void)
True if the Ethernet link is up and an IP is assigned.
Definition physical.cpp:61
bool pc_phy_monitor_begin(uint8_t, pc_phy_frame_fn)
Enter monitor mode on channel, delivering frames to cb.
Definition physical.cpp:116
bool pc_net_egress_mac(uint8_t *)
Definition physical.cpp:97
bool init_wifi_ap_physical(const char *, const char *)
Bring up a softAP, enabling AP+STA coexistence so a station link can run alongside it.
Definition physical.cpp:53
bool net_global_ipv6(pc_ip *)
The interface's global (routable) IPv6 address, if it has one.
Definition physical.cpp:69
bool pc_phy_tx_power_set(int8_t)
Cap transmit power.
Definition physical.cpp:112
size_t pc_net_ssid(char *out, size_t cap)
Copy the associated SSID (null-terminated) into out.
Definition physical.cpp:126
bool wifi_ready()
True if the WiFi station link is up (associated + an IP is assigned).
Definition physical.cpp:45
int8_t pc_net_rssi(void)
Station link RSSI in dBm, or 0 if not associated (and on host builds).
Definition physical.cpp:89
void pc_phy_monitor_end(void)
Leave monitor mode.
Definition physical.cpp:123
uint32_t pc_net_ap_ip(void)
softAP IPv4 (network byte order), or 0 if the softAP is not up (and on host builds).
Definition physical.cpp:85
bool init_ipv6_physical(void)
Enable IPv6 (dual-stack) on the Wi-Fi interface (PC_ENABLE_IPV6).
Definition physical.cpp:65
bool init_eth_physical(void)
Bring up a wired Ethernet link (PC_ENABLE_ETHERNET).
Definition physical.cpp:57
pc_iface pc_net_egress(void)
Which interface currently carries outbound traffic.
Definition physical.cpp:81
void pc_phy_monitor_set_channel(uint8_t)
Retune monitor mode to channel.
Definition physical.cpp:120
uint8_t pc_net_channel(void)
Station WiFi channel (1..14), or 0 if not associated (and on host builds).
Definition physical.cpp:134
bool pc_phy_ps_set(pc_phy_ps)
Apply a power-save mode.
Definition physical.cpp:104
bool init_wifi_radio_physical(uint8_t)
Bring the WiFi radio up in station mode WITHOUT associating to an AP.
Definition physical.cpp:49
bool pc_net_mac(uint8_t *)
Definition physical.cpp:93
Layer 1 (Physical) - link bring-up and live egress-interface reporting.
void(* pc_phy_frame_fn)(const uint8_t *frame, uint16_t len, int8_t rssi, uint8_t channel)
One received frame, delivered in neutral terms.
Definition physical.h:208
pc_phy_ps
Radio power-save mode, in the library's own vocabulary.
Definition physical.h:191
@ PC_PHY_PS_MIN_MODEM
Wake on every DTIM beacon.
@ PC_PHY_PS_NONE
Radio always on: lowest latency, highest average draw.
@ PC_PHY_PS_MAX_MODEM
Wake on a longer listen interval: lowest draw, highest latency.
#define PC_ETH_W5500_SCK
HSPI clock (S3-DevKitC default)
#define PC_ETH_W5500_CS
chip select
#define PC_ETH_W5500_SPI_MHZ
W5500 SPI clock (MHz); raise for throughput on clean wiring.
#define PC_ETH_W5500_MISO
HSPI MISO (S3-DevKitC default)
#define PC_ETH_W5500_MOSI
HSPI MOSI (S3-DevKitC default)
#define PC_ETH_W5500_INT
interrupt
#define PC_ETH_W5500_RST
reset
pc_iface
Network interface a connection arrived on (for per-route filtering).
@ PC_IFACE_ANY
Unknown / no filter (matches any interface).
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52
pc_ip_family family
address family tag
Definition ip.h:53
uint8_t bytes[16]
network order; v4 uses the first 4
Definition ip.h:54