Skip to content

OT Air Survey

JimGat edited this page Sep 14, 2026 · 4 revisions

OT/IoT Air Survey

A passive-only multi-radio survey tool that catalogues wireless devices — WiFi APs, BLE devices, ESP-NOW traffic, and (ESP32-C5 boards) 802.15.4 / Zigbee / Thread / WirelessHART — in the area around CYM. It never transmits a probe request, deauth frame, spoofed advertisement, or anything else that could disturb the environment it's surveying.


Where to Find It

Home Screen
└── IOT/OT tile
    ├── Zigbee Scout   [ESP32-C5 boards only]
    └── OT Air Survey  [all boards]

How It Works

  1. Open IOT/OT → OT Air Survey
  2. Pick a Radio Profile (below) — this controls how CYM's time-slices the radio across WiFi, BLE, ESP-NOW, and 802.15.4
  3. Optionally enter a Site name (building/zone — useful when running several surveys in different locations)
  4. Tap START SURVEY
  5. CYM cycles the radio through each profile's slots automatically in the background — you can leave the screen and use the rest of the device while it runs
  6. Tap STOP SURVEY (or come back to the screen and stop it) when done

Every device seen is written to the SD card as it's discovered — nothing is lost if the survey runs long or the device is power-cycled mid-survey (the SD write is incremental, not all-at-once at the end). (This is now reliably true — see the export note below; earlier builds could silently stop writing partway through a long survey.)

While It's Running

The survey screen shows a live running total and a per-radio breakdown:

Obs: 34
W:12 B:19 EN:2 OT:1
  • Obs — total unique devices seen so far, all radios combined
  • W — WiFi access points
  • B — BLE advertisements (legacy + extended)
  • EN — ESP-NOW traffic
  • OT — everything in the 802.15.4 family lumped together (generic 802.15.4, Zigbee, Thread/Matter, WirelessHART)

Each bucket only counts a device once, on first sighting — see "Note on totals" below for the accuracy history of these numbers.

Radio Profiles

Profile Best for
Balanced Default — general-purpose coverage across all radios
WiFi Heavy Dense AP environments
BLE Heavy Asset tracking, BLE mesh, wearables
802.15.4 Heavy Dense OT/industrial sensor environments (ESP32-C5 only)
ESP-NOW Focus ESP32 mesh / ESP-NOW gateway discovery
Drone Watch OpenDroneID (Remote ID) detection
WirelessHART IEC 62591 industrial process-control networks (ESP32-C5 only)
Thread/Matter Smart-building / home-automation devices (ESP32-C5 only)

Profiles that lean on 802.15.4 automatically fall back to WiFi+BLE on boards without that radio.


What Gets Recorded

Each survey creates its own timestamped session directory on the SD card:

/sdcard/lab/otsurvey/
├── allowlist.json               ← optional — see "Labeling Known Devices" below
└── <session-id>/
    ├── metadata.json             ← one summary record for the whole session
    ├── obs.jsonl                 ← one line per unique device seen
    └── ieee802154.pcapng         ← raw 802.15.4 frames (ESP32-C5 only)

metadata.json — the session summary

Written when the survey starts, and rewritten with final numbers when it stops:

{
  "schema": 1,
  "uuid": "3c8f1a92...",
  "state": "stopped",
  "profile": "Balanced",
  "site": "Building A / Floor 2",
  "start_time": 1234567890,
  "stop_time": 1234568490,
  "obs_count": 127,
  "geo_start": {"valid": true, "lat": 37.386100, "lon": -122.083900, "alt": 12.0, "acc": 5.2},
  "geo_end":   {"valid": true, "lat": 37.386110, "lon": -122.083920, "alt": 12.0, "acc": 5.4}
}
  • geo_start / geo_end — GPS fix at the moment the survey was started and stopped, i.e. where the survey took place. valid: false (all four numeric fields zero) means CYM had no GPS fix — live or last-known — at that moment; the key is always present either way.
  • obs_count — number of unique devices seen (see "Note on totals" below for the accuracy history of this number).

obs.jsonl — one line per device

Newline-delimited JSON, one record per unique device. A device that's seen 50 times during the survey still gets one line — its signal strength, hit count, and last-seen time update in place rather than piling up duplicate lines.

Note on totals — accuracy history: across several builds, three separate bugs inflated the survey's running counts well past the real number of devices in range. All are fixed as of the build that shipped this note.

  • Double-counting re-sightings — the count originally went up on every sighting of a device, not just the first, inflating a normal environment's numbers into the thousands. Fixed by counting a device only the first time it's seen.
  • Phantom "devices" with no real address — a WiFi scan slot or 802.15.4 frame with no usable address (an empty/not-yet-populated scan result, or a frame like an ACK that carries no source address) was being counted as a brand-new unique device every single time, since something with no address can never be recognized as "the same thing seen again." In a busy environment this could dwarf the real device count (one field session logged 97% phantom entries). Fixed by rejecting anything with no real address before it's ever counted.
  • WiFi access points missed or double-logged — the radio could switch away from WiFi to BLE in the middle of an in-progress WiFi scan, which occasionally corrupted that scan's results (the bug above with no real address was often the visible symptom) or meant a real nearby AP got logged again as "new" on a later cycle. Fixed by cleanly stopping any in-progress WiFi scan before switching radios.
  • EN (ESP-NOW) always reading zero — a separate, opposite bug from the three above: the survey's ESP-NOW dwell slot never actually turned on ESP-NOW detection, so EN stayed at 0 no matter how much real ESP-NOW traffic was nearby. Fixed by wiring the same passive detector the dedicated ESP-NOW Scout screen uses into that slot.

If you're on an older build and see the observation count (or the W total specifically) climbing much faster than seems physically plausible, or EN stuck at 0 despite known ESP-NOW devices nearby, update — this is what that was.

Export reliability: the "written to SD as it's discovered, nothing lost" guarantee above also had a gap in some builds — the underlying device-tracking storage is shared across every CYM feature (not reset per survey), and once it filled up, obs.jsonl silently stopped receiving new records for the rest of that survey (and every survey after it, until reboot) while the on-screen counters kept climbing normally with no visible sign anything was wrong. Fixed by giving each survey its own dedicated write queue that doesn't depend on that shared storage staying under capacity.

Fields on every record:

Field What it is
Device type WiFi AP, WiFi client, BLE advertisement, ESP-NOW, 802.15.4 (generic/Zigbee/Thread/WirelessHART), Drone Remote ID
Radio Which of CYM's radios captured it
MAC / address Primary address (peer MAC too, for ESP-NOW)
Signal Current RSSI, peak RSSI, trend (strengthening/weakening/stable), channel
PHY 802.11 b/g/n/ac/ax, BLE 1M/2M/Coded, or 802.15.4 O-QPSK
Label SSID, BLE device name, your allowlist override if one matches, or — for ESP-NOW — a name from /sdcard/lab/espnow/profiles.json if the MAC matches an entry (see ESP-NOW Scout)
Location Lat/lon/altitude/accuracy at the moment this specific device was first logged, plus a live-fix-vs-held-position flag
Timing First-seen and last-seen timestamps
Hit count How many times this device has been re-observed since first seen
Classifier tags Up to 4 evidence tags — e.g. "matched a known vendor OUI," "recurred across sessions," "manufacturer data matched a known pattern"

WiFi-specific: auth mode (open/WPA2/WPA3/etc.), hidden-SSID flag.

BLE-specific: address type — public, static-random, resolvable-private, or non-resolvable-private (see below).

802.15.4/Zigbee/Thread/WirelessHART-specific: PAN ID, short and extended (EUI-64) addresses, frame type, security-enabled flag, link quality (LQI), and — for WirelessHART — the classifier's 0-100 confidence score.

Drone Remote ID-specific: operator ID, the drone's self-reported GPS position (separate from CYM's own GPS position above), and UAS/aircraft type.

ieee802154.pcapng (ESP32-C5 boards)

Raw 802.15.4 MAC frames, openable directly in Wireshark, for anyone who wants to do their own frame-level Zigbee/Thread/WirelessHART analysis beyond what CYM classifies automatically.


BLE Address Type — and Why It's Not a "Device ID"

Modern BLE devices (phones, wearables, trackers) deliberately rotate their advertised MAC address every ~15 minutes specifically so nobody scanning nearby — including CYM — can track them by MAC alone. Each BLE record in obs.jsonl includes an addr_sub field showing which kind of address it saw:

Value Meaning Persistent?
Public Manufacturer-assigned, fixed Yes
Static-random Random but fixed until the device reboots Yes (for the session)
Resolvable-private Rotates on a timer No — expect it to change
Non-resolvable-private Rotates, no relationship between rotations No

This is derived from two bits of the address itself — no pairing required, fully passive. It tells you whether a given MAC is worth treating as "this is the same device across the whole survey" (public/static) or not (the two rotating types).

What this can't do: resolve a rotating address back to a specific known device. That requires the device's IRK (Identity Resolving Key), which only exists after CYM actually bonds/pairs with it — a deliberate, active step, not something a passive survey does to devices it walks past. A "bond with your own devices once, then recognize them automatically in future passive surveys" feature is on the roadmap but not yet implemented.


Labeling Known Devices

Drop a file at /sdcard/lab/otsurvey/allowlist.json (one JSON object per line) to have specific MACs show up with a friendly name instead of their raw address or broadcast name:

{"mac":"24:6f:28:ab:cd:ef","label":"Pump-PLC-01"}
{"mac":"00:1b:1e:02:03:04","label":"WH-Gateway-West"}
{"mac":"aa:bb:cc:00:11:22","label":"BLE-Beacon-Lobby"}
  • Loaded fresh at the start of each survey (up to 64 entries)
  • Overrides the auto-detected SSID/BLE name for matching MACs
  • Exact 6-byte MAC match — no OUI wildcards
  • Hex case doesn't matter

Passive-Only Guarantee

OT Air Survey never transmits. This isn't a UI toggle — it's structural:

  • Starting a survey sets a system-wide flag that every attack/injection feature checks and refuses to run against while a survey is active
  • The WiFi listener uses a passive scan (listens for beacons only — never sends a probe request)
  • The BLE listener uses a passive discovery (never sends a scan request)
  • The 802.15.4 / PCAPNG paths only ever receive; there is no transmit path wired to them

This makes it safe to run in sensitive OT/industrial environments where an active scan (probing every AP, soliciting BLE scan responses) could be disruptive or simply give away that something is scanning.


Part of the Cheap Yellow Monster wiki.

Clone this wiki locally