WiFi sniffing with Pi Zero (W) and ESP8266 pHAT


pHAT Sniffer

WiFi sniffing with Pi Zero (W) and ESP8266 pHAT

Whereas the ESP8266 has received a lot of interest in the IoT community as a standalone unit, I did not find much in terms of projects using the ESP8266 pHAT in conjunction with the Raspberry Pi. I thus decided to try to implement a WiFi sniffer, that utilizes the promiscuous mode of the ESP8266 to identify WiFi beacons/clients and presents the results in a web interface running on the Pi.

My starting point on the ESP8266 side of the project was the ESP8266 Mini Sniff project by Ray Burnette. Besides code cleanup and simplification, my main change was to make it output the sniffed beacons and clients in JSON format instead of visualizing it in ASCII.

The next step was to make a Python wrapper for communicating with the ESP8266 pHAT to retrieve JSON data and enrich it. Python communicates with the ESP8266 via serial to request and retrieve the data and via GPIO to reset it. Once the JSON data has been retrieved, the wrapper enriches the data by adding access point SSID and channel information to the clients and by adding MAC-based manufacturer information from the Wireshark manufacturer database.

With that in place, I coded a Flask web server that uses the Python wrapper to obtain sniffed WiFi beacons and clients and presents these in tables in separate tabs using Bootstrap and sortable.js. A screenshot of the client tab is shown below (cropped to only show my own devices).

Finally, the obligatory photo of the physical device: a Raspberry Pi Zero W + ESP8266 pHAT, mounted with hammer headers:

MIT-licensed project available under on GitHub: https://github.com/larsjuhljensen/phatsniffer


Great project! I will have to give it a try


Thanks :-)

One small thing to be aware of: it expects the pHAT to be on /dev/serial0. If you put the pHAT on a Pi Zero W or Pi 3, I believe it by default will be /dev/serial1, because the Bluetooth adapter is on /dev/serial0. You can swap them by adding dtoverlay=pi3-miniuart-bt to /boot/config.txt.

I’d like to make the Python wrapper automatically detect which serial port the pHAT is on, but I have not figured out a good way to do so. Also, I have read that /dev/serial1 is slower than /dev/serial0, so swapping the ports as described above is probably the better way to go in any case.


New version pushed to GitHub. The ESP8266 firmware has been updated to have a command interface to allow addition of more features in the future. The web interface has been extended with a new overview pane, which uses d3 to visualize the sniffed clients grouped by beacon.


Inspired by Hendrik Linka’s ESP8266-SSID-Text-Broadcast project, I extended the Arduino code to be able to send out fake WiFi beacon packets with user-specified SSIDs, while still actively sniffing. I also added functions to the Python wrapper to make this functionality easy to access and a Rickroll example. I do not plan to expose this via the web interface until some of the caveats below have been solved.


  • The SSIDs can currently only be up to 16 characters.
  • It is only possible to have one SSID per channel due to the data structure I have chosen.


This project sounds very promising. As I have no experience with this boards and currently wait shipping of the devices, I wonder if the RXcontrol.rate variable is the connection speed at which the individual bacon was send, and if it can be exposed along with the sum of bacons send by the emitting device.
The idea is to measure and calculate airtime usage of all beacons.


As far as I understand, the rate field encodes the supported data rates as a bit vector, so I don’t think it is what you’re after. But I could be wrong.


that would be fine, as beacons must be send at the basic data rate.
I’ll try to look into this, when I get up and running your project.

We have to check out how these rates are encoded. The 802.11 standard defines basic rates and supported rates.
Beacons are sent at the basic rate speed in order to reach all devices. The lower this basic rate is the more airtime it will require to transmit the 380 bytes.
The next factor is the interval the AP sends out this beacons. A common default value is 100ms.
So along with basic rate and a beacon counter for each BSSID it should be possible to calculate the management overhead. Al the rest is netto airtime that client devices can use to TX/RX data at the supported rates.


To implement a beacon counter, you’d have to change the code to run on only one specified channel. The current code loops over all channels to discover all clients and APs. However, to count beacons, you’d have to continuously listen to a single channel.


I understand and agree.
Counting only this single channel makes sense, as it’s the overhead of the used channel we want to know.
Then when it works for one fixed channel it would also make sense to loop through all channels. One second per channel would allow to count up to 9 beacons per BSSID (at standard 100ms interval) So we could draw a full-channel timeline with an 12-14 second update interval. In addition I expect that it would makes sense to separate higher level beacons from the low ones at the edge of around -75 dBm. Lower beacons act more like noise than a data paket.
Theoretically two Access points announcing beacons for two SSIDs at the same channel would already cover 10-15% of the available airtime. Now imagine 2-3 neighbour APs and client requests from 20-30 mobile devices, …

Let’s see what I can do within this sketches…