OUI-SPY · Volume 6

OUI-SPY Volume 6 — flock-you in Depth


6.1 About this Volume

Volumes 4 and 5 established what flock-you detects on Wi-Fi and BLE, and how it grades each hit on a five-tier scale. This volume is the implementation: the configuration block that tunes the engine, the audio mapping that turns tiers into tones, the lock-free architecture that keeps the radio callback bounded, the session-storage format that survives a reboot with a CRC check, the USB-CDC JSON protocol that streams detections to a host, and the Python companion stack — the Flask dashboard and the Classic-Bluetooth helper — that turns a buzzing XIAO into a GPS-tagged, exportable wardrive map.

flock-you is the flagship of the ecosystem (the most-starred repo, ~1480★ at fetch, MIT-licensed per its README). It is the reference implementation every other OUI-SPY module borrows from, so understanding it in depth is understanding the family. The rest of the module family — the dual-domain Detector, PCAP, BLE-Sniff, axeoff, and the Unified-Blue boot selector — is Volume 7; this volume stays on flock-you itself.

Source material: the flock-you README (2026-10-16). Where the README summarises rather than lists — the internals of flockyou_ble.py, the exact tier-4 IE bytes — the gap is flagged, not filled.

Figure. The flock-you hero/logo is available as figs/os-flock-you-logo.png.

Figure 1 — flock-you project logo
Figure 1 — flock-you project logo

Image: colonelpanichacks (GitHub).


6.2 The main.cpp config block, define by define

Everything the engine does is tuned from a block of #defines at the top of main.cpp. There is no runtime config file for these — change the define, rebuild, reflash. (The one value that is runtime-adjustable, the beep mask, lives in NVS; see §3.)

Table 1 — 2. The main.cpp config block, define by define

DefineDefaultRole
CHANNEL_MODECHANNEL_MODE_CUSTOMWi-Fi hop pattern (11 → 6 → 1)
CHANNEL_DWELL_MS250dwell per Wi-Fi channel (2× camera probe interval)
RSSI_MIN−95reject frames weaker than this (dBm)
ALERT_COOLDOWN_MS5000per-MAC repeat-alert suppression
CHECK_ADDR11enable tier-1 receiver-address OUI check
CHECK_ADDR31enable tier-1 BSSID OUI check
BEEP_MASK_DEFAULT0x1Fdefault tier enable mask (all five tiers on)
ENABLE_SSID_MATCH0tier-0 SSID keyword match — off
PROCESS_MGMT_FRAMES1evaluate 802.11 management frames
PROCESS_DATA_FRAMES1evaluate 802.11 data frames
MAX_DETECTIONS200live detection-table capacity
AUTOSAVE_INTERVAL_MS60000periodic session autosave period
LED_PIN21onboard LED (active-low)
BUZZER_PIN3passive piezo

Reading the block as a tuning philosophy:

  • Sensitivity vs noise. RSSI_MIN (−95) and ALERT_COOLDOWN_MS (5000) together set how “loud” the device is. Raising RSSI_MIN makes it alert only on close devices; the cooldown stops a single camera from screaming continuously.
  • Confidence floor. ENABLE_SSID_MATCH is off and the addr1/addr3 checks feed only the lowest-confidence tier — the defaults bias toward fewer, higher-quality alerts.
  • Frame coverage. Both PROCESS_MGMT_FRAMES and PROCESS_DATA_FRAMES default on, so the engine evaluates probe requests (management) and ordinary traffic (data) alike.
  • Persistence. MAX_DETECTIONS (200) bounds RAM; AUTOSAVE_INTERVAL_MS (60 s) bounds how much a crash can lose.

Partition layout backing this: 6 MB app, 1.94 MB SPIFFS, 20 KB nvs. The SPIFFS region holds the session files (§5); the 20 KB NVS holds the beep mask (§3). The 6 MB app partition is generous because the single image carries the Wi-Fi monitor engine, the NimBLE stack, and an async web server all at once — the Unified-Blue build (Volume 7) packs seven such modes into the same budget, which is why the XIAO’s 8 MB flash, not a smaller ESP32, is the baseline.

A note on changing these: because the block is compile-time #defines, retuning sensitivity (say, raising RSSI_MIN to −80 to alert only on close devices, or setting CHANNEL_MODE to SINGLE to park on a known camera channel) means editing main.cpp and reflashing. Only the beep mask is adjustable at runtime. For a build-vs-flash walkthrough see Volume 10.


6.3 The beep mask and tier-to-buzzer mapping

On a headless device the buzzer is the entire user interface, so the audio mapping is a first-class feature, not a nicety. Each of the five tiers has a distinct tone, and each can be individually enabled or muted through a beep mask — a 5-bit value, one bit per tier, stored in NVS under the key flockyou/beepmask, default 0x1F (binary 11111, all five tiers audible).

 beep mask (NVS key: flockyou/beepmask, default 0x1F)
 ┌───┬───┬───┬───┬───┐
 │ T4│ T3│ T2│ T1│ T0│ bit set = that tier's tone is audible
 └───┴───┴───┴───┴───┘
 16 8 4 2 1 ← bit weight

 0x1F = 11111 all tiers on (default)
 0x18 = 11000 only tiers 4 and 3 (high-confidence only)
 0x1C = 11100 tiers 4, 3, 2 (mute the misfire-prone T1 and the T0 keyword)

Table 2 — 3. The beep mask and tier-to-buzzer mapping

TierMask bitToneReads as
40x10two-note chirp, 2000 → 2800 Hz”Flock-shaped probe — high confidence”
30x08two-note chirp, 1400 → 1800 Hz”wildcard probe from a target OUI”
20x04single blip, 1200 Hz”OUI / BLE firmware signature”
10x02single blip, 800 Hz”positional OUI match — may misfire”
00x01single blip, 600 Hz”SSID keyword (off by default)”

The design intent: rising pitch = rising confidence, and chirps (two notes) outrank blips (one note). An operator who wants to cut noise on a busy street sets the mask to 0x18 and hears only the strong wildcard-probe chirps, ignoring the tier-1 positional blips the README itself warns “do misfire.” The heartbeat / proximity tone uses the strongest tier currently in range, so the device’s ongoing chatter also reflects the best evidence present.

The mask is runtime-adjustable from the dashboard (POST /api/flock/beep, §7) — you don’t rebuild to mute a tier.


6.4 The on-device architecture: ring buffer and loop

The firmware’s reliability rests on a strict separation between the time-critical radio callbacks and the slower bookkeeping in loop(). This was sketched in Volume 4 §5; here is the whole picture including BLE.

 Wi-Fi monitor RX callback ─┐ BLE NimBLE adv callback ─┐
 (fast match only, no alloc)│ (fast match only) │
 ▼ ▼
 ┌──────────────────────────────────────────────┐
 │ alertQueue[32] (lock-free ring) │
 └──────────────────────┬──────────────────────────┘
 │
 loop() ▼ drains the queue
 ┌──────────────────────────────────────────────┐
 │ 1. update detection table (highest-tier-wins) │
 │ 2. apply per-MAC ALERT_COOLDOWN_MS │
 │ 3. emit JSON detection line over USB-CDC │
 │ 4. drive buzzer tone (per beep mask) │
 │ 5. autosave every AUTOSAVE_INTERVAL_MS │
 └──────────────────────────────────────────────┘

The two radio callbacks do only matching and a single push into the 32-slot ring. They never allocate memory, never block on the buzzer, never write flash — all of which could stall the PHY and drop frames. loop() owns everything slow: the detection table, the cooldown, the serial emit, the tone, and the periodic autosave. The ring buffer is the hand-off, and its lock-free single-producer/single-consumer design means neither side waits on a mutex.

This is why a XIAO ESP32-S3 can keep up with a camera spraying wildcard probes every ~125 ms while simultaneously time-slicing in BLE scans and serving a stream to a host.

The detection table. The consumer side maintains a live table of up to MAX_DETECTIONS (200) entries, keyed by MAC. Each entry holds the device’s current (highest-ever) tier, its last-seen RSSI and channel, a hit count, and a methods_seen map — a per-method tally so the dashboard can show that a device fired, say, wifi_wildcard_probe_ie_sig 40 times and wifi_oui_addr2 6 times. The methods_seen map is what lets an operator distinguish a device that occasionally clips a tier from one that consistently produces the strong signature.

 detection table entry (keyed by MAC)
 ┌───────────────┬──────────────────────────────────────────┐
 │ mac │ aa:bb:cc:dd:ee:ff │
 │ tier │ 4 (highest ever seen — never drops) │
 │ oui │ b4:1e:52 │
 │ rssi / chan │ −61 dBm / ch 6 │
 │ hits │ 46 │
 │ methods_seen │ { wifi_wildcard_probe_ie_sig: 40, │
 │ │ wifi_oui_addr2: 6 } │
 └───────────────┴──────────────────────────────────────────┘

When a new frame arrives for a MAC already in the table, loop() bumps the hit count, updates RSSI/channel, increments the matching methods_seen counter, and raises the tier if the new hit is stronger (never lowers it). When the table is full, the engine is bounded by MAX_DETECTIONS rather than growing without limit.


6.5 Session storage and the CRC envelope

Detections persist across reboots so a wardrive can be reviewed or resumed. The format is deliberately crash-tolerant, with a CRC32 integrity check and an atomic write dance on the SPIFFS partition.

The on-disk format is two lines:

 line 1 (envelope): {"v":1,"count":N,"bytes":B,"crc":"0xXXXXXXXX"}
 line 2 (payload): [ {detection}, {detection},... ] ← the detection array
  • v — format version (1).
  • count — number of detection records in line 2.
  • bytes — length of the line-2 payload.
  • crc — CRC32 of the payload, polynomial 0xEDB88320 (the standard reversed polynomial).

The atomic save avoids a torn file if power is lost mid-write:

 save():
 1. write payload + envelope to /session.tmp
 2. re-read /session.tmp and re-validate its CRC
 3. remove /session.json
 4. rename /session.tmp → /session.json
 (if rename fails: copy + delete fallback)

Boot recovery promotes the last good session and starts clean:

 boot():
 if /session.json valid (CRC ok) → promote to /prev_session.json
 else if /session.tmp valid → use it
 delete the working files
 start with an empty live table

The effect: the previous session is always preserved as /prev_session.json (dumpable to a host, see §6), while the live table starts empty each boot. A corrupt file — caught by the CRC mismatch — is discarded rather than loaded, so a bad write can never poison a session. There is no RTC on the XIAO, so records imported after the fact get an import timestamp and no GPS match (the host supplies time and position live; §9).

What a stored record holds. Each detection in the line-2 array is the same shape as the live-table entry (§4), serialised to JSON — enough to reconstruct the table without the raw frames:

Table 3 — frames

FieldExampleNote
macb4:1e:52:aa:bb:cctable key
tier4highest ever seen
ouib4:1e:52matched prefix
methodwifi_wildcard_probe_ie_sigstrongest method
rssi / channel-61 / 6last-seen
hits46total hits for this MAC
methods_seen{…}per-method tally

Autosave and flash wear. The table is written to SPIFFS every AUTOSAVE_INTERVAL_MS (60 s) and on clean shutdown paths, so an unexpected reboot loses at most a minute of new detections. The atomic tmp-then-rename dance (above) runs on every save; because SPIFFS on the ESP32 does its own wear-levelling across the 1.94 MB partition, a 60-second cadence over a multi-hour wardrive is well within the flash’s endurance budget. The session file is small — at most MAX_DETECTIONS (200) compact records — so each write is cheap.


6.6 The USB-CDC serial protocol

The device talks to a host over the native USB-C CDC-ACM serial port (also mirrored on GPIO43 Serial1 TX at 115200 baud). The protocol is line-delimited JSON — one JSON object per line — in both directions.

Device → host, a detection line. Fields:

Table 4 — Device → host, a detection line. Fields

FieldMeaning
eventevent type (e.g. detection)
detection_methodthe named method (wifi_wildcard_probe_ie_sig, ble_mfg, …)
detection_tier0–4
protocolWiFi or BLE
mac_addressdevice MAC
ouimatched OUI prefix
device_nameadvertised name (BLE) or SSID context
rssisignal strength (dBm)
channelWi-Fi channel
frequencychannel frequency
ssidSSID if relevant

Device → host, a config line (sent at connect):

 {"event":"config", "beep_mask":31, "oui_count":34, "tiers":[... ]}

Host → device, commands:

 {"cmd":"dump_session","source":"live"} ← dump the live table
 {"cmd":"dump_session","source":"prev"} ← dump the previous saved session

Device → host, session-dump responses (in reply to dump_session):

 {"event":"session_begin",...}
 {"event":"session_det",...} ← one per stored detection
 {"event":"session_det",...}
...
 {"event":"session_end",...}

A concrete detection line, as it appears on the wire (one line, wrapped here for the page):

{"event":"detection","detection_method":"wifi_wildcard_probe_ie_sig",
 "detection_tier":4,"protocol":"WiFi","mac_address":"b4:1e:52:aa:bb:cc",
 "oui":"b4:1e:52","device_name":"","rssi":-61,"channel":6,
 "frequency":2437,"ssid":""}

And a session dump, as a sequence over time:

 host ──▶ {"cmd":"dump_session","source":"prev"}
 dev ──▶ {"event":"session_begin","count":12,...}
 dev ──▶ {"event":"session_det",... } ×12
 dev ──▶ {"event":"session_end",...}

The protocol is intentionally plain: any terminal, script, or the Flask dashboard can read it, and a session dump streams record-by-record rather than as one giant blob so a host can render progress. Because the device has no clock, imported session_det records carry no live GPS match — the host timestamps what it receives.


6.7 The Flask dashboard — flockyou.py

api/flockyou.py is a Flask + Socket.IO web application that consumes the USB-CDC stream and turns it into a live, mappable, exportable view. Run it:

 cd api
 pip install -r requirements.txt
 python flockyou.py
 # open http://localhost:5000

It opens the serial port, parses the line-JSON (§6), and pushes detections to the browser over a websocket while maintaining the GPS timeline. The HTTP surface:

Table 5 — a websocket while maintaining the GPS timeline. The HTTP surface

MethodEndpointPurpose
GET/api/flock/configread current device config (beep mask, OUI count, tiers)
POST/api/flock/beepset beeping — {"tier":0-4,"enabled":bool} or {"mask":0-31}
POST/api/flock/dump_sessionrequest a dump — `{“source”:“live"

Table 6 — 7. The Flask dashboard — flockyou.py

Socket eventDirectionPurpose
device_configserver → browserpush the device config on connect/change
session_dumpserver → browserstream a session dump to the UI

Two things worth drawing out. First, the beep mask is controllable from the browser — the POST /api/flock/beep endpoint writes the mask (§3) back to the device, so an operator mutes the misfire-prone tier-1 blips from the dashboard without touching the firmware. Second, the dashboard is the merge point: it combines the ESP32’s Wi-Fi/BLE detections with the Python BLE companion’s Classic-BT corroboration (§8) into one GPS-tagged view.

[FIGURE SLOT — Vol 6 §7] A screenshot of the flockyou.py Flask dashboard: the live detection list coloured by tier, the beep-mask toggles, and the map pane with GPS-tagged markers.

The Python side is a small, conventional stack — the requirements.txt pulls in Flask, Flask-SocketIO, and pyserial for the USB-CDC link, plus the GPS/export helpers. Nothing in it is exotic; the point of the companion is to be a thin, auditable bridge between the device’s line-JSON and a browser, not a heavyweight application. A detection posted by the serial reader is echoed to the websocket immediately, so the browser list and map update in real time as the device chirps.


6.8 The Python BLE companion — flockyou_ble.py

api/flockyou_ble.py is the host-side helper that covers the one gap the ESP32-S3 cannot: it performs Classic-Bluetooth (BR/EDR) inquiry and SDP service discovery from a laptop’s normal Bluetooth adapter, because the XIAO radio does BLE only. The methodology was laid out in Volume 5 §9; this is where it plugs in.

It matches on the Qualcomm-Android fingerprint of a Flock appliance:

Table 7 — It matches on the Qualcomm-Android fingerprint of a Flock appliance

TargetValue
device namemsm8953_32
device nameAndroid
SDP Device-ID (Qualcomm vendor)0x001D
SDP Device-ID (Qualcomm product)0x1200

Hits are tagged firmware_sig: true with a matched_signatures field and handed to the same Flask dashboard, so a Classic-BT corroboration and a BLE/Wi-Fi hit from the same physical installation line up on the same map marker.

 flockyou_ble.py ──Classic-BT inquiry + SDP──▶ match msm8953_32 / Qualcomm 0x001D/0x1200
 │ │
 └────────── firmware_sig:true ───────▶ flockyou.py dashboard (merged)

The README gives no deeper internals of this script, so its exact scan parameters are unverified from the README.---

6.9 GPS, wardriving, and the KML/CSV export

The dashboard supports three ways to get a position fix, selectable from a source dropdown:

Table 8 — The dashboard supports three ways to get a position fix, selectable from a source dropdown

GPS sourceTransportNotes
Serial NMEA puckUSB serial @ 9600 baud on the hosta plain USB GPS dongle
gpsdTCP, default localhost:2947, line-JSONshared GPS daemon
Browser Geolocation APIposted to Flasklaptop/phone location, no extra hardware

Flask matches each detection’s timestamp to the GPS timeline (interpolating position for the moment the hit arrived) and can export the merged dataset three ways:

 detections (from device) + GPS timeline (from source)
 │
 ▼ timestamp match / interpolate
 ┌──────────────────────────┐
 │ JSON — full fidelity │
 │ CSV — full fidelity │ ← the only format carrying firmware_sig
 │ KML — for Google Earth│
 └──────────────────────────┘
  • JSON — the complete record set, machine-readable.
  • CSV — the only export that carries the firmware_sig / matched_signatures fields in full (the dashboard and KML do not yet render them — see §11).
  • KML — map markers for Google Earth / mapping tools, positioned from the GPS match.

This is the wardriving story: drive or walk a route with a laptop, a GPS source, and the XIAO on USB; the dashboard plots every tier-graded Flock/Penguin/Raven hit on a live map; export the run as KML to overlay the camera coverage of a neighbourhood.

[FIGURE SLOT — Vol 6 §9] A KML export opened in a mapping tool, showing GPS-tagged detection markers along a driven route, coloured by detection tier.


6.10 Putting it together: a wardrive session

Reference walkthrough (impersonal — this describes the intended workflow, not a logged run):

 1. Flash flock-you to a XIAO ESP32-S3 (Vol 10 covers the flasher).
 2. Plug it into a laptop via a data USB-C cable → native CDC serial appears.
 3. (optional) Plug in a USB GPS puck, or start gpsd, or allow browser geolocation.
 4. (optional) Start flockyou_ble.py for Classic-BT corroboration.
 5. cd api && python flockyou.py → open http://localhost:5000
 6. The dashboard shows device config (beep_mask 0x1F, oui_count 34, five tiers).
 7. Drive/walk the route. Detections stream in, graded T0–T4, GPS-tagged.
 - Set beep mask 0x18 from the dashboard to hear only strong T3/T4 chirps.
 8. Buzzer chirps rise in pitch near a real camera; the map fills with markers.
 9. Export KML (map) or CSV (full fidelity incl. firmware_sig) at the end.

The flow exercises every subsystem in this volume: the config block tuning sensitivity (§2), the beep mask filtering the audio (§3), the ring-buffer/loop architecture keeping up with the air (§4), the session file surviving a mid-drive reboot (§5), the USB-CDC JSON stream (§6) feeding the Flask dashboard (§7) merged with the Classic-BT companion (§8) and GPS-tagged for export (§9).


6.11 Caveats and known gaps

  • firmware_sig is captured but not shown. The Classic-BT corroboration fields (firmware_sig, matched_signatures) are carried end-to-end into the CSV export but are not rendered on the live dashboard or in the KML export at the time of writing. An operator relying on the map alone will not see Classic-BT hits.
  • No RTC → imported records lose GPS. The device has no real-time clock. Live detections are timestamped and GPS-matched by the host as they stream; records imported from a dumped session get an import timestamp and no GPS match.
  • The tier-4 IE bytes are in source, not the README. As in Volume 4, the exact information- element fingerprint that earns tier 4 is named and cited but not published in the README.
  • flockyou_ble.py internals undocumented. The README confirms what the companion matches on, not how it scans; inquiry duration and adapter handling are unverified from the README.
  • Stale channel-order prose (ascending vs the shipped descending 11 → 6 → 1) carries over from the detection engine (Volume 4 §6).
  • “Passive” caveat. flock-you’s own Wi-Fi and default BLE phases are receive-only, but the broader ecosystem (detector active scan, Foxhunter, the Red tools) is not — see Volumes 5, 8, and 9.

6.12 Where this connects

The detection methodology this volume implements is Volumes 4 (Wi-Fi: the 34-OUI union, the firmware-extracted prefixes, the wildcard-probe IE fingerprint, channel hopping, the five-tier model) and 5 (BLE: NimBLE passive scan, the Penguin/Flock-GATT/Nordic-DFU/Raven signatures, the duty cycle, and the Classic-BT corroboration whose on-device plumbing is §6–§8 here).

The rest of the module family that reuses this engine — the dual-domain Detector with its eight filter classes and MQTT/Home-Assistant discovery, the PCAP and BLE-Sniff capture tools, the axeoff Axon detector, and the Unified-Blue boot selector that packs seven modes into one image — is Volume 7. The RSSI direction-finding Foxhunter is Volume 8. Drone Remote-ID detection (Sky-Spy) and the category-level treatment of the offensive Red edition are Volume 9.

The build and flash mechanics referenced in §10 — the browser web-flasher, the PlatformIO pio run -e seeed_xiao_esp32s3 path, the batch flasher, the first-run configuration, and the operational/legal posture — are Volume 10, which also carries the head-to-head comparison against SquachWatch, Nyan Box, and HaleHound. The hardware is Volume 3; the threat model is Volume 2.

Comments (0)

  1. Loading…

Comments are held for moderation — nothing appears until approved.