Skip to content

slofunk/hb0127

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 

Repository files navigation

Sea Five Wardriver — WiGLE Edition

(Disclaimer this was heavily AI assisted) A community extension of the HackerBox 0127 "Sea Five" example sketches. It picks up where the example leaves off: every network gets stamped with its GPS position at the moment it's detected, BLE scanning is filled in, and the output is WiGLE-1.4 CSV you can upload directly to wigle.net.

Two files, same names and same shape as the originals, so you can diff them side by side and see exactly what changed. No new libraries to install.

---

Where the example leaves off

The example sketches that ship with Sea Five do their job really well. They prove out every peripheral on the board — GPS, OLED, SD, both radios — and they demonstrate the two microcontrollers talking to each other in both directions, which is the genuinely tricky part of a dual-MCU design. If you're holding a freshly soldered board and want to know whether you got it right, that's exactly the code you want. The // Scan BLE / TBD comment is the folks at HackerBoxes leaving the door open on purpose, and this is one answer to that invitation.

The step from there to a working wardriver comes down to one thing: positions need to be attached to individual networks.

Right now the output file looks like this:

Date/Time: 2026-07-15 14:22:31
Sats: 9  HDOP: 1.100000
Lat: 39.00000  Long: -85.00000
 6, AA:BB:CC:DD:EE:FF MyNetwork -61 WPA2
11, 11:22:33:44:55:66 OtherNet -74 WPA2
Date/Time: 2026-07-15 14:22:46
...

It reads like the coordinates are a header for the networks below them. They're not, and the reason is a neat lesson in how concurrency shows up in embedded code even when you didn't ask for it.

MCU A's loop is read\_intercom() then ScanWiFi(). read\_intercom() drains everything MCU B sent while A was busy and writes each line to the card as it arrives. Then scanNetworks() blocks for several seconds and writes its own finds. Loop, repeat. So each cycle produces a burst of B's backlog — GPS status lines, in arrival order — followed by A's networks. B sends GPS every 3 seconds but A only reads between scans, so several GPS lines queue up and land together.

The grouping is an emergent property of the timing, not a record structure. That means the position is offset in two directions at once: it's a snapshot of wherever B was when it last transmitted, possibly 15 seconds stale by the time it reaches the card, and every AP listed underneath was seen across the following 15 seconds. At 45 mph that's a quarter-mile of road sharing one coordinate.

Which is why this can't be solved with a clever parser after the fact. The position has to be attached at the moment of detection.

---

How this version works

The hardware decides the architecture

This is the key thing to understand about the Sea Five board, and it's why you can't simply drop in firmware from other ESP32-C5 wardriver projects:

MCU A MCU B
SD card ✅ SPI (sck 8, miso 9, mosi 10, cs 5)
GPS ✅ UART (rx 5, tx 4)
OLED ✅ I²C (sda 10, scl 6)
Wi-Fi 2.4 GHz 5 GHz
BLE
USB console ✅ (FT232RL) ⚠️ shares UART0 with the intercom
Intercom UART1, gpio 0/1 UART0, gpio 0/1

The peripherals are split across two physically separate chips. A owns the card. B owns the GPS. Most single-board wardriver firmware assumes SD + GPS + display all hang off one MCU, and no amount of pin remapping crosses a chip boundary — a #define can't reach the other processor. Sea Five's layout is its own thing, and the software has to be shaped to fit it.

So the data flow follows the wiring:

   MCU B                              MCU A
   ┌────────────────┐                 ┌────────────────┐
   │ GPS ──► fix    │  $G,fix,lat,... │  cached fix    │
   │        │       │ ──────────────► │       │        │
   │        ▼       │   (every 1s)    │       ▼        │
   │ 5GHz ──► stamp │                 │ 2.4GHz ► stamp │
   │ BLE  ──► stamp │  $N,<csv row>   │       │        │
   │        │       │ ──────────────► │       ▼        │
   │        ▼       │                 │   SD CARD      │
   │  OLED  ◄───────┼─── <XNNNN> ─────┼── SD status +  │
   └────────────────┘   (stats back)  │   2.4G count   │
                                      └────────────────┘

B stamps its own records — it holds the fix in RAM the instant it finds something, so there's zero latency. B pushes the fix to A once a second; A caches it and stamps its own 2.4 GHz finds. Every record carries its own position before it travels anywhere, and A simply writes finished lines.

Once every line is self-contained, the grouping problem evaporates. It no longer matters what order things reach the card in.

The intercom protocol

Previously A appended whatever B sent, which is how GPS status text ended up in the log alongside networks. Lines are tagged now:

Direction Message Meaning
B → A $G,fix,lat,lon,alt,acc,YYYY-MM-DD HH:MM:SS,sats GPS update, once per second
B → A $N,<complete csv row> A finished record — just write it
A → B <XNNNN> SD status + 2.4 GHz count, for the OLED

The intercom now runs at 115200 baud, up from 9600. At 9600 you get ~960 bytes/sec, and WiGLE rows run about 170 bytes — roughly 5 records/sec for everything B sends, which backs up in a dense neighborhood. Both ends are ours, so it's a one-line change on each side.

⚠️ You must flash both boards. A mismatched pair talks gibberish.

Output format

WiGLE-1.4 CSV, one file per session (SeaFive\_000.csv, \_001, …), with the version header written at file creation:

WigleWifi-1.4,appRelease=SeaFive-1.0,model=SeaFive,...
MAC,SSID,AuthMode,FirstSeen,Channel,Frequency,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type
"AA:BB:CC:DD:EE:FF","MyNetwork","\[WPA2-PSK-CCMP]\[ESS]","2026-07-15 14:22:31",6,2437,-61,39.0000,-85.0000,231.4,2.8,WIFI

Two details that catch everyone out the first time:

  • AuthMode uses Android's bracket syntax, not friendly names. "WPA2" won't parse — it has to be \[WPA2-PSK-CCMP]\[ESS].
  • Frequency is a separate column from Channel, derived from it: 2.4 GHz is 2407 + ch\*5 (channel 14 is a special case at 2484), 5 GHz is 5000 + ch\*5.

AccuracyMeters is estimated as HDOP × 2.5. The receiver doesn't give us a real accuracy figure, so this is a rough but honest approximation.

No fix, no log

If the GPS fix is missing or older than 5 seconds, nothing is written.

The check happens before the dedup check, and that ordering is deliberate. If a network were marked "seen" while we had no position, it would be suppressed for the rest of the session and never logged at all. Skipping it entirely means we catch it on the next pass. A duplicate row costs nothing — WiGLE dedups server-side, and repeat observations at different coordinates are how it triangulates. A missing record is gone for good.

The OLED shows GPS: -- when there's no usable fix, so a card that isn't filling up tells you why.

---

Hardening notes

A few changes here are the kind of thing that only surfaces once code moves from a bench demo to hours of continuous logging in a moving vehicle. They're worth reading through even if you're not wardriving — they're a nice tour of the classic embedded C sharp edges, and they're easy to hit in any project.

Buffer sizing on the GPS strings.

char longitude\[10];
dtostrf(((double)nmea.getLongitude() / 1000000.0), 8, 6, longitude);

-85.921234 is 10 characters plus a null terminator — 11 bytes into a 10-byte buffer. On the bench with a fixed position you may never notice; on the road it happens on every fix. Sized to 16 here, using width 0 ("as long as it needs to be") rather than 8, which also drops the leading spaces.

Dedup table behavior once full.

if (bssidlogcount<bssidlogmax){ ...add... }
return false;   // "not seen, go log it"

Past 200 entries the insert stops but the MAC is still reported unseen, so already-logged networks start re-logging on each scan. 200 is plenty for a bench test and you'd never see it; a drive through a few neighborhoods gets there quickly. Now a 512-entry ring buffer that overwrites the oldest entry, so the table keeps working no matter how long you drive.

Loop bound in the table search.

for (int c=0; c < bssidlogcount+1; c++)

Reads one entry past the last valid one. Now bounded by the fill count.

Line buffer index across calls.

void read\_intercom() {
  int i = 0;              // local
  while (intercom.available() > 0) { ... }
}

If the UART runs dry mid-line, the partial line is lost and the next call restarts at i = 0. idx is static now, so partial lines survive between calls.

GPS throughput during scans. read\_gps() only ran between blocking scanNetworks() calls. A scan takes seconds, and the default 256-byte UART FIFO holds less NMEA than arrives in that window. Now setRxBufferSize(2048) before begin(), plus read\_gps() interleaved between the blocking scans.

Unused variable. newnets is never set, so the OLED only updated on its 1500 ms timer. Removed.

---

Building and flashing

Libraries

Nothing new to install. The list is identical to the original example:

Library Where from Notes
Adafruit SSD1306 Library Manager Pulls in Adafruit GFX automatically
MicroNMEA Library Manager GPS sentence parsing

BLE support (BLEDevice.h, BLEScan.h, BLEAdvertisedDevice.h) is part of the ESP32 Arduino core — it arrives with the board package in Boards Manager, not Library Manager. If you already built the original example, you're ready to build this one.

⚠️ Partition Scheme — set this on both boards

Tools → Partition Scheme → Huge APP (3MB No OTA/1MB SPIFFS)

This is the one setting that changes, and it's worth understanding rather than just clicking.

The ESP32's internal flash is carved into partitions at compile time, and your firmware image has to fit the partition labelled app. The default 4MB scheme reserves two app slots (ota\_0 / ota\_1) so over-the-air updates can download into the inactive one and flip a pointer — which leaves each slot only ~1.2MB, plus ~1.5MB of SPIFFS.

MCU B's image is roughly 900KB with Wi-Fi, and BLEDevice.h brings in Bluedroid — Espressif's full Bluetooth host stack — pushing it to ~1.4–1.6MB. Against a 1.2MB slot it won't link, and you'll get a "text section exceeds available space" error at build time. Deterministic, not flaky, and easy to mistake for a broken sketch.

Huge APP drops OTA to a single 3MB slot and shrinks SPIFFS to 1MB. We give up over-the-air updates, which this design never had, and internal flash storage, which we never touch — the SD card is a separate device on the SPI bus and is completely unrelated. Both are free to us.

To be clear about what this is not: it's not RAM, not temp files, and nothing to do with the SD card.

Strictly speaking only B needs it — A has no BLE and still fits the default. Set both anyway; one instruction is easier to follow than two, and A only gives up flash it wasn't using.

**For the curious:** NimBLE is a leaner BLE host than Bluedroid and would likely squeeze under the default partition. It's an extra library install, which is the only reason it's not used here.

---

What to expect on first run

  1. Power on. No fix yet, OLED shows GPS: --, nothing logs. This is correct, not a hang.
  2. Get outside or near a window. The ATGM336H's patch antenna wants sky.
  3. Sats lock, GPS: shows a count, records start flowing.
  4. MCU A's USB console echoes every line as it hits the card.
  5. Pull the SD card and upload SeaFive\_000.csv to WiGLE.

Debugging note: MCU B's intercom shares UART0 with its USB console — that's why the original has "select ONLY ONE of the next two lines." While the intercom is live, B has no serial console. Watch A's console instead; it echoes everything that reaches the card.

---

Known unknowns

Being upfront about the parts that are best-effort rather than verified:

  • The BLE row format (Type=BLE, AuthMode=\[BLE], channel and frequency 0) is a best reading of the convention, not confirmed against a live upload. Some projects recommend keeping BLE in a separate file so the Wi-Fi CSV stays clean. If WiGLE objects, that's the fallback and it's an easy change: give B a $B, prefix and have A write those to SeaFive\_000\_BLE.csv.
  • Wi-Fi / BLE coexistence on the C5, with a blocking scan of each, should be fine — but if you see resets or scan starvation, that's the first suspect. Stretching BLE\_SCAN\_SECS or scanning BLE every Nth loop is the lever.

Corrections and pull requests welcome on both counts.

---

Deliberately left simple

Kept minimal on purpose — this should stay something you can read straight through and diff against the original example:

  • No FreeRTOS tasks or async scanning. Would tighten the timing, would also double the difficulty of reading the code.
  • No web UI, no direct upload. Pull the card.
  • No permanent per-session dedup (hash table, or a seen-list on SD). Real complexity for a problem WiGLE already solves server-side.

Every one of these is a good next project if you want to take it further.

---

Built on the HackerBox 0127 "Sea Five" example sketches — thanks to the folks at HackerBoxes for the board, the kit, and example code that made the hardware easy to reason about. WiGLE CSV field reference cross-checked against dkyazzentwatwa/esp32-gps-wifi-wigle. Wardriving prior art worth your time: JosephHewitt/wardriver_rev3, justcallmekoko/ESP32DualBandWardriver, Hamspiced/piglet.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages