ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
espnow.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file espnow.h
6 * @brief ESP-NOW peer messaging with a typed envelope (PROTOCORE_ENABLE_ESPNOW).
7 *
8 * Connectionless ESP32 peer-to-peer radio messaging (no AP, no IP) wrapped in a
9 * small typed envelope so a receiver can demux by message type and reject a
10 * truncated frame. Two layers:
11 *
12 * - **Host-testable core (pure):** the 3-byte envelope codec
13 * (protocore_espnow_encode / decode) and a bounded peer registry
14 * (PROTOCORE_ESPNOW_MAX_PEERS, no heap). Same on host and ESP32.
15 * - **ESP32 binding (ARDUINO):** protocore_espnow_begin / add_peer / send /
16 * broadcast over the esp_now API, delivering decoded frames to a callback -
17 * which the application can bridge to WebSocket/SSE.
18 *
19 * ESP-NOW's raw payload is capped at 250 bytes; the 3-byte envelope leaves
20 * PROTOCORE_ESPNOW_MAX_PAYLOAD for data. No stdlib, no heap.
21 *
22 * @author Douglas Quigg (dstroy0)
23 * @date 2026
24 */
25
26#ifndef PROTOCORE_ESPNOW_H
27#define PROTOCORE_ESPNOW_H
28
29#include "protocore_config.h" // the entry point: protocore_types.h for the widths
30
31#if PROTOCORE_ENABLE_ESPNOW
32
34
35// PROTOCORE_ESPNOW_BORROW - the bytes this module runs out of - is stated in protocore_config.h, which sums
36// it into its arena. A caller takes them once and passes the pointer to every call. How they
37// are carved is this module's and is never named here.
38
39/** @brief Envelope header size (magic + type + length). */
40#define PROTOCORE_ESPNOW_HDR 3
41
42/** @brief Magic byte marking a library envelope. */
43#define PROTOCORE_ESPNOW_MAGIC 0xE5
44
45/** @brief Max application payload per frame (250-byte radio MTU minus the header). */
46#define PROTOCORE_ESPNOW_MAX_PAYLOAD 247
47
48/** @brief Decoded-frame callback: sender MAC, message type, payload. */
49typedef void (*protocore_espnow_recv_fn)(const uint8_t mac[6], uint8_t type, const uint8_t *payload, size_t len);
50
51/** @brief What encode takes: type, payload, len, out, cap. */
52typedef struct
53{
54 uint8_t type;
55 const uint8_t *payload;
56 size_t len;
57 uint8_t *out;
58 size_t cap;
59} EspnowEncodeArgs;
60
61/** @brief What decode takes: buf, len, type, payload, plen. */
62typedef struct
63{
64 const uint8_t *buf;
65 size_t len;
66 uint8_t *type;
67 const uint8_t **payload; ///< set to point inside buf (no copy)
68 size_t *plen;
69} EspnowDecodeArgs;
70
71/** @brief What peer_add takes: mac. */
72typedef struct
73{
74 const uint8_t *mac; ///< 6 bytes.
75} EspnowPeerAddArgs;
76
77/** @brief What peer_has takes: mac. */
78typedef struct
79{
80 const uint8_t *mac; ///< 6 bytes.
81} EspnowPeerHasArgs;
82
83/** @brief What peer_remove takes: mac. */
84typedef struct
85{
86 const uint8_t *mac; ///< 6 bytes.
87} EspnowPeerRemoveArgs;
88
89/** @brief What begin takes: channel, cb. */
90typedef struct
91{
92 uint8_t channel;
93 protocore_espnow_recv_fn cb;
94} EspnowBeginArgs;
95
96/** @brief What add_peer takes: mac. */
97typedef struct
98{
99 const uint8_t *mac; ///< 6 bytes.
100} EspnowAddPeerArgs;
101
102/** @brief What send takes: mac, type, payload, len. */
103typedef struct
104{
105 const uint8_t *mac; ///< 6 bytes.
106 uint8_t type;
107 const uint8_t *payload;
108 size_t len;
109} EspnowSendArgs;
110
111/** @brief What broadcast takes: type, payload, len. */
112typedef struct
113{
114 uint8_t type;
115 const uint8_t *payload;
116 size_t len;
117} EspnowBroadcastArgs;
118
119/**
120 * @brief ESP-NOW peer messaging with a typed envelope (PROTOCORE_ENABLE_ESPNOW).
121 *
122 * A caller sets the members a call takes, invokes it through ::Espnow with the bytes it runs
123 * out of, and reads the outcome off the same handle.
124 *
125 * Espnow.encode_args.type = ...;
126 * Espnow.encode_args.payload = ...;
127 * Espnow.encode_args.len = ...;
128 * Espnow.encode_args.out = ...;
129 * Espnow.encode_args.cap = ...;
130 * Espnow.encode(work);
131 * // Espnow.n is what the call reports
132 *
133 * @var EspnowNs::encode_args what encode takes: type, payload, len, out, cap
134 * @var EspnowNs::decode_args what decode takes: buf, len, type, payload, plen
135 * @var EspnowNs::peer_add_args what peer_add takes: mac
136 * @var EspnowNs::peer_has_args what peer_has takes: mac
137 * @var EspnowNs::peer_remove_args what peer_remove takes: mac
138 * @var EspnowNs::begin_args what begin takes: channel, cb
139 * @var EspnowNs::add_peer_args what add_peer takes: mac
140 * @var EspnowNs::send_args what send takes: mac, type, payload, len
141 * @var EspnowNs::broadcast_args what broadcast takes: type, payload, len
142 * @var EspnowNs::ok true on a well-formed envelope
143 * @var EspnowNs::n total bytes written to out, or 0 if it does not fit / payload too ...
144 * @var EspnowNs::encode frame a message: [magic][type][len] + payload
145 * @var EspnowNs::decode validate and unpack a framed message. Checks the magic and that the ...
146 * @var EspnowNs::peers_reset forget all registered peers
147 * @var EspnowNs::peer_add register mac (idempotent). false if the table is full
148 * @var EspnowNs::peer_has true if mac is in the peer registry
149 * @var EspnowNs::peer_remove remove mac from the registry. true if it was present
150 * @var EspnowNs::peer_count the number of registered peers
151 * @var EspnowNs::begin initialize ESP-NOW on channel and deliver decoded frames to cb. ...
152 * @var EspnowNs::add_peer add a unicast peer to both the registry and the radio
153 * @var EspnowNs::send encode and transmit a message to mac. true if queued to the radio
154 * @var EspnowNs::broadcast send to the broadcast address (all peers in range)
155 *
156 * @c work is PROTOCORE_ESPNOW_BORROW bytes the CALLER took, at an address it knows. It is not held past the call, so
157 * nothing here aliases it. How those bytes are carved is this module's and is never named here.
158 */
159typedef struct
160{
161 EspnowEncodeArgs encode_args;
162 EspnowDecodeArgs decode_args;
163 EspnowPeerAddArgs peer_add_args;
164 EspnowPeerHasArgs peer_has_args;
165 EspnowPeerRemoveArgs peer_remove_args;
166 EspnowBeginArgs begin_args;
167 EspnowAddPeerArgs add_peer_args;
168 EspnowSendArgs send_args;
169 EspnowBroadcastArgs broadcast_args;
170 proto_bool ok;
171 size_t n;
172} EspnowVars;
173
174/** @brief The operands and the outcome. */
175extern EspnowVars EspnowV;
176
177/** @brief The entries. */
178typedef struct
179{
180 void (*const encode)(uint8_t *work);
181 void (*const decode)(uint8_t *work);
182 void (*const peers_reset)(uint8_t *work);
183 void (*const peer_add)(uint8_t *work);
184 void (*const peer_has)(uint8_t *work);
185 void (*const peer_remove)(uint8_t *work);
186 void (*const peer_count)(uint8_t *work);
187 void (*const begin)(uint8_t *work);
188 void (*const add_peer)(uint8_t *work);
189 void (*const send)(uint8_t *work);
190 void (*const broadcast)(uint8_t *work);
191} EspnowNs;
192
193// What the table binds, defined once in the .c and taking one parameter each: everything
194// else an entry needs is an operand in EspnowV or a region of the borrow at a fixed offset.
195void protocore_espnow_encode(uint8_t *work);
196void protocore_espnow_decode(uint8_t *work);
197void protocore_espnow_peers_reset(uint8_t *work);
198void protocore_espnow_peer_add(uint8_t *work);
199void protocore_espnow_peer_has(uint8_t *work);
200void protocore_espnow_peer_remove(uint8_t *work);
201void protocore_espnow_peer_count(uint8_t *work);
202void protocore_espnow_begin(uint8_t *work);
203void protocore_espnow_add_peer(uint8_t *work);
204void protocore_espnow_send(uint8_t *work);
205void protocore_espnow_broadcast(uint8_t *work);
206
207// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
208// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
209// `Espnow.encode(work)` resolves to a named function and becomes a DIRECT call. An extern table
210// leaves the call indirect and the symbol live at every level, -O2 -flto included.
211static const EspnowNs Espnow __attribute__((unused)) = {
212 .encode = protocore_espnow_encode,
213 .decode = protocore_espnow_decode,
214 .peers_reset = protocore_espnow_peers_reset,
215 .peer_add = protocore_espnow_peer_add,
216 .peer_has = protocore_espnow_peer_has,
217 .peer_remove = protocore_espnow_peer_remove,
218 .peer_count = protocore_espnow_peer_count,
219 .begin = protocore_espnow_begin,
220 .add_peer = protocore_espnow_add_peer,
221 .send = protocore_espnow_send,
222 .broadcast = protocore_espnow_broadcast,
223};
224
225/** @brief The 6-octet broadcast address every peer accepts, and always a peer itself. */
226extern const uint8_t PROTOCORE_ESPNOW_BROADCAST[6];
227
228/**
229 * @brief The PROTOCORE_ESPNOW_BORROW bytes this module's state lives in.
230 *
231 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
232 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
233 * walks, so the state lasts the life of the program.
234 *
235 * @return the span.
236 */
237uint8_t *protocore_espnow_span(void);
238
240
241#endif // PROTOCORE_ENABLE_ESPNOW
242
243#endif // PROTOCORE_ESPNOW_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97