Beyond Expert-Level Guide to WiFi & BLE Surveillance Device Detection
This document synthesizes knowledge from the DEFLOCK/Flocker codebase with comprehensive research on RF signal detection, IEEE 802.11 frame parsing, BLE advertisement scanning, and device fingerprinting techniques.
Companion documents:
- Signals Quick Reference — Consolidated cheat sheet
- Signals Kismet — Advanced Kismet configuration & API
- Signals Voice Assistant — LFM 2.5 + Parakeet + PocketTTS
- Signals Memory Agent — Agno + SQLite + LanceDB persistence
- Signals Acoustic Detection — Audio surveillance & ShotSpotter
- Signals LoRa & LPWAN — LoRa/LoRaWAN/Meshtastic
- Signals ML Detection — Machine learning anomaly detection
- Signals ESP32-S3 Camera — Camera integration & WiFi field tests
- Signals Image/Video — Camera streaming & processing
- RF Signal Fundamentals
- WiFi Promiscuous Mode Detection
- BLE Advertisement Scanning
- Device Fingerprinting
- Signal Analysis & Ranging
- Detection Targets Reference
- ESP32-S3 Hardware Considerations
- Code Patterns & Best Practices
- Kismet Wireless Monitoring
- Linux Monitor Mode & Channel Control
- Scapy Python Packet Analysis
- RTL-SDR & ISM Band Monitoring
- Wardriving Tools & Integration
The 2.4GHz ISM band provides 13 channels (14 in some regions) with 5MHz spacing. Key characteristics for surveillance detection:
| Property | Value | Detection Impact |
|---|---|---|
| Frequency Range | 2.400–2.4835 GHz | Longer range, better wall penetration than 5GHz |
| Channel Width | 20MHz (standard) | Adjacent channel interference possible |
| Overlapping Channels | 1, 6, 11 are non-overlapping | Target devices often use these |
| Typical Range | 50-100m outdoors | Detection range limited by receiver sensitivity |
BLE uses three dedicated advertising channels to minimize WiFi interference:
| Channel | Frequency | WiFi Relationship |
|---|---|---|
| 37 | 2.402 GHz | Below WiFi Ch1 |
| 38 | 2.426 GHz | Between WiFi Ch6-7 |
| 39 | 2.480 GHz | Above WiFi Ch14 |
Advertising Interval: Surveillance devices typically advertise every 100-1000ms, enabling passive detection without active connections.
ESP32-S3 handles WiFi and BLE via time-division multiplexing:
- Shared Radio: Single 2.4GHz radio services both protocols
- Arbitration: ESP-IDF's coexistence layer prioritizes traffic
- Recommendation: Stagger WiFi channel hops and BLE scans to minimize conflicts
Enable packet capture without association to any network:
#include "esp_wifi.h"
// Enable promiscuous mode
esp_wifi_set_promiscuous(true);
// Register packet callback
esp_wifi_set_promiscuous_rx_cb(wifi_sniffer_callback);
// Filter to management frames only (probes + beacons)
wifi_promiscuous_filter_t filter = {
.filter_mask = WIFI_PROMISCUOUS_FILTER_MASK_MGMT
};
esp_wifi_set_promiscuous_filter(&filter);Key Functions:
| Function | Purpose |
|---|---|
esp_wifi_set_promiscuous(bool) |
Enable/disable monitor mode |
esp_wifi_set_promiscuous_rx_cb() |
Register packet handler |
esp_wifi_set_promiscuous_filter() |
Filter packet types (MGMT/DATA/CTRL) |
esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE) |
Set monitoring channel |
┌──────────────────────────────────────────────────────────────────┐
│ Frame Control │ Duration │ Addr1 (DA) │ Addr2 (SA) │ Addr3 (BSSID) │ Seq │
│ 2 bytes │ 2 bytes │ 6 bytes │ 6 bytes │ 6 bytes │ 2B │
└──────────────────────────────────────────────────────────────────┘
↓
Subtypes: 0x04 = Probe Request
0x08 = Beacon
Address 2 (Source Address): The MAC address of the transmitting device—critical for OUI-based detection.
Extract source MAC from offset 10-15 (0-indexed):
const uint8_t *src_mac = &packet_payload[10];
char mac_str[18];
snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x",
src_mac[0], src_mac[1], src_mac[2],
src_mac[3], src_mac[4], src_mac[5]);Probe Request (subtype 0x04):
- Sent by devices searching for networks
- Contains SSID the device is seeking (or wildcard)
- Body starts immediately with tagged parameters
Beacon (subtype 0x08):
- Broadcast by APs/devices advertising networks
- 12-byte fixed parameters precede tagged parameters:
┌─────────────────────────────────────────────────────────────────┐
│ Timestamp │ Beacon Interval │ Capability │ Tagged Parameters...│
│ 8 bytes │ 2 bytes │ 2 bytes │ (SSID, etc.) │
└─────────────────────────────────────────────────────────────────┘
Tagged parameters use TLV (Type-Length-Value) format:
| Element ID | Length | Value |
|---|---|---|
| 1 byte | 1 byte | Variable |
SSID Element ID = 0
// For beacons: skip 12-byte fixed parameters
const uint8_t *tagged_params = frame_body + 12;
int remaining = frame_body_len - 12;
// For probe requests: start immediately
// const uint8_t *tagged_params = frame_body;
// Parse SSID (ID=0)
if (remaining > 2 && tagged_params[0] == 0) {
uint8_t ssid_len = tagged_params[1];
if (ssid_len <= 32 && ssid_len < remaining - 2) {
memcpy(ssid, &tagged_params[2], ssid_len);
ssid[ssid_len] = '\0';
}
}Surveillance devices may operate on any channel. Systematic hopping ensures coverage:
| Profile | Hop Interval | Use Case | Trade-off |
|---|---|---|---|
| HIGHWAY | 50ms | High-speed (60+ mph) | May miss weak signals |
| URBAN | 100ms | City driving | Balanced |
| SWEEP | 250ms | Stationary/parking | Maximum sensitivity |
Full 2.4GHz sweep time: hop_interval × 13 channels
- HIGHWAY: 650ms
- URBAN: 1.3s
- SWEEP: 3.25s
void hopChannel() {
static uint8_t currentChannel = 1;
currentChannel = (currentChannel % 13) + 1;
esp_wifi_set_channel(currentChannel, WIFI_SECOND_CHAN_NONE);
}Promiscuous callbacks execute in interrupt context. Critical requirements:
void IRAM_ATTR wifi_sniffer_callback(void *buf, wifi_promiscuous_pkt_type_t type) {
// 1. Validate immediately
if (type != WIFI_PKT_MGMT) return;
if (!buf) return;
const wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
// 2. Bounds check before access
if (pkt->rx_ctrl.sig_len < 24) return; // Min header size
if (pkt->rx_ctrl.sig_len > 2500) return; // Max reasonable
// 3. Minimize processing - queue for main loop if complex
// 4. No heap allocation (malloc/new)
// 5. No blocking calls (Serial.print in some versions)
}IRAM_ATTR: Places function in RAM for faster ISR execution.
┌─────────────────────────────────────────────────────────────┐
│ Application Code │
├─────────────────────────────────────────────────────────────┤
│ NimBLEDevice │ NimBLEScan │ NimBLEAdvertisedDevice │
├─────────────────────────────────────────────────────────────┤
│ NimBLE Host Stack │
├─────────────────────────────────────────────────────────────┤
│ ESP32 BLE Controller │
└─────────────────────────────────────────────────────────────┘
Initialization Pattern:
#include <NimBLEDevice.h>
NimBLEDevice::init("");
NimBLEScan* pScan = NimBLEDevice::getScan();
pScan->setAdvertisedDeviceCallbacks(new MyCallbacks());
pScan->setActiveScan(true); // Request scan response
pScan->setInterval(50); // 50ms between scans
pScan->setWindow(50); // 50ms scan window (100% duty)
pScan->start(1, false); // 1 second scan, don't clear resultsAdvertisements contain AD (Advertising Data) structures:
| AD Type | Name | Detection Use |
|---|---|---|
0x01 |
Flags | Device capabilities |
0x02/0x03 |
Incomplete/Complete 16-bit UUIDs | Standard services |
0x06/0x07 |
Incomplete/Complete 128-bit UUIDs | Raven fingerprinting |
0x08/0x09 |
Short/Complete Local Name | Device name matching |
0xFF |
Manufacturer Specific | Vendor data |
Duty Cycle = Window / Interval × 100%
| Profile | Interval | Window | Duty | Power | Detection |
|---|---|---|---|---|---|
| Aggressive | 50ms | 50ms | 100% | High | Maximum |
| Balanced | 50ms | 30ms | 60% | Medium | Good |
| Power-save | 100ms | 30ms | 30% | Low | Reduced |
// Profile-based configuration
const ScanProfileConfig profiles[] = {
{ "HIGHWAY", 50, 50 }, // 100% duty, catch fast-moving devices
{ "URBAN", 50, 50 }, // 100% duty, balanced
{ "SWEEP", 50, 30 } // 60% duty, energy saving
};| Mode | Behavior | Advantage | Disadvantage |
|---|---|---|---|
| Passive | Listen only | Stealthy, low power | Limited data (31 bytes max) |
| Active | Send SCAN_REQ | Gets SCAN_RSP with more data | Detectable, uses airtime |
Recommendation: Use active scanning—surveillance devices aren't typically monitoring for scanners.
| Type | Format | Persistence | Detection Impact |
|---|---|---|---|
| Public | IEEE OUI-based | Permanent | Reliable fingerprinting |
| Static Random | Bit 47:46 = 11 | Per-power-cycle | Semi-reliable |
| Private Resolvable | Bit 47:46 = 01 | Rotating | Difficult to track |
| Private Non-Resolvable | Bit 47:46 = 00 | Rotating | Cannot track |
Surveillance devices typically use public or static random addresses for reliability.
The Organizationally Unique Identifier (OUI) comprises the first 3 bytes of a MAC address:
MAC Address: 58:8e:81:xx:xx:xx
├─────────┤
OUI → "FS Ext Battery" device manufacturer
Known Flock Safety OUI Prefixes (from codebase analysis):
static const char* mac_prefixes[] = {
// FS Ext Battery (BLE beacons)
"58:8e:81", "58:8e:87", "cc:cc:cc", "ec:1b:bd", "90:35:ea",
"04:0d:84", "f0:82:c0", "1c:34:f1", "38:5b:44", "94:34:69",
"b4:e3:f9", "90:35:86",
// Flock WiFi devices
"70:c9:4e", "3c:91:80", "d8:f3:bc", "80:30:49", "14:5a:fc",
"74:4c:a1", "08:3a:88", "9c:2f:9d", "94:08:53", "e4:aa:ea",
// ... (17K+ patterns in detection_patterns.h)
};Matching Algorithm:
bool checkMacPrefix(const uint8_t* mac) {
char macStr[9];
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x",
mac[0], mac[1], mac[2]);
for (int i = 0; i < MAC_PREFIX_COUNT && i < MAX_ITERATIONS; i++) {
if (strncasecmp(macStr, mac_prefixes[i], 8) == 0) {
return true;
}
}
return false;
}Flock Safety and related devices use identifiable SSID patterns:
| Pattern | Device Type | Protocol |
|---|---|---|
FLOCK* |
Flock Safety ALPR | WiFi |
FS Ext Battery |
Extended Battery Module | WiFi/BLE |
Penguin* |
Penguin surveillance | WiFi/BLE |
Pigvision* |
Pigvision systems | WiFi |
Raven* |
ShotSpotter gunshot detector | BLE |
bool checkSsidPattern(const char* ssid) {
static const char* patterns[] = {
"FLOCK", "Flock", "flock",
"FS Ext Battery", "FS+Ext+Battery",
"Penguin", "Pigvision"
};
for (int i = 0; i < PATTERN_COUNT; i++) {
if (strcasestr(ssid, patterns[i])) {
return true;
}
}
return false;
}The most reliable Raven detection method—matching advertised BLE service UUIDs:
| Service UUID | Description | Firmware | Detection Priority |
|---|---|---|---|
00003100-0000-1000-8000-00805f9b34fb |
GPS Location | 1.2.0+ | HIGH (unique) |
00003200-0000-1000-8000-00805f9b34fb |
Power/Battery | 1.2.0+ | HIGH |
00003300-0000-1000-8000-00805f9b34fb |
Network Status | 1.2.0+ | HIGH |
00003400-0000-1000-8000-00805f9b34fb |
Upload Stats | 1.2.0+ | MEDIUM |
00003500-0000-1000-8000-00805f9b34fb |
Error/Failure | 1.2.0+ | MEDIUM |
0000180a-0000-1000-8000-00805f9b34fb |
Device Info | All | LOW (standard) |
00001809-0000-1000-8000-00805f9b34fb |
Health (Legacy) | 1.1.x | MEDIUM |
00001819-0000-1000-8000-00805f9b34fb |
Location (Legacy) | 1.1.x | MEDIUM |
Firmware 1.3.1 Full Characteristic Set:
| Service | Characteristic | UUID | Data |
|---|---|---|---|
| GPS (0x3100) | Latitude | 0x3101 | Float |
| GPS (0x3100) | Longitude | 0x3102 | Float |
| GPS (0x3100) | Altitude | 0x3103 | Float |
| Power (0x3200) | Board Temp | 0x3201 | String |
| Power (0x3200) | Battery Voltage | 0x3202 | String |
| Power (0x3200) | Charge Current | 0x3203 | String |
| Power (0x3200) | Solar Voltage | 0x3204 | String |
| Power (0x3200) | Battery State | 0x3205 | String |
| Network (0x3300) | LTE RSSI | 0x3304 | String |
| Network (0x3300) | WiFi SSID | 0x3308 | String |
| Upload (0x3400) | Audio Uploads | 0x3403 | Count |
UUID Matching Implementation:
bool checkRavenServiceUuid(NimBLEAdvertisedDevice* device) {
if (!device->haveServiceUUID()) return false;
static const char* raven_uuids[] = {
"00003100-0000-1000-8000-00805f9b34fb", // GPS
"00003200-0000-1000-8000-00805f9b34fb", // Power
"00003300-0000-1000-8000-00805f9b34fb", // Network
"00003400-0000-1000-8000-00805f9b34fb", // Upload
"00003500-0000-1000-8000-00805f9b34fb", // Error
"00001809-0000-1000-8000-00805f9b34fb", // Legacy Health
"00001819-0000-1000-8000-00805f9b34fb" // Legacy Location
};
int serviceCount = min(device->getServiceUUIDCount(), 16);
for (int i = 0; i < serviceCount; i++) {
std::string uuid = device->getServiceUUID(i).toString();
for (int j = 0; j < 7; j++) {
if (strcasecmp(uuid.c_str(), raven_uuids[j]) == 0) {
return true;
}
}
}
return false;
}Raven firmware version can be inferred from advertised services:
| Services Present | Firmware Version |
|---|---|
| 0x1809, 0x1819 only | 1.1.x (Legacy) |
| 0x3100, 0x3200, 0x3300 | 1.2.x |
| 0x3100–0x3500 + 0x3205 | 1.3.x (Latest) |
RSSI (Received Signal Strength Indicator) measures signal power in dBm:
| RSSI Range | Signal Quality | Detection Confidence | Typical Distance |
|---|---|---|---|
| > -50 dBm | Excellent | Very High | < 3m |
| -50 to -65 dBm | Good | High | 3-10m |
| -65 to -75 dBm | Fair | Medium | 10-25m |
| -75 to -85 dBm | Weak | Low | 25-50m |
| < -85 dBm | Poor | Very Low | > 50m |
The log-distance path loss model:
d = 10^((A - RSSI) / (10 × n))
Where:
- d = Estimated distance (meters)
- A = RSSI at 1 meter reference distance (typically -40 to -60 dBm)
- RSSI = Measured signal strength (dBm)
- n = Path loss exponent
| Environment | Path Loss Exponent (n) |
|---|---|
| Free space | 2.0 |
| Open outdoor | 2.0–2.5 |
| Urban outdoor | 2.7–3.5 |
| Indoor (light walls) | 3.0–4.0 |
| Indoor (heavy walls) | 4.0–6.0 |
Example Calculation:
Given: A = -45 dBm, RSSI = -72 dBm, n = 2.5 (urban)
d = 10^((-45 - (-72)) / (10 × 2.5))
d = 10^(27 / 25)
d = 10^1.08
d ≈ 12 meters
Expect ±6 dB fluctuation due to:
- Multipath interference
- Antenna orientation
- Body/vehicle shadowing
- Environmental changes
Filtering Recommendations:
#define RSSI_SAMPLES 5
int filterRssi(int newRssi) {
static int samples[RSSI_SAMPLES];
static int index = 0;
samples[index] = newRssi;
index = (index + 1) % RSSI_SAMPLES;
// Running average
int sum = 0;
for (int i = 0; i < RSSI_SAMPLES; i++) {
sum += samples[i];
}
return sum / RSSI_SAMPLES;
}The codebase implements a rolling window confidence meter:
#define ASSURITY_WINDOW_SIZE 12
#define ASSURITY_DECAY_MS 30000
int assurityBuffer[ASSURITY_WINDOW_SIZE];
int assurityLevel = 0;
void recordPositiveDetection(int score) {
// score: 1-10 based on detection quality
// FLOCK/RAVEN direct match = 10
// MAC prefix match = 7
// Weak signal match = 3
assurityBuffer[index] = constrain(score, 1, 10);
index = (index + 1) % ASSURITY_WINDOW_SIZE;
// Calculate percentage
int sum = 0;
for (int i = 0; i < ASSURITY_WINDOW_SIZE; i++) {
sum += assurityBuffer[i];
}
assurityLevel = (sum * 100) / (ASSURITY_WINDOW_SIZE * 10);
}
const char* getAssurityLabel(int level) {
if (level >= 75) return "CONFIRMED";
if (level >= 50) return "LIKELY";
if (level >= 25) return "POSSIBLE";
return "SCANNING";
}Primary Detection Methods: WiFi SSID, MAC OUI, BLE device name
| Attribute | Pattern | Priority |
|---|---|---|
| WiFi SSID | FLOCK*, Flock-*, FS_* |
HIGH |
| MAC Prefix | See §4.1 (17K+ patterns) | HIGH |
| BLE Name | Flock*, FLOCK* |
MEDIUM |
Physical Characteristics:
- Mounted on poles/streetlights
- Solar-powered variants common
- Cellular backhaul (LTE)
Primary Detection Methods: BLE advertisement, WiFi probe
| Attribute | Pattern | Priority |
|---|---|---|
| BLE Name | FS Ext Battery, FS+Ext+Battery |
CRITICAL |
| MAC Prefix | 58:8e:81, 58:8e:87 |
HIGH |
| WiFi SSID | FS Ext Battery |
HIGH |
Note: Extended batteries advertise more frequently than cameras—often the first detected component.
Primary Detection Methods: BLE Service UUID fingerprinting
| Attribute | Pattern | Priority |
|---|---|---|
| Service UUID | 00003100-* (GPS) |
CRITICAL |
| Service UUID | 00003200-* (Power) |
HIGH |
| BLE Name | Raven*, ShotSpotter* |
MEDIUM |
Firmware Versions in Field:
- 1.1.7 (Legacy)
- 1.2.0 (Transitional)
- 1.3.1 (Current)
Threat Score: 100 (maximum) due to acoustic surveillance capability.
| Attribute | Pattern | Priority |
|---|---|---|
| WiFi SSID | Penguin* |
HIGH |
| BLE Name | Penguin* |
HIGH |
| Attribute | Pattern | Priority |
|---|---|---|
| WiFi SSID | Pigvision* |
MEDIUM |
| BLE Name | Pigvision* |
MEDIUM |
| Component | GPIO | Notes |
|---|---|---|
| OLED SDA | 17 | 128x64 SSD1306 |
| OLED SCL | 18 | I2C clock |
| OLED RST | 21 | Display reset |
| Vext Power | 36 | OLED power control |
| PRG Button | 0 | User/reset button |
| LED | 35 | Status indicator |
OLED Power Sequence:
// Enable OLED power
pinMode(VEXT_PIN, OUTPUT);
digitalWrite(VEXT_PIN, LOW); // Active LOW
// Reset display
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW);
delay(50);
digitalWrite(OLED_RST, HIGH);
// Initialize
Wire.begin(OLED_SDA, OLED_SCL);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);ESP32-S3 shares a single radio between protocols:
| Configuration | WiFi Impact | BLE Impact |
|---|---|---|
| WiFi Priority | Full throughput | Delayed scans |
| BLE Priority | Missed packets | Full scan rate |
| Balanced | Channel hop during BLE gaps | Scan during WiFi switch |
Recommended Approach:
void loop() {
// 1. WiFi channel hop (fast)
hopChannel();
// 2. BLE scan during WiFi dwell time
if (millis() - lastBleScan >= bleScanInterval) {
pBLEScan->start(1, false); // 1 second async scan
lastBleScan = millis();
}
// 3. Process results
updateDisplay();
}JPL Rule 6 compliance—automatic recovery from hangs:
#include "esp_task_wdt.h"
#define WDT_TIMEOUT_SEC 30
void setup() {
esp_task_wdt_init(WDT_TIMEOUT_SEC, true); // 30s, panic=true
esp_task_wdt_add(NULL); // Add current task
}
void loop() {
esp_task_wdt_reset(); // Pet the watchdog each iteration
// ... processing
}Retain detection statistics across power cycles:
#include <Preferences.h>
Preferences prefs;
void loadPersistentData() {
prefs.begin("deflock", true); // read-only
totalDetections = prefs.getInt("totalDet", 0);
flockDetections = prefs.getInt("flockDet", 0);
ravenDetections = prefs.getInt("ravenDet", 0);
prefs.end();
}
void savePersistentData() {
prefs.begin("deflock", false); // read-write
prefs.putInt("totalDet", totalDetections);
prefs.putInt("flockDet", flockDetections);
prefs.putInt("ravenDet", ravenDetections);
prefs.end();
}Prevent alert spam while maintaining tracking:
#define DEDUP_CACHE_SIZE 16
#define DEDUP_COOLDOWN_MS 3000
struct DedupEntry {
char macAddress[18];
unsigned long lastSeen;
};
static DedupEntry dedupCache[DEDUP_CACHE_SIZE];
bool shouldAlertForDevice(const char* macAddr) {
unsigned long now = millis();
for (int i = 0; i < DEDUP_CACHE_SIZE; i++) {
if (strcasecmp(dedupCache[i].macAddress, macAddr) == 0) {
if (now - dedupCache[i].lastSeen < DEDUP_COOLDOWN_MS) {
dedupCache[i].lastSeen = now;
return false; // Still in cooldown
}
dedupCache[i].lastSeen = now;
return true; // Cooldown expired
}
}
// New device - add to cache
addToCache(macAddr, now);
return true;
}Prevent buffer overflows and injection:
static void sanitizeString(char* dest, const char* src, size_t maxLen) {
if (!dest || maxLen == 0) return;
if (!src) { dest[0] = '\0'; return; }
size_t i = 0, j = 0;
while (i < maxLen - 1 && src[j] != '\0') {
// Only printable ASCII
if (src[j] >= 0x20 && src[j] <= 0x7E) {
dest[i++] = src[j];
}
j++;
}
dest[i] = '\0';
}
static int sanitizeRssi(int rssi) {
return constrain(rssi, -127, 0);
}
static bool isValidMacFormat(const char* mac) {
if (!mac || strlen(mac) != 17) return false;
for (int i = 0; i < 17; i++) {
if (i % 3 == 2) {
if (mac[i] != ':') return false;
} else {
if (!isxdigit(mac[i])) return false;
}
}
return true;
}All loops must have static upper bounds:
#define MAX_PATTERN_ITERATIONS 64
#define MAX_SERVICE_UUID_ITERATIONS 16
bool checkMacPrefix(const uint8_t* mac) {
// Explicit bound prevents runaway iteration
const int iterLimit = min(MAC_PREFIX_COUNT, MAX_PATTERN_ITERATIONS);
for (int i = 0; i < iterLimit; i++) {
if (matchesPrefix(mac, mac_prefixes[i])) {
return true;
}
}
return false;
}Configurable profiles for different use cases:
enum ScanProfile { PROFILE_HIGHWAY, PROFILE_URBAN, PROFILE_SWEEP };
struct ScanProfileConfig {
const char* name;
const char* shortName;
int channelHopMs;
int bleScanDuration;
int bleScanInterval;
int bleWindow; // Duty cycle control
};
static const ScanProfileConfig profiles[] = {
{ "HIGHWAY", "HWY+", 50, 1, 300, 50 }, // 100% BLE duty
{ "URBAN", "URB~", 100, 1, 500, 50 }, // 100% BLE duty
{ "SWEEP", "SWP-", 250, 1, 800, 30 } // 60% BLE duty
};
void applyScanProfile(ScanProfile profile) {
channelHopInterval = profiles[profile].channelHopMs;
pBLEScan->setWindow(profiles[profile].bleWindow);
pBLEScan->setInterval(50); // Keep interval constant
}int calculateThreatScore(const char* deviceType, int rssi) {
int score = 70; // Base score
// Device type bonuses
if (strstr(deviceType, "FLOCK")) score = 95;
else if (strstr(deviceType, "RAVEN")) score = 100; // Max threat
else if (strstr(deviceType, "PENGUIN")) score = 90;
// Signal strength adjustment
if (rssi > -50) score = min(score + 5, 100); // Very close
if (rssi < -80) score = max(score - 10, 50); // Far away
return score;
}[Start] → Enable Promiscuous → Register Callback → Set Channel
↓
Receive Packet
↓
Validate Frame Type (MGMT)
↓
Extract: Addr2 (MAC), SSID
↓
Match: OUI prefix OR SSID pattern
↓
[DETECTION ALERT]
[Start] → Init NimBLE → Configure Scan → Start Scanning
↓
onResult Callback
↓
Extract: MAC, Name, Service UUIDs
↓
Match: MAC prefix OR Name pattern OR Raven UUID
↓
[DETECTION ALERT]
| UUID | Device | Type |
|---|---|---|
00003100-* |
Raven GPS | BLE Service |
00003200-* |
Raven Power | BLE Service |
58:8e:81:* |
FS Battery | MAC OUI |
FLOCK* |
Flock Camera | WiFi SSID |
Kismet is an open-source 802.11 wireless network detector, sniffer, and WIDS (Wireless Intrusion Detection System) that operates in passive mode, making it ideal for surveillance device detection.
┌─────────────────────────────────────────────────────────────────────┐
│ Kismet Architecture │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ kismet_server │ ←── │ kismet_client │ │ Drones │ │
│ │ (Core/UI) │ │ (Web UI) │ │ (Remote Cap) │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ ↑ │ │
│ │ │ │
│ ┌──────┴──────────────────────────────────────────┴───────┐ │
│ │ Datasources │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ WiFi │ │ BLE │ │ RTL-SDR │ │ ADSB │ │ │
│ │ │ Adapter │ │ Adapter │ │ 433MHz │ │ Decoder │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Components:
- kismet_server: Centralized packet decoding, device tracking, alerting
- kismet_client: Web-based UI (default port 2501)
- Datasources: Capture interfaces (WiFi, BLE, SDR, etc.)
- Drones: Remote capture devices forwarding packets to server
# Ubuntu/Debian
sudo apt-get install kismet kismet-plugins
# Add user to kismet group (avoids sudo for captures)
sudo usermod -aG kismet $USER
# Start Kismet
kismetAccess Web UI: http://localhost:2501
Edit /etc/kismet/kismet.conf or pass via CLI:
# WiFi interface in monitor mode
kismet -c wlan0mon
# Multiple sources
kismet -c wlan0mon -c wlan1mon -c hci0:type=linuxbluetooth
# Remote capture (from drone device)
kismet_cap_linux_wifi --connect 192.168.1.100:3501 --source wlan0monkismet.conf datasource examples:
# WiFi with channel hopping
source=wlan0mon:name=wifi_monitor,hop=true,hop_rate=5/sec
# Lock to specific channels (surveillance hotspots)
source=wlan1mon:name=fixed_scanner,channels="1,6,11"
# Bluetooth/BLE via HCI
source=hci0:type=linuxbluetooth,name=ble_scanner
# RTL-SDR for 433MHz ISM
source=rtl433-0:type=rtl433,name=ism_scannerServer Configuration (kismet.conf):
remote_capture_enabled=true
remote_capture_listen=0.0.0.0
remote_capture_port=3501Drone/Remote Device:
# On remote Raspberry Pi with WiFi adapter
kismet_cap_linux_wifi --tcp --connect 192.168.1.10:3501 --source wlan0monKismet provides a comprehensive REST API for scripting detection systems:
# Get all devices (JSON)
curl -u admin:password http://localhost:2501/devices/all_devices.json
# Filter by device type
curl http://localhost:2501/devices/by-type/Wi-Fi%20AP/devices.json
# Get device by MAC
curl http://localhost:2501/devices/by-mac/AA:BB:CC:DD:EE:FF/device.json
# Subscribe to alerts
curl http://localhost:2501/alerts/last-time/0/alerts.jsonPython Automation Example:
import requests
import json
KISMET_URL = "http://localhost:2501"
AUTH = ("admin", "password")
def get_all_devices():
resp = requests.get(f"{KISMET_URL}/devices/all_devices.json", auth=AUTH)
return resp.json()
def find_flock_devices():
devices = get_all_devices()
flock_matches = []
for dev in devices:
ssid = dev.get("kismet.device.base.name", "")
mac = dev.get("kismet.device.base.macaddr", "")
if "FLOCK" in ssid.upper() or "FS EXT" in ssid.upper():
flock_matches.append({"mac": mac, "ssid": ssid})
return flock_matches
# Poll for new detections
for match in find_flock_devices():
print(f"FLOCK DEVICE: {match['ssid']} - {match['mac']}")kismet_alerts.conf:
# Alert on specific SSID patterns
alert=SURVSSID,10/min,5/sec,Surveillance SSID detected
# Alert on OUI prefixes
alert=SURVOUI,10/min,5/sec,Surveillance device OUI detectedkismet_filter.conf (filter specific MACs/SSIDs):
# Track these SSIDs
ssid_track=FLOCK,Penguin,Pigvision,FS Ext Battery
# Track these OUI prefixes
oui_track=58:8E:81,70:C9:4E,3C:91:80| Format | Extension | Use |
|---|---|---|
| Kismet DB | .kismet |
SQLite3, full device/packet data |
| PCAP-NG | .pcapng |
Wireshark-compatible packets |
| JSON | .json |
Device summaries |
Extract devices from Kismet DB:
sqlite3 Kismet-*.kismet "SELECT devmac,type,phyname FROM devices;"Convert to Wireshark-compatible PCAP:
kismetdb-to-pcap --in Kismet-*.kismet --out capture.pcapngairmon-ng (from Aircrack-ng suite) is the most reliable method:
# Install Aircrack-ng
sudo apt-get install aircrack-ng
# List wireless interfaces
sudo airmon-ng
# Kill interfering processes (NetworkManager, wpa_supplicant)
sudo airmon-ng check kill
# Start monitor mode (creates wlan0mon)
sudo airmon-ng start wlan0
# Verify
iwconfig wlan0mon
# Mode:Monitor Frequency:2.437 GHz
# Stop monitor mode
sudo airmon-ng stop wlan0monWhen airmon-ng isn't available:
# Bring interface down
sudo ip link set wlan0 down
# Set monitor mode
sudo iw dev wlan0 set type monitor
# Bring interface up
sudo ip link set wlan0 up
# Verify
iw dev wlan0 info
# type monitor# Set specific channel
sudo iw dev wlan0mon set channel 6
# Set channel with HT mode (for 802.11n)
sudo iw dev wlan0mon set channel 6 HT20
sudo iw dev wlan0mon set channel 6 HT40+
# Verify current channel
iw dev wlan0mon info | grep channel#!/bin/bash
# channel_hopper.sh - Hop through 2.4GHz channels
INTERFACE="wlan0mon"
CHANNELS="1 2 3 4 5 6 7 8 9 10 11 12 13"
HOP_INTERVAL=0.25 # 250ms
while true; do
for ch in $CHANNELS; do
iw dev "$INTERFACE" set channel "$ch" 2>/dev/null
sleep "$HOP_INTERVAL"
done
done# Does adapter support monitor mode?
iw list | grep -A 10 "Supported interface modes" | grep monitor
# Supported channels/frequencies
iw list | grep -A 50 "Frequencies:"
# Driver information
ethtool -i wlan0Recommended Adapters for Monitor Mode:
| Chipset | Driver | Monitor | Injection | Notes |
|---|---|---|---|---|
| Atheros AR9271 | ath9k_htc | ✅ | ✅ | ALFA AWUS036NHA |
| Ralink RT3070 | rt2800usb | ✅ | ✅ | Common, well-supported |
| Realtek RTL8812AU | rtl8812au | ✅ | ✅ | 5GHz support |
| MediaTek MT7612U | mt76x2u | ✅ | ✅ | Dual-band |
Scapy is a powerful Python library for packet crafting and analysis, ideal for building custom surveillance detection scripts.
pip install scapy
# For full 802.11 support
sudo apt-get install tcpdump#!/usr/bin/env python3
from scapy.all import *
def packet_handler(pkt):
if pkt.haslayer(Dot11):
# Extract frame type/subtype
frame_type = pkt.type
frame_subtype = pkt.subtype
# Get addresses
src_mac = pkt.addr2
dst_mac = pkt.addr1
bssid = pkt.addr3
print(f"Type: {frame_type}, Subtype: {frame_subtype}")
print(f" Src: {src_mac}, Dst: {dst_mac}, BSSID: {bssid}")
# Sniff on monitor interface
sniff(iface="wlan0mon", prn=packet_handler, count=100, store=0)#!/usr/bin/env python3
from scapy.all import *
detected_ssids = set()
def extract_ssid(pkt):
if pkt.haslayer(Dot11Beacon) or pkt.haslayer(Dot11ProbeResp):
ssid = pkt[Dot11Elt].info.decode('utf-8', errors='ignore')
bssid = pkt[Dot11].addr3
if ssid and (bssid, ssid) not in detected_ssids:
detected_ssids.add((bssid, ssid))
# Check for surveillance patterns
if any(p in ssid.upper() for p in ['FLOCK', 'PENGUIN', 'FS EXT']):
print(f"🚨 SURVEILLANCE: {ssid} ({bssid})")
else:
print(f"Network: {ssid} ({bssid})")
sniff(iface="wlan0mon", prn=extract_ssid, store=0)# Management frames only (beacons, probes)
sniff(iface="wlan0mon", filter="type mgt", prn=handler)
# Beacons only (subtype 8)
sniff(iface="wlan0mon", filter="type mgt subtype beacon", prn=handler)
# Probe requests (subtype 4)
sniff(iface="wlan0mon", filter="type mgt subtype probe-req", prn=handler)
# Frames from specific BSSID
sniff(iface="wlan0mon", filter="wlan addr2 AA:BB:CC:DD:EE:FF", prn=handler)def get_rssi(pkt):
if pkt.haslayer(RadioTap):
# dBm signal strength
try:
rssi = pkt[RadioTap].dBm_AntSignal
return rssi
except:
return None
return None
def handler_with_rssi(pkt):
if pkt.haslayer(Dot11Beacon):
ssid = pkt[Dot11Elt].info.decode('utf-8', errors='ignore')
bssid = pkt[Dot11].addr3
rssi = get_rssi(pkt)
print(f"{ssid} ({bssid}) RSSI: {rssi} dBm")
sniff(iface="wlan0mon", filter="type mgt subtype beacon", prn=handler_with_rssi)from scapy.all import *
# Capture and save
packets = sniff(iface="wlan0mon", count=1000)
wrpcap("surveillance_capture.pcap", packets)
# Read existing capture
packets = rdpcap("surveillance_capture.pcap")
for pkt in packets:
if pkt.haslayer(Dot11Beacon):
print(pkt.summary())#!/usr/bin/env python3
"""Scapy-based surveillance device detector"""
from scapy.all import *
from datetime import datetime
import json
SURVEILLANCE_SSIDS = ["FLOCK", "PENGUIN", "PIGVISION", "FS EXT", "RAVEN"]
SURVEILLANCE_OUIS = ["58:8E:81", "70:C9:4E", "3C:91:80", "D8:F3:BC"]
detections = []
def check_surveillance(ssid, mac):
# SSID check
for pattern in SURVEILLANCE_SSIDS:
if pattern in ssid.upper():
return True, f"SSID match: {pattern}"
# OUI check
oui = mac[:8].upper()
if oui in SURVEILLANCE_OUIS:
return True, f"OUI match: {oui}"
return False, None
def surveillance_handler(pkt):
if pkt.haslayer(Dot11Beacon) or pkt.haslayer(Dot11ProbeResp):
try:
ssid = pkt[Dot11Elt].info.decode('utf-8', errors='ignore')
bssid = pkt[Dot11].addr3
rssi = pkt[RadioTap].dBm_AntSignal if pkt.haslayer(RadioTap) else None
is_surveillance, reason = check_surveillance(ssid, bssid)
if is_surveillance:
detection = {
"timestamp": datetime.now().isoformat(),
"ssid": ssid,
"bssid": bssid,
"rssi": rssi,
"reason": reason
}
detections.append(detection)
print(f"🚨 ALERT: {json.dumps(detection)}")
except Exception as e:
pass
print("Starting surveillance detection...")
print("Monitoring for: " + ", ".join(SURVEILLANCE_SSIDS))
try:
sniff(iface="wlan0mon", prn=surveillance_handler, store=0)
except KeyboardInterrupt:
print(f"\nTotal detections: {len(detections)}")
with open("detections.json", "w") as f:
json.dump(detections, f, indent=2)RTL-SDR dongles enable monitoring of the 433MHz ISM band, where many IoT devices and some surveillance equipment transmit.
| Dongle | Frequency Range | Notes |
|---|---|---|
| RTL-SDR Blog V3 | 500kHz–1.7GHz | Best sensitivity, bias-T |
| NooElec NESDR | 24MHz–1.7GHz | Budget option |
| Generic RTL2832U | 24MHz–1.7GHz | Varies by tuner |
# Ubuntu/Debian
sudo apt-get install rtl-sdr rtl-433
# macOS
brew install rtl_433
# From source
git clone https://github.com/merbanan/rtl_433.git
cd rtl_433 && mkdir build && cd build
cmake .. && make && sudo make install# Start monitoring (auto-detect devices)
rtl_433
# Analyze unknown signals
rtl_433 -a
# JSON output for parsing
rtl_433 -F json
# Specific frequency (433.92 MHz)
rtl_433 -f 433920000
# Multiple frequencies
rtl_433 -f 315000000 -f 433920000 -f 868000000rtl_433 includes 200+ decoders for common devices:
# List available decoders
rtl_433 -R help
# Enable specific decoders only
rtl_433 -R 40 -R 41 # Oregon Scientific sensors
# Disable specific decoders
rtl_433 -R -40 # Disable decoder 40For unknown surveillance devices:
# Analyze mode - capture and decode unknown signals
rtl_433 -a
# Save raw I/Q data for analysis
rtl_433 -S all # Save all signals
rtl_433 -S unknown # Save only unknown signals
# Output saved to g###_###M_###k.cu8 filesAnalyze saved signal:
# Inspect binary patterns
rtl_433 -r g001_433.92M_250k.cu8 -A
# Decode with specific pulse parameters
rtl_433 -r g001_433.92M_250k.cu8 -X "n=custom,m=OOK_PWM,s=400,l=800,r=1000"#!/bin/bash
# rtl_monitor.sh - Continuous ISM band monitoring with logging
LOG_DIR="/var/log/rtl_433"
mkdir -p "$LOG_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOGFILE="$LOG_DIR/ism_$TIMESTAMP.json"
rtl_433 -f 433920000 -f 315000000 -F json 2>/dev/null | \
while read -r line; do
echo "$line" >> "$LOGFILE"
# Check for surveillance keywords
if echo "$line" | grep -qi "flock\|raven\|surveillance"; then
echo "ALERT: $line" | tee -a "$LOG_DIR/alerts.log"
fi
done#!/usr/bin/env python3
import subprocess
import json
def monitor_ism_band(callback, frequency=433920000):
"""Monitor ISM band and call callback for each decoded signal"""
cmd = ["rtl_433", "-f", str(frequency), "-F", "json"]
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
text=True, bufsize=1
)
for line in process.stdout:
try:
data = json.loads(line.strip())
callback(data)
except json.JSONDecodeError:
pass
def surveillance_check(data):
"""Check if signal matches surveillance device patterns"""
model = data.get("model", "").lower()
if any(p in model for p in ["flock", "raven", "shot"]):
print(f"🚨 ISM SURVEILLANCE: {data}")
else:
print(f"Device: {data.get('model')} - {data.get('id')}")
if __name__ == "__main__":
monitor_ism_band(surveillance_check)WiGLE (Wireless Geographic Logging Engine) is the premier wardriving database.
API Setup:
# Get API token from wigle.net/account
export WIGLE_API_NAME="your_api_name"
export WIGLE_API_TOKEN="your_api_token"Query nearby networks:
import requests
WIGLE_URL = "https://api.wigle.net/api/v2"
AUTH = (WIGLE_API_NAME, WIGLE_API_TOKEN)
def search_networks(lat, lon, radius_km=1):
"""Search WiGLE for networks near coordinates"""
params = {
"latrange1": lat - 0.01 * radius_km,
"latrange2": lat + 0.01 * radius_km,
"longrange1": lon - 0.01 * radius_km,
"longrange2": lon + 0.01 * radius_km,
}
resp = requests.get(f"{WIGLE_URL}/network/search", params=params, auth=AUTH)
return resp.json()
def find_surveillance_networks(lat, lon):
"""Find potential surveillance networks in WiGLE database"""
networks = search_networks(lat, lon)
for net in networks.get("results", []):
ssid = net.get("ssid", "")
if any(p in ssid.upper() for p in ["FLOCK", "PENGUIN", "FS EXT"]):
print(f"📍 WIGLE HIT: {ssid} at ({net['trilat']}, {net['trilong']})")airodump-ng for passive scanning:
# Start capture with CSV output
sudo airodump-ng wlan0mon --write scan_output --output-format csv
# Target specific channel
sudo airodump-ng wlan0mon -c 6 --bssid AA:BB:CC:DD:EE:FF -w targetParse airodump-ng CSV:
import csv
def parse_airodump_csv(filepath):
"""Parse airodump-ng CSV output for surveillance devices"""
with open(filepath, 'r') as f:
reader = csv.reader(f)
for row in reader:
if len(row) < 14:
continue
bssid = row[0].strip()
channel = row[3].strip()
essid = row[13].strip() if len(row) > 13 else ""
if any(p in essid.upper() for p in ["FLOCK", "PENGUIN"]):
print(f"SURVEILLANCE: {essid} ({bssid}) Ch{channel}")Start gpsd:
sudo apt-get install gpsd gpsd-clients
# USB GPS
sudo gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock
# Phone GPS via Bluetooth
sudo gpsd /dev/rfcomm0Python GPS client:
import gps
def get_current_position():
"""Get current GPS coordinates from gpsd"""
session = gps.gps(mode=gps.WATCH_ENABLE)
for report in session:
if report['class'] == 'TPV':
lat = getattr(report, 'lat', None)
lon = getattr(report, 'lon', None)
if lat and lon:
return lat, lon
return None, NoneKali NetHunter provides mobile wardriving with GPS:
# Enable monitor mode on external adapter
airmon-ng start wlan1
# Start GPS publisher
sudo gpsctl -n /dev/ttyUSB0
# Launch Kismet with GPS and WiFi
kismet -c wlan1mon --use-gpsd-gps#!/usr/bin/env python3
"""Complete wardriving-style surveillance detector"""
import threading
import subprocess
import json
from datetime import datetime
import gps
import sqlite3
class SurveillanceDetector:
def __init__(self, db_path="detections.db"):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.setup_database()
self.gps_position = (None, None)
def setup_database(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS detections (
id INTEGER PRIMARY KEY,
timestamp TEXT,
ssid TEXT,
bssid TEXT,
rssi INTEGER,
latitude REAL,
longitude REAL,
detection_type TEXT,
protocol TEXT
)
""")
self.conn.commit()
def log_detection(self, ssid, bssid, rssi, detection_type, protocol):
lat, lon = self.gps_position
self.conn.execute(
"INSERT INTO detections VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)",
(datetime.now().isoformat(), ssid, bssid, rssi,
lat, lon, detection_type, protocol)
)
self.conn.commit()
print(f"🚨 {protocol}: {ssid} ({bssid}) @ ({lat}, {lon})")
def gps_thread(self):
session = gps.gps(mode=gps.WATCH_ENABLE)
for report in session:
if report['class'] == 'TPV':
self.gps_position = (
getattr(report, 'lat', None),
getattr(report, 'lon', None)
)
def kismet_thread(self, kismet_url="http://localhost:2501"):
# Poll Kismet REST API
import requests
while True:
try:
resp = requests.get(f"{kismet_url}/devices/all_devices.json")
for dev in resp.json():
ssid = dev.get("kismet.device.base.name", "")
if "FLOCK" in ssid.upper():
self.log_detection(ssid, dev.get("kismet.device.base.macaddr"),
None, "FLOCK", "WIFI")
except:
pass
time.sleep(5)
def run(self):
threads = [
threading.Thread(target=self.gps_thread, daemon=True),
threading.Thread(target=self.kismet_thread, daemon=True),
]
for t in threads:
t.start()
print("Surveillance detector running. Press Ctrl+C to stop.")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Shutting down...")
if __name__ == "__main__":
detector = SurveillanceDetector()
detector.run()[Start] → Enable Promiscuous → Register Callback → Set Channel
↓
Receive Packet
↓
Validate Frame Type (MGMT)
↓
Extract: Addr2 (MAC), SSID
↓
Match: OUI prefix OR SSID pattern
↓
[DETECTION ALERT]
[Start] → Init NimBLE → Configure Scan → Start Scanning
↓
onResult Callback
↓
Extract: MAC, Name, Service UUIDs
↓
Match: MAC prefix OR Name pattern OR Raven UUID
↓
[DETECTION ALERT]
[Start] → Configure Datasources → Start kismet_server
↓
Collect from WiFi/BLE/SDR
↓
Query REST API (devices.json)
↓
Filter: SSID/MAC/OUI patterns
↓
Log with GPS coordinates
↓
[DETECTION ALERT]
| UUID | Device | Type |
|---|---|---|
00003100-* |
Raven GPS | BLE Service |
00003200-* |
Raven Power | BLE Service |
58:8e:81:* |
FS Battery | MAC OUI |
FLOCK* |
Flock Camera | WiFi SSID |
| Task | Command |
|---|---|
| Monitor mode | sudo airmon-ng start wlan0 |
| Set channel | sudo iw dev wlan0mon set channel 6 |
| Scapy sniff | sniff(iface="wlan0mon", filter="type mgt") |
| Kismet start | kismet -c wlan0mon |
| RTL-SDR scan | rtl_433 -F json |
- Signals Quick Reference — Fast lookup cheat sheet
- Signals Acoustic Detection — ShotSpotter/Raven audio, ESP32 microphones, ML classification
- Signals LoRa & LPWAN — LoRa sniffing, Meshtastic detection, regional frequencies
- Signals ML Detection — Anomaly detection, TensorFlow Lite on ESP32, behavior profiling
- Signals ESP32-S3 Camera — XIAO ESP32-S3 camera, WiFi findings
- Signals Image/Video — OV2640, MJPEG streaming, NGINX
- Main Scanner:
display-clients/esp32s3-heltec-v4/src/main.cpp - Detection Patterns:
src/detection_patterns.h(17K+ entries) - Raven UUIDs:
datasets/raven_configurations.json
- DeFlock.me: Crowdsourced ALPR locations and detection patterns
- IEEE OUI Database: https://standards-oui.ieee.org
- ESP-IDF WiFi API: https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/network/esp_wifi.html
- NimBLE-Arduino: https://github.com/h2zero/NimBLE-Arduino
- Kismet Wireless: https://www.kismetwireless.net
- Scapy Documentation: https://scapy.net
- rtl_433: https://github.com/merbanan/rtl_433
- WiGLE API: https://api.wigle.net
- Aircrack-ng: https://www.aircrack-ng.org
- GainSec: Raven BLE service UUID dataset
- FoggedLens/deflock: Detection methodologies
- colonelpanichacks: Original flock-you implementation
Document Version: 3.0 | Updated: 2026-02-06 | Based on DEFLOCK v3.2.0-secure | Part of signals detection suite