OUI-SPY · Volume 5
OUI-SPY Volume 5 — Detection Methodology II: BLE
5.1 About this Volume
Volume 4 covered the Wi-Fi half of OUI-SPY’s detection engine. This volume covers the Bluetooth Low Energy half: how the firmware listens for BLE advertisements, the specific signatures it keys on (all of them firmware-derived rather than guessed), how a BLE hit maps onto the same five-tier confidence model the Wi-Fi path uses, and — the part the ESP32 genuinely cannot do alone — the host-side Classic-Bluetooth corroboration performed by the Python companion.
BLE matters here because not every surveillance object on a street is a Wi-Fi station. The Penguin external battery packs that power Flock cameras advertise over BLE. So do the cameras’ Nordic-based accessory and firmware-update interfaces, and the Raven acoustic gunshot sensors (SoundThinking / ShotSpotter). A Wi-Fi-only detector would walk straight past a powered-down-but-provisioning camera whose battery pack is chirping on 2.4 GHz BLE. The two radios share the same antenna and the same SoC on a XIAO ESP32-S3, so OUI-SPY runs both in one firmware image, time-slicing the radio between them.
As in Volume 4, the content is drawn from the flock-you README (2026-10-16). BLE
signatures are quoted verbatim — company IDs in hex, GATT service UUIDs in full — because
the exact values are the methodology.
Voice / scope. Everything the ESP32 does on BLE here is scanning — receiving advertisements. The detector firmware’s BLE phase and the Foxhunter use active scanning (it sends a scan request), which is the “asterisk” on the word passive; §8 is honest about it. The host-side Classic-BT step in §9 runs on a laptop, not the ESP32.
5.2 Why BLE, and what a BLE advertisement carries
A BLE device that wants to be discoverable broadcasts advertising packets on three dedicated advertising channels (37, 38, 39, sitting in the gaps between Wi-Fi channels 1, 6, and 11). A scanner does not need to connect, pair, or authenticate to hear them — the advertisement is a shout into the room. Each packet can carry a handful of advertising-data (AD) structures, and OUI-SPY keys on four kinds of them:
BLE advertising packet
┌───────────────────────────────────────────────────────────┐
│ preamble │ access addr │ PDU (payload) │ CRC │
└───────────────────────────────────────────────────────────┘
│
▼ the PDU holds AD structures:
┌──────────────────────────────────────────────────────────────┐
│ Flags │
│ Complete/Shortened Local Name ─────▶ "Penguin-1234567890" │ ← NAME
│ Manufacturer Specific Data ─────▶ company ID 0x09C8 … │ ← MFG
│ Complete List of 16-bit Svc UUIDs ───▶ 0x3100 … 0x3500 │ ← GATT (16-bit)
│ Complete List of 128-bit Svc UUIDs ──▶ e8ccbb38-…-df172e6f │ ← GATT (128-bit)
└──────────────────────────────────────────────────────────────┘
- Local Name — the human-readable name the device advertises. Cheap to spoof, but in practice surveillance accessories ship with fixed, patterned names.
- Manufacturer Specific Data — begins with a 16-bit company identifier assigned by the Bluetooth SIG. The company ID is a strong signal: it names the chip/firmware vendor.
- Service UUIDs — the GATT services the device offers, as either 16-bit short UUIDs or full 128-bit UUIDs. A device that advertises a specific vendor GATT service is announcing its function.
Each of these four becomes a named detection_method — ble_name, ble_mfg,
ble_gatt_svc — all of which land at tier 2 (§7).
5.3 NimBLE passive scan and the 10% duty cycle
The BLE stack is NimBLE-Arduino, chosen over the Bluedroid stack for its much smaller RAM and flash footprint — important when the same firmware is also running a Wi-Fi monitor callback and a web server. NimBLE registers an advertisement callback that fires for every advertising packet the radio hears.
The radio is a single resource shared between Wi-Fi monitor mode and BLE scanning, so the firmware time-slices it. The BLE phase is run at roughly a 10% duty cycle:
Table 1 — firmware time-slices it. The BLE phase is run at roughly a 10% duty cycle
| Parameter | Value | Meaning |
|---|---|---|
| scan window | 30 ms | radio actually listening for BLE |
| scan interval | 300 ms | period between the starts of listen windows |
| duty | ~10% | 30 / 300 — fraction of time spent on BLE |
| remainder | ~90% | left for Wi-Fi monitor-mode sniffing |
one 300 ms interval
├──────────────────────────────────────────────────────────┤
│■■■│ │
│BLE│◀────────────── Wi-Fi monitor mode ──────────────────▶│
30ms
The lopsided split reflects where the primary threat lives: the ALPR cameras are chiefly a Wi-Fi problem (wildcard probes, §4 of Volume 4), and the BLE accessories are a supporting signal. Ten per cent of airtime is enough to reliably catch a battery pack that re-advertises several times a second, without starving the Wi-Fi channel-hopping schedule.
Sharing one radio between two jobs. The XIAO ESP32-S3 has a single 2.4 GHz radio, and Wi-Fi monitor mode and BLE scanning both want it. BLE advertising lives on three fixed channels (37/38/39) interleaved with the Wi-Fi channel plan, so the firmware does not try to run both at once — it owns the radio for a 30 ms BLE window, then returns it to the Wi-Fi channel-hopping schedule for the rest of the 300 ms interval.
2.4 GHz band
ch: 1 6 11
│ │ │ Wi-Fi non-overlapping (Vol 4 §6)
───▲─┴──▲─────┴──▲─────┴────▲──── 2400 → 2480 MHz
│ │ │ │
BLE37 BLE38 BLE39 BLE advertising channels sit in the gaps
Why NimBLE and not Bluedroid: Bluedroid is the full-featured Classic-BT + BLE stack that ships
with ESP-IDF, but it is heavy on RAM and flash. NimBLE-Arduino is BLE-only and far leaner,
which is what lets flock-you run a BLE scanner, a Wi-Fi monitor callback, a 32-slot ring
buffer, and a web server in the same 8 MB image with room to spare. The cost — no Classic-BT —
is exactly the gap the host-side Python companion fills (§9).
5.4 The firmware-derived BLE signature set
Every BLE signature in flock-you was pulled from a device or firmware image, not invented.
This is the BLE counterpart to the firmware-extracted Wi-Fi OUIs in Volume 4 §4, and it is
again the thing that separates OUI-SPY from a name-matching scanner. The full set:
Table 2 — again the thing that separates OUI-SPY from a name-matching scanner. The full set
| Class | Signature | Value | Belongs to |
|---|---|---|---|
| Name | advertised Local Name | Penguin-NNNNNNNNNN | Penguin battery pack |
| Name | advertised Local Name | bare 10-digit number | Penguin battery pack |
| Name | advertised Local Name | FS Ext Battery | Flock external battery |
| Name | advertised Local Name | DfuTarg | device in DFU (update) mode |
| Mfr data | company ID | 0x09C8 (XUNTONG) | Penguin mfr data |
| GATT (128-bit) | service UUID | e8ccbb38-9532-46a8-9fe5-1814df172e6f | Flock accessory service |
| GATT (128-bit) | service UUID | 00001530-1212-efde-1523-785feabcd123 | Nordic DFU service |
| GATT (16-bit) | service UUID range | 0x3100 – 0x3500 | Raven acoustic sensor |
Any single hit in this table is sufficient for a tier-2 alert. The classes are complementary: a device caught lying about its name can still be caught by its company ID or its GATT service, and vice versa.
5.5 Penguin battery packs — the name family
The Penguin is the external battery pack that powers a pole-mounted Flock camera where mains power is inconvenient. It advertises over BLE for provisioning and status, and it is the most reliable BLE tell that a camera installation is nearby — often detectable before the camera’s own Wi-Fi is up.
OUI-SPY catches it four different ways, because the advertised name varies with firmware state and model:
Penguin name family
───────────────────
"Penguin-1234567890" ── named + 10-digit serial ┐
"1234567890" ── bare 10-digit serial (no prefix) ├─ ble_name (tier 2)
"FS Ext Battery" ── Flock external battery label │
"DfuTarg" ── Nordic DFU bootloader name ┘
+
company ID 0x09C8 (XUNTONG) ── manufacturer-data path ── ble_mfg (tier 2)
Penguin-NNNNNNNNNNand the bare 10-digit number are the same device at different firmware revisions — some ship thePenguin-prefix, some advertise only the serial.FS Ext Battery(“FS” = Flock Safety) is the labelled external-battery variant.DfuTargis the Nordic DFU bootloader default advertising name. A Penguin (or other Nordic-based accessory) sitting in device-firmware-update mode announces itself asDfuTarg; catching it is catching a camera accessory mid-update.- Company ID
0x09C8(XUNTONG) is the manufacturer-specific-data path to the same device. XUNTONG is the firmware/chip vendor behind the pack; the company ID is harder to spoof than the name and survives a device that advertises no readable name at all.
A worked advertisement decode. A Penguin advertisement that trips flock-you on two
paths at once looks, in AD-structure terms, like this (illustrative layout; the exact byte
offsets vary with firmware):
AD structures in one Penguin advertising packet
┌──────┬──────────────────────────────────────────────┬──────────────┐
│ len │ type / value │ what fires │
├──────┼──────────────────────────────────────────────┼──────────────┤
│ 0x02 │ 0x01 Flags = 0x06 (LE General Disc, no BR/EDR) │ — │
│ 0x0C │ 0x09 Complete Local Name "Penguin-1234567890" │ ble_name T2 │
│ 0x05 │ 0xFF Mfr Data, company 0xC8 0x09 (= 0x09C8) … │ ble_mfg T2 │
└──────┴──────────────────────────────────────────────┴──────────────┘
▲
note: the company ID is little-endian on the wire — C8 09 → 0x09C8
The firmware does not need both to fire; either the name or the company ID alone reaches tier 2. But catching both in one packet is what makes the Penguin the most dependable BLE tell in the set: two independent signatures from the same advertisement, one of them (the company ID) resistant to spoofing.
5.6 Flock accessory, Nordic DFU, and Raven GATT services
Beyond the Penguin, three GATT service UUIDs round out the set.
Flock accessory service — e8ccbb38-9532-46a8-9fe5-1814df172e6f. A full 128-bit vendor
UUID advertised by the camera’s own BLE accessory interface. Because 128-bit UUIDs are
effectively unique per vendor product, a device advertising this service is making a strong,
low-false-positive claim about what it is. This is a ble_gatt_svc hit.
Nordic DFU service — 00001530-1212-efde-1523-785feabcd123. The well-known Nordic
Semiconductor Device Firmware Update service UUID. Many Nordic-nRF-based accessories (the
Penguin among them) expose it. On its own it is less specific than the Flock accessory UUID —
plenty of unrelated Nordic products use the same DFU service — but combined with a Penguin
name or the XUNTONG company ID in the same advertisement, it strongly corroborates a
camera-accessory DFU session. It is still scored ble_gatt_svc, tier 2.
Raven acoustic sensors — 0x3100 through 0x3500. The Raven is SoundThinking /
ShotSpotter’s acoustic gunshot-detection sensor — a different surveillance system from the
Flock ALPR cameras, but increasingly co-deployed. It advertises a range of 16-bit GATT
services, 0x3100–0x3500, and the firmware treats a hit anywhere in that range as a Raven
detection.
A notable disclosure, stated as reference fact. The
flock-youREADME records that within the Raven service range, services0x3101and0x3102leak GPS coordinates and are unauthenticated — i.e. the sensor’s position is readable over BLE without pairing. This is reported here as documentation of what the detector keys on and why the Raven is catchable, not as an exploitation recipe. OUI-SPY’s handling is passive: it notes the service, grades the hit at tier 2, and chirps.| GATT target | Form | Specificity | Scored | |-------------|------|-------------|--------| |e8ccbb38-…-1814df172e6f| 128-bit vendor UUID | very high (unique product) |ble_gatt_svc| |00001530-…-785feabcd123| 128-bit Nordic DFU | moderate (shared across Nordic) |ble_gatt_svc| |0x3100–0x3500| 16-bit range | high (Raven sensor family) |ble_gatt_svc|
5.7 How a BLE hit grades on the five-tier scale
BLE and Wi-Fi feed the same five-tier confidence model (introduced in Volume 4 §8). All three BLE methods land at tier 2 — the same tier as a Wi-Fi transmitter-OUI match, below the Wi-Fi wildcard-probe tiers (3 and 4) and above the positional/keyword tiers (1 and 0).
Table 3 — 7. How a BLE hit grades on the five-tier scale
| Tier | Method(s) relevant to BLE | Buzzer |
|---|---|---|
| 4 | (Wi-Fi IE fingerprint only — no BLE path) | two-note 2000 → 2800 Hz |
| 3 | (Wi-Fi wildcard probe only — no BLE path) | two-note 1400 → 1800 Hz |
| 2 | ble_name, ble_mfg, ble_gatt_svc | single blip 1200 Hz |
| 1 | (Wi-Fi addr1/addr3 only — no BLE path) | single blip 800 Hz |
| 0 | (Wi-Fi SSID only — no BLE path) | single blip 600 Hz |
Why tier 2 and not higher? Because BLE detection here is a signature match — a name, a
company ID, or a service UUID — which is strong but not the full behavioural fingerprint that
earns Wi-Fi tier 4. There is deliberately no “BLE IE fingerprint” tier; the BLE advertisement
structures are matched for presence, not fingerprinted for shape. The three BLE methods are
peers of wifi_oui_addr2: a genuine, transmitter-side identity match worth a confident single
1200 Hz blip.
The same tier rules from Volume 4 apply: highest tier wins and never drops, a stronger tier
pre-empts the per-MAC ALERT_COOLDOWN_MS, and the tone encodes the tier so a headless device
is still readable by ear.
Robustness of the three BLE methods. Not all tier-2 BLE hits are equally trustworthy, even though they share a tier. Ranking by how hard each is to fake:
Table 4 — though they share a tier. Ranking by how hard each is to fake
| Method | Keys on | Spoof difficulty | Notes |
|---|---|---|---|
ble_gatt_svc (128-bit) | full vendor UUID | high | 128-bit UUIDs are effectively unique per product |
ble_mfg | SIG company ID | high | company IDs are assigned; faking one is deliberate |
ble_gatt_svc (Nordic DFU) | shared DFU UUID | medium | used by many unrelated Nordic products |
ble_gatt_svc (Raven 16-bit) | 16-bit range | medium | short UUIDs collide more readily |
ble_name | advertised string | low | any device can advertise any name |
The practical reading: a ble_name-only hit should be treated with the same scepticism as a
Wi-Fi tier-1 blip, while a 128-bit ble_gatt_svc or a ble_mfg company-ID hit is as solid as
BLE gets here. When two methods fire on the same device — a Penguin name and the XUNTONG
company ID — the confidence is effectively corroborated even though the tier number does not
change.
5.8 Passive vs active scan — the asterisk on “passive”
OUI-SPY is marketed as a passive detector, and the Wi-Fi path (Volume 4) is genuinely receive-only. The BLE story needs the asterisk the brief insists be stated plainly.
A BLE scanner can operate in two modes:
PASSIVE scan ACTIVE scan
──────────── ───────────
device ──adv──▶ scanner device ──adv──▶ scanner
scanner ──SCAN_REQ──▶ device ← a transmission
device ──SCAN_RSP──▶ scanner
hears only what is broadcast solicits extra data (scan response)
TX silent TX active — reveals the scanner's presence
flock-you’s BLE phase is passive — it listens to advertisements only (the 30 ms / 300 ms window above). NoSCAN_REQis sent; the detector stays RF-silent.- The
ouispy-detectorBLE phase uses active scan — it sends scan requests to pull the scan-response payload, which can surface a name or data the device withholds from the bare advertisement. The Foxhunter also leans on active scan for its tight RSSI loop.
The tradeoff is the classic one: active scan sees more (scan responses often carry the local name and extra service UUIDs that fit in the advertisement’s limited budget), but it transmits, so a sufficiently clever target — or a spectrum monitor — could detect the detector. For a tool whose entire posture is “know what’s watching you without being noticed,” that is a real distinction. The default flock-you BLE path stays passive; the operator opts into active scanning by choosing the detector or foxhunter mode.
Posture takeaway. If true RF silence matters — you do not want your own scan to be observable — stay in
flock-you’s passive BLE phase and the Wi-Fi monitor path. The moment you switch to the detector’s active BLE scan or the Foxhunter, you are transmitting scan requests. That is a legitimate, legal thing to do with your own hardware, but it is not silent.
5.9 Host-side Classic-Bluetooth corroboration
There is a class of surveillance device the ESP32-S3 simply cannot see on its own: ones that
identify over Classic Bluetooth (BR/EDR) rather than BLE. The XIAO ESP32-S3 radio does
BLE, not Classic-BT inquiry, so flock-you offloads this to a Python companion
(api/flockyou_ble.py) running on a laptop with a normal Bluetooth adapter.
The companion performs a Classic-BT inquiry and SDP (Service Discovery Protocol) query, and matches on:
Table 5 — matches on
| Corroboration target | Value | Meaning |
|---|---|---|
| device name | msm8953_32 | a Qualcomm MSM8953 Android reference-platform name |
| device name | Android | generic Android device name |
| SDP Device-ID (Qualcomm) | 0x001D | Qualcomm vendor in the Device ID profile |
| SDP Device-ID (Qualcomm) | 0x1200 | Qualcomm product-side identifier |
The logic connects back to Volume 4’s firmware provenance: Flock cameras are built on
Qualcomm silicon (the QCA9377 Wi-Fi SoC; the compute side is Qualcomm Android). A device that
inquires as msm8953_32 or reports a Qualcomm SDP Device-ID is corroborating “this is a
Qualcomm-Android-based appliance” from a radio the ESP32 can’t reach.
Hits from the companion are tagged firmware_sig: true with a matched_signatures field
in the API stream, distinguishing them from the ESP32’s own BLE/Wi-Fi hits.
ESP32-S3 (flock-you) Laptop (flockyou_ble.py)
──────────────────── ────────────────────────
Wi-Fi monitor ┐ Classic-BT inquiry + SDP
BLE passive ├─ USB-CDC JSON ─────▶ Flask dashboard ◀── corroboration
┘ │ (firmware_sig:true)
▼
merged, GPS-tagged map
Honest limit of coverage. The dashboard and the KML export do not yet render the
firmware_sig/matched_signaturesfields — only the CSV export carries them in full. So Classic-BT corroboration is captured and exported, but not surfaced on the live map at the time of writing.The dashboard plumbing, the GPS tagging, and the export formats are the subject of Volume 6.
5.10 BLE detection flow, end to end
Bringing §3–§9 together:
BLE adv ──▶ NimBLE adv callback (30 ms window / 300 ms interval, passive)
│
▼
┌──────────────────────────────────────────────┐
│ evaluate AD structures against signature set │
│ • Local Name ∈ Penguin family? ─▶ ble_name │
│ • Mfr company ID = 0x09C8 (XUNTONG)? ─▶ ble_mfg
│ • Svc UUID = Flock / Nordic-DFU 128-bit? │
│ or 16-bit ∈ 0x3100–0x3500 (Raven)? │
│ ─▶ ble_gatt_svc │
└───────────────┬────────────────────────────────┘
│ any hit
▼
TIER 2 (1200 Hz blip)
│
▼
┌──────────────────────────────────────────────┐
│ same table / highest-tier-wins / per-MAC cooldown│
│ JSON line over USB-CDC (protocol = BLE) │
└───────────────┬────────────────────────────────┘
│
▼ (optional, on a host)
Flask dashboard ◀────────── flockyou_ble.py Classic-BT
+ GPS tag + map corroboration (firmware_sig:true)
The structural parallels to the Wi-Fi flow (Volume 4 §9) are deliberate: both paths produce
records in the same format, feed the same tiered table with the same cooldown, and emit the
same JSON line shape differing only in the protocol field. The firmware is one engine with
two front ends.
5.11 Limitations and caveats
- Active scan is not silent. The detector-mode and Foxhunter BLE phases transmit scan requests (§8). Only the flock-you default BLE phase is passive. Anyone claiming OUI-SPY is “fully passive” is describing one mode, not all of them.
- Names are spoofable.
ble_nameis the weakest of the three BLE methods — a name is a byte string any device can advertise. The firmware mitigates this by also keying on company ID and GATT UUID, which are harder to fake, but able_name-only hit deserves the same scepticism as a Wi-Fi tier-1 blip. - Nordic DFU is shared. The
00001530-…DFU service is used by countless unrelated Nordic products; a lone DFU hit is not a camera. It is meaningful mainly alongside a Penguin name or the XUNTONG company ID in the same advertisement. - Classic-BT needs a host. The
msm8953_32/ Qualcomm-SDP corroboration only exists when the Python companion is running on a laptop. A standalone XIAO buzzer never sees it, and the dashboard/KML don’t render the result even when it does (§9). - The Raven GPS-leak claim is README-sourced. The
0x3101/0x3102unauthenticated-GPS disclosure (§6) comes from theflock-youREADME and is reported as documentation of the detection target, not independently verified here. - 10% duty means misses are possible. A device that advertises rarely could fall entirely into a Wi-Fi window. The 30/300 split is tuned for the Penguin’s several-per-second cadence; a slow advertiser is a corner case.
5.12 Where this connects
The Wi-Fi half of the detection engine — the 34-prefix OUI union, the firmware-extracted
b4:1e:52 / 00:03:7f prefixes, promiscuous mode, channel hopping, and the tier-4 wildcard-
probe IE fingerprint — is Volume 4. The five-tier confidence model those two volumes share is
defined in full there (Volume 4 §8).
The implementation details only sketched here — the USB-CDC JSON protocol that carries the
protocol: BLE detection lines, the session storage and CRC envelope, the Flask dashboard
(flockyou.py) that merges ESP32 and Python-companion streams, the GPS tagging, and the
JSON/CSV/KML export — are the subject of Volume 6, “flock-you in depth.”
The threat devices named here (Penguin battery packs, Raven / ShotSpotter acoustic sensors, and the Flock cameras they attach to) are described as physical systems — what they are, who makes them, how they are deployed — in Volume 2. The hardware this runs on, including which XIAO variant to pick and the antenna options, is Volume 3.
Comments (0)