-
Notifications
You must be signed in to change notification settings - Fork 6
OT 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.
Home Screen
└── IOT/OT tile
├── Zigbee Scout [ESP32-C5 boards only]
└── OT Air Survey [all boards]
- Open IOT/OT → OT Air Survey
- Pick a Radio Profile (below) — this controls how CYM's time-slices the radio across WiFi, BLE, ESP-NOW, and 802.15.4
- Optionally enter a Site name (building/zone — useful when running several surveys in different locations)
- Tap START SURVEY
- 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
- 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.)
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.
| 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.
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)
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).
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.
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.
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.
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
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.