ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
espnow.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 espnow.cpp
6 * @brief ESP-NOW envelope codec + peer registry (pure) and esp_now binding (ESP32).
7 */
8
10
11#if PC_ENABLE_ESPNOW
12
13#include <string.h>
14
15#if defined(ARDUINO)
16#include <esp_idf_version.h> // ESP_IDF_VERSION / ESP_IDF_VERSION_VAL for the recv-cb ABI guard
17#include <esp_now.h>
18#include <esp_wifi.h>
19#endif
20const uint8_t PC_ESPNOW_BROADCAST[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
21
22// ---------------------------------------------------------------------------
23// Envelope codec
24// ---------------------------------------------------------------------------
25size_t pc_espnow_encode(uint8_t type, const uint8_t *payload, size_t len, uint8_t *out, size_t cap)
26{
27 if (!out || len > PC_ESPNOW_MAX_PAYLOAD || cap < len + PC_ESPNOW_HDR)
28 {
29 return 0;
30 }
31 out[0] = PC_ESPNOW_MAGIC;
32 out[1] = type;
33 out[2] = (uint8_t)len;
34 if (len && payload)
35 {
36 memcpy(out + PC_ESPNOW_HDR, payload, len);
37 }
38 return len + PC_ESPNOW_HDR;
39}
40
41bool pc_espnow_decode(const uint8_t *buf, size_t len, uint8_t *type, const uint8_t **payload, size_t *plen)
42{
43 if (!buf || len < PC_ESPNOW_HDR || buf[0] != PC_ESPNOW_MAGIC)
44 {
45 return false;
46 }
47 size_t declared = buf[2];
48 if (declared + PC_ESPNOW_HDR != len) // length must match exactly (no trailing/short)
49 {
50 return false;
51 }
52 if (type)
53 {
54 *type = buf[1];
55 }
56 if (payload)
57 {
58 *payload = buf + PC_ESPNOW_HDR;
59 }
60 if (plen)
61 {
62 *plen = declared;
63 }
64 return true;
65}
66
67// ---------------------------------------------------------------------------
68// Peer registry
69// ---------------------------------------------------------------------------
70namespace
71{
72struct Peer
73{
74 uint8_t mac[6];
75 bool used;
76};
77
78// All ESP-NOW runtime state, owned by one instance (internal linkage): the peer registry
79// (host + radio) plus the radio binding's recv callback and channel, grouped so it is one
80// named owner, unreachable from any other translation unit.
81struct EspnowCtx
82{
83 Peer peers[PC_ESPNOW_MAX_PEERS];
84#ifdef ARDUINO
85 pc_espnow_recv_fn recv = nullptr;
86 uint8_t channel = 0;
87#endif
88};
89EspnowCtx s_espnow;
90
91int peer_find(const EspnowCtx &c, const uint8_t mac[6])
92{
93 for (int i = 0; i < PC_ESPNOW_MAX_PEERS; i++)
94 {
95 if (c.peers[i].used && memcmp(c.peers[i].mac, mac, 6) == 0)
96 {
97 return i;
98 }
99 }
100 return -1;
101}
102} // namespace
103
104void pc_espnow_peers_reset(void)
105{
106 for (int i = 0; i < PC_ESPNOW_MAX_PEERS; i++)
107 {
108 s_espnow.peers[i].used = false;
109 }
110}
111
112bool pc_espnow_peer_add(const uint8_t mac[6])
113{
114 if (!mac)
115 {
116 return false;
117 }
118 if (peer_find(s_espnow, mac) >= 0)
119 {
120 return true; // idempotent
121 }
122 for (int i = 0; i < PC_ESPNOW_MAX_PEERS; i++)
123 {
124 if (!s_espnow.peers[i].used)
125 {
126 memcpy(s_espnow.peers[i].mac, mac, 6);
127 s_espnow.peers[i].used = true;
128 return true;
129 }
130 }
131 return false; // table full
132}
133
134bool pc_espnow_peer_has(const uint8_t mac[6])
135{
136 return mac && peer_find(s_espnow, mac) >= 0;
137}
138
139bool pc_espnow_peer_remove(const uint8_t mac[6])
140{
141 int i = mac ? peer_find(s_espnow, mac) : -1;
142 if (i < 0)
143 {
144 return false;
145 }
146 s_espnow.peers[i].used = false;
147 return true;
148}
149
150int pc_espnow_peer_count(void)
151{
152 int n = 0;
153 for (int i = 0; i < PC_ESPNOW_MAX_PEERS; i++)
154 {
155 if (s_espnow.peers[i].used)
156 {
157 n++;
158 }
159 }
160 return n;
161}
162
163// ---------------------------------------------------------------------------
164// ESP32 radio binding
165// ---------------------------------------------------------------------------
166#ifdef ARDUINO
167
168namespace
169{
170// The ESP-NOW receive callback signature changed in ESP-IDF 5.0 (Arduino-ESP32 3.x): the
171// source MAC moved into an esp_now_recv_info_t. Match whichever the compiled core expects.
172#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
173void on_recv(const esp_now_recv_info_t *info, const uint8_t *data, int len)
174{
175 const uint8_t *mac = info ? info->src_addr : nullptr;
176#else
177void on_recv(const uint8_t *mac, const uint8_t *data, int len)
178{
179#endif
180 if (!s_espnow.recv || len < 0 || !mac)
181 {
182 return;
183 }
184 uint8_t type;
185 const uint8_t *payload;
186 size_t plen;
187 if (pc_espnow_decode(data, (size_t)len, &type, &payload, &plen))
188 {
189 s_espnow.recv(mac, type, payload, plen);
190 }
191}
192
193bool radio_add_peer(const uint8_t mac[6], uint8_t channel)
194{
195 esp_now_peer_info_t p;
196 memset(&p, 0, sizeof(p));
197 memcpy(p.peer_addr, mac, 6);
198 p.channel = channel;
199 p.encrypt = false;
200 if (esp_now_is_peer_exist(mac))
201 {
202 return true;
203 }
204 return esp_now_add_peer(&p) == ESP_OK;
205}
206} // namespace
207
208bool pc_espnow_begin(uint8_t channel, pc_espnow_recv_fn cb)
209{
210 s_espnow.channel = channel;
211 s_espnow.recv = cb;
212 if (esp_now_init() != ESP_OK)
213 {
214 return false;
215 }
216 esp_now_register_recv_cb(on_recv);
217 pc_espnow_peers_reset();
218 return radio_add_peer(PC_ESPNOW_BROADCAST, channel); // broadcast is always a peer
219}
220
221bool pc_espnow_add_peer(const uint8_t mac[6])
222{
223 if (!pc_espnow_peer_add(mac))
224 {
225 return false;
226 }
227 return radio_add_peer(mac, s_espnow.channel);
228}
229
230bool pc_espnow_send(const uint8_t mac[6], uint8_t type, const uint8_t *payload, size_t len)
231{
232 uint8_t frame[PC_ESPNOW_HDR + PC_ESPNOW_MAX_PAYLOAD];
233 size_t n = pc_espnow_encode(type, payload, len, frame, sizeof(frame));
234 if (n == 0)
235 {
236 return false;
237 }
238 return esp_now_send(mac, frame, n) == ESP_OK;
239}
240
241bool pc_espnow_broadcast(uint8_t type, const uint8_t *payload, size_t len)
242{
243 return pc_espnow_send(PC_ESPNOW_BROADCAST, type, payload, len);
244}
245
246#else // host build - no radio
247
248bool pc_espnow_begin(uint8_t, pc_espnow_recv_fn)
249{
250 return false;
251}
252bool pc_espnow_add_peer(const uint8_t mac[6])
253{
254 return pc_espnow_peer_add(mac);
255}
256bool pc_espnow_send(const uint8_t *, uint8_t, const uint8_t *, size_t)
257{
258 return false;
259}
260bool pc_espnow_broadcast(uint8_t, const uint8_t *, size_t)
261{
262 return false;
263}
264
265#endif // ARDUINO
266
267#endif // PC_ENABLE_ESPNOW
ESP-NOW peer messaging with a typed envelope (PC_ENABLE_ESPNOW).