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

ParameterValueMeaning
scan window30 msradio actually listening for BLE
scan interval300 msperiod 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

ClassSignatureValueBelongs to
Nameadvertised Local NamePenguin-NNNNNNNNNNPenguin battery pack
Nameadvertised Local Namebare 10-digit numberPenguin battery pack
Nameadvertised Local NameFS Ext BatteryFlock external battery
Nameadvertised Local NameDfuTargdevice in DFU (update) mode
Mfr datacompany ID0x09C8 (XUNTONG)Penguin mfr data
GATT (128-bit)service UUIDe8ccbb38-9532-46a8-9fe5-1814df172e6fFlock accessory service
GATT (128-bit)service UUID00001530-1212-efde-1523-785feabcd123Nordic DFU service
GATT (16-bit)service UUID range0x3100 – 0x3500Raven 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-NNNNNNNNNN and the bare 10-digit number are the same device at different firmware revisions — some ship the Penguin- prefix, some advertise only the serial.
  • FS Ext Battery (“FS” = Flock Safety) is the labelled external-battery variant.
  • DfuTarg is the Nordic DFU bootloader default advertising name. A Penguin (or other Nordic-based accessory) sitting in device-firmware-update mode announces itself as DfuTarg; 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-you README records that within the Raven service range, services 0x3101 and 0x3102 leak 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

TierMethod(s) relevant to BLEBuzzer
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
2ble_name, ble_mfg, ble_gatt_svcsingle 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

MethodKeys onSpoof difficultyNotes
ble_gatt_svc (128-bit)full vendor UUIDhigh128-bit UUIDs are effectively unique per product
ble_mfgSIG company IDhighcompany IDs are assigned; faking one is deliberate
ble_gatt_svc (Nordic DFU)shared DFU UUIDmediumused by many unrelated Nordic products
ble_gatt_svc (Raven 16-bit)16-bit rangemediumshort UUIDs collide more readily
ble_nameadvertised stringlowany 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). No SCAN_REQ is sent; the detector stays RF-silent.
  • The ouispy-detector BLE 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 targetValueMeaning
device namemsm8953_32a Qualcomm MSM8953 Android reference-platform name
device nameAndroidgeneric Android device name
SDP Device-ID (Qualcomm)0x001DQualcomm vendor in the Device ID profile
SDP Device-ID (Qualcomm)0x1200Qualcomm 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_signatures fields — 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_name is 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 a ble_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/0x3102 unauthenticated-GPS disclosure (§6) comes from the flock-you README 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)

  1. Loading…

Comments are held for moderation — nothing appears until approved.