Wi-Fi SSID Receiver
Decode Wi-Fi beacons on a 2.4 GHz channel, list nearby networks, and plot their symbols.
Blocks used
Overview
Every Wi-Fi access point keeps announcing its network, whether or not anyone is connected. It typically sends a beacon frame every 100 time units of 1,024 µs, about ten times per second.[1] Each beacon carries the network name, the SSID of up to 32 bytes, and the hardware address of the access point, its BSSID.[2]
This flowgraph captures one channel at 20 MS/s and decodes its legacy Wi-Fi frames in a Python block written with NumPy. It handles the OFDM rates from 6 to 54 Mbps and, experimentally, 1 Mbps DSSS frames with the long preamble. Frames that pass their checksum fill a live network table in the note and a constellation of the recovered symbols, next to a spectrum and waterfall fed directly by the radio. Running it needs an antenna for the 2.4 GHz band, access points within reach, and a supported radio that covers 2.4 GHz at 20 MS/s, such as a HackRF One.[3]
How it works
At 20 MS/s, an OFDM symbol is 64 samples plus a 16-sample cyclic prefix, 4 µs in all, and its 64 subcarriers sit 312.5 kHz apart. Of these, 48 carry data and 4 carry known pilots.[4] The block keeps the end of each update so frames split between updates still decode. It works in four steps.
- Detection. Every OFDM frame opens with a 16-sample pattern repeated ten times. Correlating the signal with itself 16 samples later produces a plateau there, and the phase of that correlation gives a first estimate of the frequency offset.[4]
- Channel and header. Two copies of a known long training symbol follow. The phase between them refines the offset, and their spectrum divided by the known values gives the gain and phase of each subcarrier. The next symbol, always BPSK at code rate 1/2, holds the data rate and the frame length in bytes.[2]
- Data. Each data symbol is equalized, and a straight-line fit to the four pilot phases removes the rotation left by residual frequency and timing error.[4] The points become soft bits, which pass through the deinterleaver, a Viterbi decoder for the convolutional code, and the descrambler.[2]
- Checks. A frame counts only if its CRC-32 frame check sequence matches. Beacons and probe responses then give up their BSSID, their SSID, and the channel they advertise.[2]
A second path correlates the same samples with the 11-chip Barker code that spreads each bit of a 1 Mbps DSSS frame, then checks its header and frame checksums.[2]
What to look for
The spectrum and waterfall show the 20 MHz window around 2,437 MHz, channel 6 in the saved file. Wi-Fi arrives in bursts, so frames appear in the waterfall as thin streaks, not a steady carrier. Transmissions on neighboring channels, which sit 5 MHz apart,[2] spill into the window too, but only frames centered on the tuned channel decode.
When a network is first heard, the Python console prints its SSID, BSSID, channel, and data rate. The note's live table holds one row per BSSID, so several rows can share a name when a network spans several access points. A network that hides its name sends an empty SSID and shows as <hidden>. The power column is the power of the whole update that carried the frame, not of that access point alone.
The constellation shows the equalized data subcarriers of the last 50 OFDM symbols, taken only from frames that passed their checksum. A beacon sent at 6 Mbps is BPSK, so its points split into two tight clusters at −1 and +1 on the real axis.[2] Faster frames that decode add QPSK, 16-QAM, or 64-QAM grids. Frames sent with DSSS never appear here, and the display stays at the origin until the first OFDM frame decodes.
For the frame formats and a complete GNU Radio receiver, see the sources below.
Going further
Counting frames per access point shows what share of its beacons the receiver decodes. At the usual beacon interval of 102.4 ms, a nearby access point whose beacons all decode adds about 9.8 frames per second, a little more if it also answers probe requests.
Give a Python block one input and no outputs, connect it to the SDR output so it runs once per update, and paste the code below. Every 10 seconds it reads the decoder's network table from the environment and prints the frame rate of each access point on the tuned channel, starting at 0.0 for one heard for the first time.
import time
_INTERVAL = 10.0
_last_time = None
_last_counts = {}
def compute(ctx):
global _last_time, _last_counts
now = time.monotonic()
if _last_time is not None and now - _last_time < _INTERVAL:
return
channel = ctx.env.get("wifi_receiver", {}).get("channel")
networks = ctx.env.get("wifi_networks", {}).get("accessPoints", [])
counts = {
(ap["bssid"], ap["ssid"]): int(ap["framesSeen"])
for ap in networks
if ap["tunedChannel"] == channel
}
if _last_time is not None:
elapsed = now - _last_time
for (bssid, ssid), count in sorted(counts.items()):
rate = (count - _last_counts.get((bssid, ssid), count)) / elapsed
print(f"{ssid} ({bssid}): {rate:.1f} frames/s")
_last_time, _last_counts = now, counts
Frequency and gain can change freely. Channel n from 1 to 13 sits at 2,412 MHz + 5 MHz × (n − 1), and channel 14 at 2,484 MHz.[2] The sample rate must stay at 20 MS/s, because the symbol length and subcarrier spacing are written into the code. The Python block reference explains how a block reads the environment.
The decoder handles only the legacy formats. Frames in the newer 802.11n, ac, and ax formats, the faster CCK rates of 802.11b, short-preamble DSSS frames, and frames longer than 1,024 bytes are skipped, and 5 GHz channels are not mapped. The code favors readable, commented steps over speed. When the compute ratio the console prints every five seconds rises above 1, the decoder falls behind, and Buffer Loss on the SDR block rises.
References
References
-
T. Carpenter, "802.11 beacon intervals: The real story," CWNP, May 2014. ↩
-
Part 11: Wireless LAN Medium Access Control (MAC) and Physical Layer (PHY) Specifications, IEEE, IEEE Std 802.11-2020, Feb. 2021, Clauses 9, 15, and 17, doi:10.1109/IEEESTD.2021.9363693. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Great Scott Gadgets, "HackRF One," HackRF documentation. hackrf.readthedocs.io/en/latest/hackrf_one.html ↩
-
B. Bloessl, M. Segata, C. Sommer, and F. Dressler, "An IEEE 802.11a/g/p OFDM receiver for GNU Radio," in Proc. 2nd Workshop Softw. Radio Implementation Forum (SRIF), Hong Kong, China, Aug. 2013, pp. 9-16, doi:10.1145/2491246.2491248, author copy. ↩ ↩2 ↩3