(tl;dr version - the sniffer project is at this Github repo. Project is well documented there)
For various reasons, I wanted to build a portable device to sniff WiFi and Bluetooth signals. My goals for the project were for the device to be able to:
- "Survey" all devices in the area, and log the results for later analysis
- Continuously monitor for target broadcasters
- based on:
- WiFi SSID
- WiFi MAC prefix (Manufacturer's OUI)
- Bluetooth device name
- Bluetooth MAC prefix (Manufacturer's OUI)
- Bluetooth service UUID value
- Log detection for analysis
- Signal detection in an obvious way
- Have GPS to keep track of geographic location (as well as timekeeping)
I found a couple of projects on Github that looked kind of promising, but not exactly what I was looking for. One seemed a bit more promising than the others at first glance. The project developer was happy to sell pre-built hardware (with a buzzer for alerts), but could run on just an ESP32 dev board. I happened to have some ESP32-S3s around, so I downloaded the code and gave it a try.
It didn't work as advertised - it didn't detect what it was designed to detect, but picked up other things. A quick look at the code showed that the WiFi detection was a bit of a dog's breakfast - no differentiation between 'data' and 'management' frames, incorrect offsets into the packets, etc. I considered starting with that project, forking it and sending PRs to the upstream to help contribute. Then I noticed that project had no license. Sigh.
Kids - if you put something on Github, you really need to select a license. If your project is "just for yourself", mark it private! The Github T&C's are very clear about this - no license declaration means the entire repo is "all rights reserved". It's unclear if it's even legal to clone and install; changes, mods, etc. are right out.
I opened an issue asking the dev to add a license, but crickets. Given that, I decided to just start from scratch.
First Prototype
I started a new project with a Seeed XIAO ESP32-S3. Why that particular dev board? Small size, cheap, fast, WiFi/BT built in, and I had some in my bin. Capabilities that were of big interest to me were the 8MB PSRAM and 8MB Flash. Lots of resources for an embedded device. (Who else remembers 4K of RAM being extravagant!!)
My first decision was development platform. I chose Arduino. WTF - why Arduino? Aren't I a grown up? It was because I can't stand the way Copilot (and the AI mentality in general) has been shoved into VS Code. It's unfortunate; I had been a huge fan of VS Code for a very long time, but can't even bring myself to open it anymore.
(I discovered Zed after I was well into this project. Too bad I was so far along - I would've happily used it. Can't recommend it enough over VS Code.)
The first prototype was made on perfboard and consisted of:
- an EPS32-S3
- a cheap GPS module (with serial NMEA interface)
- 2 RGB LEDs
- 6 N-channel FETs (to drive the LEDs), current limiting resistors
Using this platform, the first pass of the code was developed. The main focus was the "survey" function and GPS integration. I took it wardriving a few times and found it to be a pretty good start.
Prototype to PCB
I felt the concept was good enough and ready for a real PCB with one change: I liked the way the two LEDs worked for signaling conditions, but didn't like needing 6 FETs to drive them. I remembered I had some through-hole WS2812 addressable LEDs, and using them would really simplify the hardware. It was important to me for the entire design to use through-hole components to simplify construction for anyone with basic soldering skills.
With the current US trade situation, I decided to go with OSHPark to make the boards. I used KiCAD for the simple schematic and layout and sent the gerbers out for fab. Three weeks and $19 later I had 3 boards. They looked great! It took just a few minutes to put one together for testing and continued development. More wardriving!
Enclosure
OpenSCAD was used to design a case to hold the EPS32, GPS module, and their two antennae. I had originally thought of adding a battery, but decided against it in the end. The first 3D version was very utilitarian and had some minor dimensional issues:
Notes about the code
The Github repo is linked above at the top of this post for reference.
The code is pretty straight forward with a few interesting features:
- The 8MB EPS32 Flash is custom partitioned: 4MB for application, 4MB for a filesystem.
- Most dynamic memory allocations are made in PSRAM (a bit more below)
- Logs, survey results, config, etc. are stored in the Flash filesystem. Most files are JSON with the logs being plain text.
- There is a fairly rich command line interface that can be used to start/stop scans and surveys, manage the filesystem, and get basic diagnostic status information.
The repo also contains a companion Python script that is used to pull survey results and populate a Sqlite database (examples and demo in the next post).
About that PSRAM
By default, if a developer allocates memory using `new`, that memory is allocated from the internal SRAM - a limited resource. The `ps_malloc()` statement will allocate from the external PSRAM chip. It's just a bit slower, but with 8MB to play with it's worth it.
The code makes much use of STL containers, again allocated from SRAM by default. A bit of extra code is needed to get them to allocate from PSRAM. This is from `alloc.h`:
namespace flk
{
template <class T>
class psramAlloc
{
public:
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t ;
using propagate_on_container_move_assignment = std::true_type;
// use ps_malloc() to force allocation from external PSRAM chip
T* allocate(size_type n, const void* hint = 0)
{
return static_cast<T*>(ps_malloc(n*sizeof(T)));
}
// matching heap release to ps_malloc is free()
void deallocate(T* p, size_type n)
{
free (p);
}
// The full external PSRAM chip is only 8Meg, and there's no virtual memory, so don't think about
// setting max size to MAX_UINT
size_type max_size() const
{
return (size_type(0x7fffff / sizeof(T)));
}
// be sure to call constructor/destructors
void construct(T* p, const T& value) { _construct(p, value); }
void destroy(T* p) { _destroy(p); }
};
// to allocate std::string in external RAM instead of onboard SRAM. Slower, but SRAM is a limited resource
typedef std::basic_string<char, std::char_traits<char>, psramAlloc<std::string::value_type>> string;
};
The big thing to note here is that there's a custom allocator template class that calls `ps_malloc()` and `free()` instead of `new` and `delete`. Declaring an STL container to use the PSRAM allocator is just a little more complicated:
static std::map<flk::string, found_wifi_t, std::less<>, flk::psramAlloc<std::map<flk::string, found_wifi_t>::value_type>> wifiDevices;
This is for an STL map with a std::string as the key and found_wifi_t struct as the member. Also note that flk::string is just a std::string allocated from PSRAM (see the typedef above).
Next post
In the next post, I'll show examples of using the device.






Comments
Post a Comment