Volumes 4 and 5 established whatflock-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
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
Define
Default
Role
CHANNEL_MODE
CHANNEL_MODE_CUSTOM
Wi-Fi hop pattern (11 → 6 → 1)
CHANNEL_DWELL_MS
250
dwell per Wi-Fi channel (2× camera probe interval)
RSSI_MIN
−95
reject frames weaker than this (dBm)
ALERT_COOLDOWN_MS
5000
per-MAC repeat-alert suppression
CHECK_ADDR1
1
enable tier-1 receiver-address OUI check
CHECK_ADDR3
1
enable tier-1 BSSID OUI check
BEEP_MASK_DEFAULT
0x1F
default tier enable mask (all five tiers on)
ENABLE_SSID_MATCH
0
tier-0 SSID keyword match — off
PROCESS_MGMT_FRAMES
1
evaluate 802.11 management frames
PROCESS_DATA_FRAMES
1
evaluate 802.11 data frames
MAX_DETECTIONS
200
live detection-table capacity
AUTOSAVE_INTERVAL_MS
60000
periodic session autosave period
LED_PIN
21
onboard LED (active-low)
BUZZER_PIN
3
passive 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
Tier
Mask bit
Tone
Reads as
4
0x10
two-note chirp, 2000 → 2800 Hz
”Flock-shaped probe — high confidence”
3
0x08
two-note chirp, 1400 → 1800 Hz
”wildcard probe from a target OUI”
2
0x04
single blip, 1200 Hz
”OUI / BLE firmware signature”
1
0x02
single blip, 800 Hz
”positional OUI match — may misfire”
0
0x01
single 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.
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
Field
Example
Note
mac
b4:1e:52:aa:bb:cc
table key
tier
4
highest ever seen
oui
b4:1e:52
matched prefix
method
wifi_wildcard_probe_ie_sig
strongest method
rssi / channel
-61 / 6
last-seen
hits
46
total 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
Field
Meaning
event
event type (e.g. detection)
detection_method
the named method (wifi_wildcard_probe_ie_sig, ble_mfg, …)
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
Method
Endpoint
Purpose
GET
/api/flock/config
read current device config (beep mask, OUI count, tiers)
POST
/api/flock/beep
set beeping — {"tier":0-4,"enabled":bool} or {"mask":0-31}
POST
/api/flock/dump_session
request a dump — `{“source”:“live"
Table 6 — 7. The Flask dashboard — flockyou.py
Socket event
Direction
Purpose
device_config
server → browser
push the device config on connect/change
session_dump
server → browser
stream 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
Target
Value
device name
msm8953_32
device name
Android
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.
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 source
Transport
Notes
Serial NMEA puck
USB serial @ 9600 baud on the host
a plain USB GPS dongle
gpsd
TCP, default localhost:2947, line-JSON
shared GPS daemon
Browser Geolocation API
posted to Flask
laptop/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)