ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
wifi_sniffer.h
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.h
6 * @brief 802.11 frame decode + traffic tally + RSSI roaming decision (PC_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"
22#include <stddef.h>
23#include <stdint.h>
24
25#if PC_ENABLE_WIFI_SNIFFER
26
27/** @brief 802.11 frame type (Frame Control bits 2-3). */
28struct WifiType
29{
30 static constexpr uint8_t WIFI_TYPE_MGMT = 0; ///< management (beacon, probe, auth, assoc, ...).
31 static constexpr uint8_t WIFI_TYPE_CTRL = 1; ///< control (RTS/CTS/ACK, ...).
32 static constexpr uint8_t WIFI_TYPE_DATA = 2; ///< data.
33 static constexpr uint8_t WIFI_TYPE_EXT = 3; ///< extension.
34};
35
36/** @brief A decoded 802.11 MAC header. Addresses not present for the frame's length are left zeroed. */
37struct WifiFrame
38{
39 uint8_t version; ///< protocol version (FC bits 0-1).
40 uint8_t type; ///< WIFI_TYPE_*.
41 uint8_t subtype; ///< FC bits 4-7.
42 bool to_ds; ///< to the distribution system.
43 bool from_ds; ///< from the distribution system.
44 bool retry; ///< retransmission.
45 bool protected_frame; ///< the Protected Frame (WEP/WPA) flag.
46 uint8_t naddr; ///< number of addresses decoded (1..3).
47 uint8_t addr1[6]; ///< receiver / destination (role varies by DS bits).
48 uint8_t addr2[6]; ///< transmitter / source (present when naddr >= 2).
49 uint8_t addr3[6]; ///< BSSID / source / dest (present when naddr >= 3).
50};
51
52/**
53 * @brief Decode the 802.11 MAC header of a captured frame.
54 *
55 * Requires at least the Frame Control + Duration + Address1 (10 bytes); Address2 is decoded at >= 16
56 * bytes and Address3 at >= 24 bytes, with @p out->naddr reporting how many were present.
57 * @return true if @p len >= 10 and @p frame is non-null; false otherwise.
58 */
59bool pc_wifi_parse(const uint8_t *frame, size_t len, WifiFrame *out);
60
61/** @brief Running per-type frame tally. */
62struct WifiStats
63{
64 uint32_t mgmt;
65 uint32_t ctrl;
66 uint32_t data;
67 uint32_t other;
68 uint32_t total;
69};
70
71/** @brief Zero a tally. */
72void pc_wifi_stats_reset(WifiStats *s);
73
74/** @brief Fold one decoded frame into the tally. */
75void pc_wifi_stats_add(WifiStats *s, const WifiFrame *f);
76
77/**
78 * @brief Channel-agility roaming decision.
79 * @return true if @p cand_rssi exceeds @p cur_rssi by more than @p hysteresis_db (so a switch is worth it).
80 */
81bool pc_wifi_should_roam(int8_t cur_rssi, int8_t cand_rssi, uint8_t hysteresis_db);
82
83// --- Channel-hop scan schedule ----------------------------------------------------------
84//
85// A survey walks a channel range, dwelling on each long enough to hear its traffic, then wraps.
86// The schedule is pure (wrap-safe time math on pc_millis()); driving the radio is the binding
87// below.
88
89/** @brief Channel-hop schedule across [chan_first, chan_last]. */
90struct WifiScan
91{
92 uint8_t chan_first; ///< first channel of the sweep (1..14)
93 uint8_t chan_last; ///< last channel of the sweep (>= chan_first)
94 uint8_t channel; ///< channel currently dwelt on
95 uint16_t dwell_ms; ///< dwell per channel
96 uint32_t last_hop_ms; ///< when the current dwell started
97 uint32_t sweeps; ///< completed wraps back to chan_first
98};
99
100/** @brief Start a sweep at @p first, dwelling @p dwell_ms per channel. Clamps to 1..14 and first<=last. */
101void pc_wifi_scan_init(WifiScan *s, uint8_t first, uint8_t last, uint16_t dwell_ms, uint32_t now_ms);
102
103/** @brief True once the current channel's dwell has elapsed (wrap-safe against a millis rollover). */
104bool pc_wifi_scan_due(const WifiScan *s, uint32_t now_ms);
105
106/**
107 * @brief Advance to the next channel (wrapping to chan_first and counting a sweep) and restart the dwell.
108 * @return the new channel, or 0 if @p s is null.
109 */
110uint8_t pc_wifi_scan_next(WifiScan *s, uint32_t now_ms);
111
112// --- Per-channel RSSI survey ------------------------------------------------------------
113
114/** @brief What was heard on one channel during the survey. */
115struct WifiChannelSurvey
116{
117 uint32_t frames; ///< frames decoded on this channel
118 int8_t best_rssi; ///< strongest RSSI seen (dBm); PC_WIFI_RSSI_NONE if nothing heard
119 uint8_t best_bssid[6]; ///< transmitter of the strongest frame
120};
121
122/** @brief Sentinel for "no frame heard yet" in WifiChannelSurvey::best_rssi. */
123#define PC_WIFI_RSSI_NONE (-128)
124
125/** @brief Survey across the scanned channel range (index 0 == @c first). */
126struct WifiSurvey
127{
128 WifiChannelSurvey ch[PC_WIFI_SNIFFER_MAX_CHANNELS];
129 uint8_t first; ///< channel represented by ch[0]
130 uint8_t count; ///< channels tracked (<= PC_WIFI_SNIFFER_MAX_CHANNELS)
131};
132
133/** @brief Clear the survey to track @p count channels starting at @p first. */
134void pc_wifi_survey_reset(WifiSurvey *s, uint8_t first, uint8_t count);
135
136/** @brief Fold one captured frame (on @p channel, at @p rssi) into the survey. Out-of-range channels are ignored. */
137void pc_wifi_survey_add(WifiSurvey *s, uint8_t channel, int8_t rssi, const WifiFrame *f);
138
139/** @brief The survey entry for @p channel, or nullptr if it is outside the tracked range. */
140const WifiChannelSurvey *pc_wifi_survey_get(const WifiSurvey *s, uint8_t channel);
141
142/**
143 * @brief Find the strongest channel heard, ignoring @p exclude_channel (pass 0 to exclude nothing).
144 * @return true if any channel had traffic; @p out_channel / @p out_rssi are then set.
145 */
146bool pc_wifi_survey_best(const WifiSurvey *s, uint8_t exclude_channel, uint8_t *out_channel, int8_t *out_rssi);
147
148#if PC_ENABLE_PROMISC
149// --- Live capture binding (ESP32) -------------------------------------------------------
150//
151// Drives the promiscuous-capture owner (services/radio/promisc) rather than installing a second radio
152// callback: pc_promisc_begin() delivers each frame to an internal sink that decodes it
153// (pc_wifi_parse), tallies it (pc_wifi_stats_add), and folds it into the survey. Call
154// pc_wifi_sniffer_tick() from the loop to hop channels on the dwell schedule.
155//
156// Concurrency: the sink runs in the Wi-Fi driver's callback context, so the stats and survey are
157// updated underneath a reader. They are deliberately lock-free - the accessors below hand back a
158// live snapshot that may be a frame or two ahead of a value read a moment earlier, which is the
159// right trade for a passive diagnostics panel (a lock in the radio callback would risk stalling
160// capture). Counters are aligned 32-bit and RSSI is a byte, so a reader sees whole values, never
161// torn ones; do not treat a report as an instantaneous consistent cut across channels.
162
163/**
164 * @brief Start a live channel-hopping sniff across [first_chan, last_chan].
165 *
166 * Requires the radio to be up (init_wifi_physical() or init_wifi_radio_physical()).
167 * Resets the stats + survey.
168 * @return true if capture started.
169 */
170bool pc_wifi_sniffer_begin(uint8_t first_chan, uint8_t last_chan, uint16_t dwell_ms);
171
172/** @brief Hop to the next channel when the dwell has elapsed. Cheap to call every loop. */
173void pc_wifi_sniffer_tick(void);
174
175/** @brief Stop capture. */
176void pc_wifi_sniffer_end(void);
177
178/** @brief The running traffic tally (never null). */
179const WifiStats *pc_wifi_sniffer_stats(void);
180
181/** @brief The per-channel survey (never null). */
182const WifiSurvey *pc_wifi_sniffer_survey(void);
183
184/** @brief The live scan schedule (never null) - current channel, sweeps completed. */
185const WifiScan *pc_wifi_sniffer_scan(void);
186#endif // PC_ENABLE_PROMISC
187
188#endif // PC_ENABLE_WIFI_SNIFFER
189#endif // PROTOCORE_WIFI_SNIFFER_H
User-facing configuration for ProtoCore.
#define PC_WIFI_SNIFFER_MAX_CHANNELS
Channels tracked by the WiFi sniffer's per-channel survey (PC_WIFI_SNIFFER_MAX_CHANNELS).