ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
wifi_sniffer.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 wifi_sniffer.cpp
6 * @brief 802.11 frame decode + traffic tally + RSSI roaming decision (see wifi_sniffer.h).
7 */
8
10
11#if PC_ENABLE_WIFI_SNIFFER
12
13#include <string.h>
14
15#if PC_ENABLE_PROMISC
16#include "services/radio/promisc/promisc.h" // the promiscuous-capture owner
17#include "services/system/clock.h" // pc_millis - the monotonic source
18#endif
19
20bool pc_wifi_parse(const uint8_t *frame, size_t len, WifiFrame *out)
21{
22 if (!frame || !out || len < 10) // FrameControl(2) + Duration(2) + Address1(6)
23 {
24 return false;
25 }
26
27 memset(out, 0, sizeof(*out));
28
29 uint8_t fc0 = frame[0];
30 uint8_t fc1 = frame[1];
31 out->version = fc0 & 0x03;
32 out->type = (fc0 >> 2) & 0x03;
33 out->subtype = (fc0 >> 4) & 0x0F;
34 out->to_ds = (fc1 & 0x01) != 0;
35 out->from_ds = (fc1 & 0x02) != 0;
36 out->retry = (fc1 & 0x08) != 0;
37 out->protected_frame = (fc1 & 0x40) != 0;
38
39 // Address1 always present at this length (offset 4).
40 memcpy(out->addr1, frame + 4, 6);
41 out->naddr = 1;
42 if (len >= 16)
43 {
44 memcpy(out->addr2, frame + 10, 6);
45 out->naddr = 2;
46 }
47 if (len >= 24)
48 {
49 memcpy(out->addr3, frame + 16, 6);
50 out->naddr = 3;
51 }
52 return true;
53}
54
55void pc_wifi_stats_reset(WifiStats *s)
56{
57 if (s)
58 {
59 memset(s, 0, sizeof(*s));
60 }
61}
62
63void pc_wifi_stats_add(WifiStats *s, const WifiFrame *f)
64{
65 if (!s || !f)
66 {
67 return;
68 }
69 switch (f->type)
70 {
71 case WifiType::WIFI_TYPE_MGMT:
72 s->mgmt++;
73 break;
74 case WifiType::WIFI_TYPE_CTRL:
75 s->ctrl++;
76 break;
77 case WifiType::WIFI_TYPE_DATA:
78 s->data++;
79 break;
80 default:
81 s->other++;
82 break;
83 }
84 s->total++;
85}
86
87bool pc_wifi_should_roam(int8_t cur_rssi, int8_t cand_rssi, uint8_t hysteresis_db)
88{
89 // Both are negative dBm (stronger = closer to 0). Roam only if the candidate clears the current
90 // by more than the hysteresis, computed in a wide signed type to avoid int8 overflow.
91 int32_t margin = (int32_t)cand_rssi - (int32_t)cur_rssi;
92 return margin > (int32_t)hysteresis_db;
93}
94
95// --- Channel-hop scan schedule ----------------------------------------------------------
96
97namespace
98{
99uint8_t clamp_channel(uint8_t c)
100{
101 if (c < 1)
102 {
103 return 1;
104 }
105 if (c > 14)
106 {
107 return 14;
108 }
109 return c;
110}
111} // namespace
112
113void pc_wifi_scan_init(WifiScan *s, uint8_t first, uint8_t last, uint16_t dwell_ms, uint32_t now_ms)
114{
115 if (!s)
116 {
117 return;
118 }
119 s->chan_first = clamp_channel(first);
120 s->chan_last = clamp_channel(last);
121 if (s->chan_last < s->chan_first)
122 {
123 s->chan_last = s->chan_first;
124 }
125 s->channel = s->chan_first;
126 s->dwell_ms = dwell_ms;
127 s->last_hop_ms = now_ms;
128 s->sweeps = 0;
129}
130
131bool pc_wifi_scan_due(const WifiScan *s, uint32_t now_ms)
132{
133 if (!s)
134 {
135 return false;
136 }
137 // Unsigned subtraction is correct across a millis() rollover.
138 return (now_ms - s->last_hop_ms) >= s->dwell_ms;
139}
140
141uint8_t pc_wifi_scan_next(WifiScan *s, uint32_t now_ms)
142{
143 if (!s)
144 {
145 return 0;
146 }
147 if (s->channel >= s->chan_last)
148 {
149 s->channel = s->chan_first;
150 s->sweeps++;
151 }
152 else
153 {
154 s->channel++;
155 }
156 s->last_hop_ms = now_ms;
157 return s->channel;
158}
159
160// --- Per-channel RSSI survey ------------------------------------------------------------
161
162void pc_wifi_survey_reset(WifiSurvey *s, uint8_t first, uint8_t count)
163{
164 if (!s)
165 {
166 return;
167 }
168 memset(s, 0, sizeof(*s));
169 s->first = clamp_channel(first);
170 s->count = (count > PC_WIFI_SNIFFER_MAX_CHANNELS) ? (uint8_t)PC_WIFI_SNIFFER_MAX_CHANNELS : count;
171 for (uint8_t i = 0; i < PC_WIFI_SNIFFER_MAX_CHANNELS; i++)
172 {
173 s->ch[i].best_rssi = PC_WIFI_RSSI_NONE;
174 }
175}
176
177namespace
178{
179// Index of `channel` in the survey table, or -1 if outside the tracked range.
180int survey_index(const WifiSurvey *s, uint8_t channel)
181{
182 if (!s || channel < s->first)
183 {
184 return -1;
185 }
186 int idx = (int)channel - (int)s->first;
187 if (idx >= (int)s->count)
188 {
189 return -1;
190 }
191 return idx;
192}
193} // namespace
194
195void pc_wifi_survey_add(WifiSurvey *s, uint8_t channel, int8_t rssi, const WifiFrame *f)
196{
197 int idx = survey_index(s, channel);
198 if (idx < 0)
199 {
200 return;
201 }
202 WifiChannelSurvey *e = &s->ch[idx];
203 e->frames++;
204 if (e->best_rssi == PC_WIFI_RSSI_NONE || rssi > e->best_rssi)
205 {
206 e->best_rssi = rssi;
207 // The transmitter address is the AP for a beacon; only present once addr2 was decoded.
208 if (f && f->naddr >= 2)
209 {
210 memcpy(e->best_bssid, f->addr2, 6);
211 }
212 }
213}
214
215const WifiChannelSurvey *pc_wifi_survey_get(const WifiSurvey *s, uint8_t channel)
216{
217 int idx = survey_index(s, channel);
218 return (idx < 0) ? nullptr : &s->ch[idx];
219}
220
221bool pc_wifi_survey_best(const WifiSurvey *s, uint8_t exclude_channel, uint8_t *out_channel, int8_t *out_rssi)
222{
223 if (!s)
224 {
225 return false;
226 }
227 bool found = false;
228 uint8_t best_ch = 0;
229 int8_t best = PC_WIFI_RSSI_NONE;
230 for (uint8_t i = 0; i < s->count; i++)
231 {
232 uint8_t ch = (uint8_t)(s->first + i);
233 if (ch == exclude_channel || s->ch[i].best_rssi == PC_WIFI_RSSI_NONE)
234 {
235 continue;
236 }
237 if (!found || s->ch[i].best_rssi > best)
238 {
239 found = true;
240 best = s->ch[i].best_rssi;
241 best_ch = ch;
242 }
243 }
244 if (found)
245 {
246 if (out_channel)
247 {
248 *out_channel = best_ch;
249 }
250 if (out_rssi)
251 {
252 *out_rssi = best;
253 }
254 }
255 return found;
256}
257
258#if PC_ENABLE_PROMISC
259
260/** @brief Owned state for the live channel-hopping sniff. */
261struct WifiSnifferCtx
262{
263 WifiStats stats;
264 WifiSurvey survey;
265 WifiScan scan;
266 bool running;
267};
268
269namespace
270{
271WifiSnifferCtx s_sniffer = {};
272
273// The promisc sink: decode, tally, survey. Runs in the WiFi driver's callback context, so it only
274// touches the owned context - no allocation, no blocking.
275void sniffer_sink(const uint8_t *frame, uint16_t len, int8_t rssi, uint8_t channel)
276{
277 WifiFrame f;
278 if (!pc_wifi_parse(frame, len, &f))
279 {
280 return;
281 }
282 pc_wifi_stats_add(&s_sniffer.stats, &f);
283 pc_wifi_survey_add(&s_sniffer.survey, channel, rssi, &f);
284}
285} // namespace
286
287bool pc_wifi_sniffer_begin(uint8_t first_chan, uint8_t last_chan, uint16_t dwell_ms)
288{
289 uint32_t now = pc_millis();
290 pc_wifi_stats_reset(&s_sniffer.stats);
291 pc_wifi_scan_init(&s_sniffer.scan, first_chan, last_chan, dwell_ms, now);
292 pc_wifi_survey_reset(&s_sniffer.survey, s_sniffer.scan.chan_first,
293 (uint8_t)(s_sniffer.scan.chan_last - s_sniffer.scan.chan_first + 1));
294 s_sniffer.running = pc_promisc_begin(s_sniffer.scan.channel, sniffer_sink);
295 return s_sniffer.running;
296}
297
298void pc_wifi_sniffer_tick(void)
299{
300 if (!s_sniffer.running)
301 {
302 return;
303 }
304 uint32_t now = pc_millis();
305 if (!pc_wifi_scan_due(&s_sniffer.scan, now))
306 {
307 return;
308 }
309 pc_promisc_set_channel(pc_wifi_scan_next(&s_sniffer.scan, now));
310}
311
312void pc_wifi_sniffer_end(void)
313{
314 if (!s_sniffer.running)
315 {
316 return;
317 }
318 pc_promisc_end();
319 s_sniffer.running = false;
320}
321
322const WifiStats *pc_wifi_sniffer_stats(void)
323{
324 return &s_sniffer.stats;
325}
326
327const WifiSurvey *pc_wifi_sniffer_survey(void)
328{
329 return &s_sniffer.survey;
330}
331
332const WifiScan *pc_wifi_sniffer_scan(void)
333{
334 return &s_sniffer.scan;
335}
336
337#endif // PC_ENABLE_PROMISC
338
339#endif // PC_ENABLE_WIFI_SNIFFER
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
Wi-Fi promiscuous (monitor) capture (PC_ENABLE_PROMISC) - passive 802.11 sniffing.
#define PC_WIFI_SNIFFER_MAX_CHANNELS
Channels tracked by the WiFi sniffer's per-channel survey (PC_WIFI_SNIFFER_MAX_CHANNELS).
802.11 frame decode + traffic tally + RSSI roaming decision (PC_ENABLE_WIFI_SNIFFER).