ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
wifi_sniffer.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 wifi_sniffer.h
6 * @brief 802.11 frame decode + traffic tally + RSSI roaming decision (PROTOCORE_ENABLE_WIFI_SNIFFER).
7 *
8 * The ESP32 can run its WiFi MAC in promiscuous mode and hand raw 802.11 frames to a callback. Turning
9 * those into a useful sniffer / traffic analyzer / RF-diagnostics panel means decoding the 802.11 MAC
10 * header (frame control type/subtype + flags, and the three addresses whose roles - receiver /
11 * transmitter / BSSID - depend on the ToDS/FromDS bits), tallying frames by type, and, for
12 * channel-agility roaming, deciding when a candidate AP is enough stronger than the current one to switch.
13 *
14 * This is that pure decode + decision layer; the promiscuous-mode radio callback belongs to the app. No
15 * heap, no stdlib, host-testable against captured frame bytes.
16 */
17
18#ifndef PROTOCORE_WIFI_SNIFFER_H
19#define PROTOCORE_WIFI_SNIFFER_H
20
21#include "protocore_config.h" // the entry point: protocore_types.h for the widths
22
23#if PROTOCORE_ENABLE_WIFI_SNIFFER
24
26
27// PROTOCORE_WIFI_SNIFFER_BORROW - the bytes this module runs out of - is stated in protocore_config.h, which sums
28// it into its arena. A caller takes them once and passes the pointer to every call. How they
29// are carved is this module's and is never named here.
30
31/** @brief 802.11 frame type (Frame Control bits 2-3). */
32#define WIFI_TYPE_MGMT 0 ///< management (beacon, probe, auth, assoc, ...).
33#define WIFI_TYPE_CTRL 1 ///< control (RTS/CTS/ACK, ...).
34#define WIFI_TYPE_DATA 2 ///< data.
35#define WIFI_TYPE_EXT 3 ///< extension.
36
37/** @brief Sentinel for "no frame heard yet" in WifiChannelSurvey::best_rssi. */
38#define PROTOCORE_WIFI_RSSI_NONE (-128)
39
40/** @brief A decoded 802.11 MAC header. Addresses not present for the frame's length are left zeroed. */
41typedef struct
42{
43 uint8_t version; ///< protocol version (FC bits 0-1).
44 uint8_t type; ///< WIFI_TYPE_*.
45 uint8_t subtype; ///< FC bits 4-7.
46 proto_bool to_ds; ///< to the distribution system.
47 proto_bool from_ds; ///< from the distribution system.
48 proto_bool retry; ///< retransmission.
49 proto_bool protected_frame; ///< the Protected Frame (WEP/WPA) flag.
50 uint8_t naddr; ///< number of addresses decoded (1..3).
51 uint8_t addr1[6]; ///< receiver / destination (role varies by DS bits).
52 uint8_t addr2[6]; ///< transmitter / source (present when naddr >= 2).
53 uint8_t addr3[6]; ///< BSSID / source / dest (present when naddr >= 3).
54} WifiFrame;
55
56/** @brief Running per-type frame tally. */
57typedef struct
58{
59 uint32_t mgmt;
60 uint32_t ctrl;
61 uint32_t data;
62 uint32_t other;
63 uint32_t total;
64} WifiStats;
65
66/** @brief Channel-hop schedule across [chan_first, chan_last]. */
67typedef struct
68{
69 uint8_t chan_first; ///< first channel of the sweep (1..14)
70 uint8_t chan_last; ///< last channel of the sweep (>= chan_first)
71 uint8_t channel; ///< channel currently dwelt on
72 uint16_t dwell_ms; ///< dwell per channel
73 uint32_t last_hop_ms; ///< when the current dwell started
74 uint32_t sweeps; ///< completed wraps back to chan_first
75} WifiScan;
76
77/** @brief What was heard on one channel during the survey. */
78typedef struct
79{
80 uint32_t frames; ///< frames decoded on this channel
81 int8_t best_rssi; ///< strongest RSSI seen (dBm); PROTOCORE_WIFI_RSSI_NONE if nothing heard
82 uint8_t best_bssid[6]; ///< transmitter of the strongest frame
83} WifiChannelSurvey;
84
85/** @brief Survey across the scanned channel range (index 0 == @c first). */
86typedef struct
87{
88 WifiChannelSurvey ch[PROTOCORE_WIFI_SNIFFER_MAX_CHANNELS];
89 uint8_t first; ///< channel represented by ch[0]
90 uint8_t count; ///< channels tracked (<= PROTOCORE_WIFI_SNIFFER_MAX_CHANNELS)
91} WifiSurvey;
92
93/** @brief What parse takes: frame, len, out. */
94typedef struct
95{
96 const uint8_t *frame;
97 size_t len;
98 WifiFrame *out;
99} WifiSnifferParseArgs;
100
101/** @brief What stats_reset takes: s. */
102typedef struct
103{
104 WifiStats *s;
105} WifiSnifferStatsResetArgs;
106
107/** @brief What stats_add takes: s, f. */
108typedef struct
109{
110 WifiStats *s;
111 const WifiFrame *f;
112} WifiSnifferStatsAddArgs;
113
114/** @brief What should_roam takes: cur_rssi, cand_rssi, hysteresis_db. */
115typedef struct
116{
117 int8_t cur_rssi;
118 int8_t cand_rssi;
119 uint8_t hysteresis_db;
120} WifiSnifferShouldRoamArgs;
121
122/** @brief What scan_init takes: s, first, last, dwell_ms, now_ms. */
123typedef struct
124{
125 WifiScan *s;
126 uint8_t first;
127 uint8_t last;
128 uint16_t dwell_ms;
129 uint32_t now_ms;
130} WifiSnifferScanInitArgs;
131
132/** @brief What scan_due takes: s, now_ms. */
133typedef struct
134{
135 const WifiScan *s;
136 uint32_t now_ms;
137} WifiSnifferScanDueArgs;
138
139/** @brief What scan_next takes: s, now_ms. */
140typedef struct
141{
142 WifiScan *s;
143 uint32_t now_ms;
144} WifiSnifferScanNextArgs;
145
146/** @brief What survey_reset takes: s, first, count. */
147typedef struct
148{
149 WifiSurvey *s;
150 uint8_t first;
151 uint8_t count;
152} WifiSnifferSurveyResetArgs;
153
154/** @brief What survey_add takes: s, channel, rssi, f. */
155typedef struct
156{
157 WifiSurvey *s;
158 uint8_t channel;
159 int8_t rssi;
160 const WifiFrame *f;
161} WifiSnifferSurveyAddArgs;
162
163/** @brief What survey_get takes: s, channel. */
164typedef struct
165{
166 const WifiSurvey *s;
167 uint8_t channel;
168} WifiSnifferSurveyGetArgs;
169
170/** @brief What survey_best takes: s, exclude_channel, out_channel, ... */
171typedef struct
172{
173 const WifiSurvey *s;
174 uint8_t exclude_channel;
175 uint8_t *out_channel;
176 int8_t *out_rssi;
177} WifiSnifferSurveyBestArgs;
178
179/** @brief What begin takes: first_chan, last_chan, dwell_ms. */
180typedef struct
181{
182 uint8_t first_chan;
183 uint8_t last_chan;
184 uint16_t dwell_ms;
185} WifiSnifferBeginArgs;
186
187/**
188 * @brief 802.11 frame decode + traffic tally + RSSI roaming decision (PROTOCORE_ENABLE_WIFI_SNIFFER).
189 *
190 * A caller sets the members a call takes, invokes it through ::WifiSniffer with the bytes it runs
191 * out of, and reads the outcome off the same handle.
192 *
193 * WifiSniffer.parse_args.frame = ...;
194 * WifiSniffer.parse_args.len = ...;
195 * WifiSniffer.parse_args.out = ...;
196 * WifiSniffer.parse(work);
197 * // WifiSniffer.ok is what the call reports
198 *
199 * @var WifiSnifferNs::parse_args what parse takes: frame, len, out
200 * @var WifiSnifferNs::stats_reset_args what stats_reset takes: s
201 * @var WifiSnifferNs::stats_add_args what stats_add takes: s, f
202 * @var WifiSnifferNs::should_roam_args what should_roam takes: cur_rssi, cand_rssi, hysteresis_db
203 * @var WifiSnifferNs::scan_init_args what scan_init takes: s, first, last, dwell_ms, now_ms
204 * @var WifiSnifferNs::scan_due_args what scan_due takes: s, now_ms
205 * @var WifiSnifferNs::scan_next_args what scan_next takes: s, now_ms
206 * @var WifiSnifferNs::survey_reset_args what survey_reset takes: s, first, count
207 * @var WifiSnifferNs::survey_add_args what survey_add takes: s, channel, rssi, f
208 * @var WifiSnifferNs::survey_get_args what survey_get takes: s, channel
209 * @var WifiSnifferNs::survey_best_args what survey_best takes: s, exclude_channel, out_channel,
210 * @var WifiSnifferNs::begin_args what begin takes: first_chan, last_chan, dwell_ms
211 * @var WifiSnifferNs::ok true if len >= 10 and frame is non-null; false otherwise
212 * @var WifiSnifferNs::value the new channel, or 0 if s is null
213 * @var WifiSnifferNs::ptr the pointer a call reports
214 * @var WifiSnifferNs::stats_out the running per-type tally the sniff fills
215 * @var WifiSnifferNs::survey_out the per-channel survey the sniff fills
216 * @var WifiSnifferNs::scan_out the channel-hop schedule the sniff is running
217 * @var WifiSnifferNs::parse decode the 802.11 MAC header of a captured frame. Requires at least ...
218 * @var WifiSnifferNs::stats_reset zero a tally
219 * @var WifiSnifferNs::stats_add fold one decoded frame into the tally
220 * @var WifiSnifferNs::should_roam channel-agility roaming decision
221 * @var WifiSnifferNs::scan_init start a sweep at first, dwelling dwell_ms per channel. Clamps to ...
222 * @var WifiSnifferNs::scan_due true once the current channel's dwell has elapsed (wrap-safe ...
223 * @var WifiSnifferNs::scan_next advance to the next channel (wrapping to chan_first and counting a ...
224 * @var WifiSnifferNs::survey_reset clear the survey to track count channels starting at first
225 * @var WifiSnifferNs::survey_add fold one captured frame (on channel, at rssi) into the survey. ...
226 * @var WifiSnifferNs::survey_get the survey entry for channel, or nullptr if it is outside the ...
227 * @var WifiSnifferNs::survey_best find the strongest channel heard, ignoring exclude_channel (pass 0 ...
228 * @var WifiSnifferNs::begin start a live channel-hopping sniff across [first_chan, last_chan]. ...
229 * @var WifiSnifferNs::tick hop to the next channel when the dwell has elapsed. Cheap to call ...
230 * @var WifiSnifferNs::end stop capture
231 * @var WifiSnifferNs::stats the running traffic tally (never null)
232 * @var WifiSnifferNs::survey the per-channel survey (never null)
233 * @var WifiSnifferNs::scan the live scan schedule (never null) - current channel, sweeps ...
234 *
235 * @c work is PROTOCORE_WIFI_SNIFFER_BORROW bytes the CALLER took, at an address it knows. It is not held past the call,
236 * so nothing here aliases it. How those bytes are carved is this module's and is never named here.
237 */
238typedef struct
239{
240 WifiSnifferParseArgs parse_args;
241 WifiSnifferStatsResetArgs stats_reset_args;
242 WifiSnifferStatsAddArgs stats_add_args;
243 WifiSnifferShouldRoamArgs should_roam_args;
244 WifiSnifferScanInitArgs scan_init_args;
245 WifiSnifferScanDueArgs scan_due_args;
246 WifiSnifferScanNextArgs scan_next_args;
247 WifiSnifferSurveyResetArgs survey_reset_args;
248 WifiSnifferSurveyAddArgs survey_add_args;
249 WifiSnifferSurveyGetArgs survey_get_args;
250 WifiSnifferSurveyBestArgs survey_best_args;
251 WifiSnifferBeginArgs begin_args;
252 proto_bool ok;
253 uint8_t value;
254 const WifiChannelSurvey *ptr;
255 const WifiStats *stats_out;
256 const WifiSurvey *survey_out;
257 const WifiScan *scan_out;
258} WifiSnifferVars;
259
260/** @brief The operands and the outcome. */
261extern WifiSnifferVars WifiSnifferV;
262
263/** @brief The entries. */
264typedef struct
265{
266 void (*const parse)(uint8_t *work);
267 void (*const stats_reset)(uint8_t *work);
268 void (*const stats_add)(uint8_t *work);
269 void (*const should_roam)(uint8_t *work);
270 void (*const scan_init)(uint8_t *work);
271 void (*const scan_due)(uint8_t *work);
272 void (*const scan_next)(uint8_t *work);
273 void (*const survey_reset)(uint8_t *work);
274 void (*const survey_add)(uint8_t *work);
275 void (*const survey_get)(uint8_t *work);
276 void (*const survey_best)(uint8_t *work);
277 void (*const begin)(uint8_t *work);
278 void (*const tick)(uint8_t *work);
279 void (*const end)(uint8_t *work);
280 void (*const stats)(uint8_t *work);
281 void (*const survey)(uint8_t *work);
282 void (*const scan)(uint8_t *work);
283} WifiSnifferNs;
284
285// What the table binds, defined once in the .c and taking one parameter each: everything
286// else an entry needs is an operand in WifiSnifferV or a region of the borrow at a fixed offset.
287void protocore_wifi_sniffer_parse(uint8_t *work);
288void protocore_wifi_sniffer_stats_reset(uint8_t *work);
289void protocore_wifi_sniffer_stats_add(uint8_t *work);
290void protocore_wifi_sniffer_should_roam(uint8_t *work);
291void protocore_wifi_sniffer_scan_init(uint8_t *work);
292void protocore_wifi_sniffer_scan_due(uint8_t *work);
293void protocore_wifi_sniffer_scan_next(uint8_t *work);
294void protocore_wifi_sniffer_survey_reset(uint8_t *work);
295void protocore_wifi_sniffer_survey_add(uint8_t *work);
296void protocore_wifi_sniffer_survey_get(uint8_t *work);
297void protocore_wifi_sniffer_survey_best(uint8_t *work);
298void protocore_wifi_sniffer_begin(uint8_t *work);
299void protocore_wifi_sniffer_tick(uint8_t *work);
300void protocore_wifi_sniffer_end(uint8_t *work);
301void protocore_wifi_sniffer_stats(uint8_t *work);
302void protocore_wifi_sniffer_survey(uint8_t *work);
303void protocore_wifi_sniffer_scan(uint8_t *work);
304
305// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
306// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
307// `WifiSniffer.parse(work)` resolves to a named function and becomes a DIRECT call. An extern table
308// leaves the call indirect and the symbol live at every level, -O2 -flto included.
309static const WifiSnifferNs WifiSniffer __attribute__((unused)) = {
310 .parse = protocore_wifi_sniffer_parse,
311 .stats_reset = protocore_wifi_sniffer_stats_reset,
312 .stats_add = protocore_wifi_sniffer_stats_add,
313 .should_roam = protocore_wifi_sniffer_should_roam,
314 .scan_init = protocore_wifi_sniffer_scan_init,
315 .scan_due = protocore_wifi_sniffer_scan_due,
316 .scan_next = protocore_wifi_sniffer_scan_next,
317 .survey_reset = protocore_wifi_sniffer_survey_reset,
318 .survey_add = protocore_wifi_sniffer_survey_add,
319 .survey_get = protocore_wifi_sniffer_survey_get,
320 .survey_best = protocore_wifi_sniffer_survey_best,
321 .begin = protocore_wifi_sniffer_begin,
322 .tick = protocore_wifi_sniffer_tick,
323 .end = protocore_wifi_sniffer_end,
324 .stats = protocore_wifi_sniffer_stats,
325 .survey = protocore_wifi_sniffer_survey,
326 .scan = protocore_wifi_sniffer_scan,
327};
328
329/**
330 * @brief The PROTOCORE_WIFI_SNIFFER_BORROW bytes this module's state lives in.
331 *
332 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
333 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
334 * walks, so the state lasts the life of the program.
335 *
336 * @return the span.
337 */
338uint8_t *protocore_wifi_sniffer_span(void);
339
341
342#endif // PROTOCORE_ENABLE_WIFI_SNIFFER
343
344#endif // PROTOCORE_WIFI_SNIFFER_H
#define PROTOCORE_WIFI_SNIFFER_MAX_CHANNELS
Channels tracked by the WiFi sniffer's per-channel survey (PROTOCORE_WIFI_SNIFFER_MAX_CHANNELS).
#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