ESP32-based BLE sniffing network deployed across multiple sites. Current deployments:
Passively captures BLE advertisements, stores to PostgreSQL via MQTT, visualizes in Grafana.
ESP32 (ESPHome) --> WireGuard VPN --> MQTT (Mosquitto, 10.99.0.1)
|
ble-collector (Python)
|
PostgreSQL (ble-postgres)
|
Grafana (ble-grafana)
All services in Docker at /opt/docker/ble-platform/ on 192.168.11.18.
CREATE TABLE advertisements (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
reporter TEXT NOT NULL,
mac TEXT NOT NULL, -- BLE MAC (currently raw, tokenization planned)
rssi INTEGER,
name TEXT,
service_uuids TEXT[],
tx_power INTEGER,
raw_payload JSONB,
address_type TEXT -- 'public' or 'random'
);
CREATE TABLE nodes (
reporter TEXT PRIMARY KEY,
site TEXT NOT NULL,
node_label TEXT NOT NULL,
location TEXT, -- physical description
floor_x_pct FLOAT, -- % of floor plan image width
floor_y_pct FLOAT, -- % of floor plan image height
floor_plan TEXT -- floor plan reference name
);
| reporter | site | label | location | x% | y% | floor plan |
|---|---|---|---|---|---|---|
| ble-sniffer-1 | Mink River Basin | MRB1 | Center junction room | 53 | 43 | 12010 WIS-42 Floor 0 |
| ble-sniffer-2 | Mink River Basin | MRB2 | — | — | — | — |
| ble-sniffer-3 | Mink River Basin | MRB3 | Back room, behind TV | 8 | 76 | 12010 WIS-42 Floor 0 |
| ble-sniffer-4 | Mink River Basin | MRB4 | — | — | — | — |
| ble-sniffer-5 | Mink River Basin | MRB5 | — | — | — | — |
| ble-sniffer-sbm2 | Sister Bay Marina | SBM2 | — | — | — | — |
| Sensor | Real-world position | Physical location |
|---|---|---|
| Sniffer 1 (ble-sniffer-1) | 34.7 ft E, 34.1 ft N | Center junction room |
| Sniffer 3 (ble-sniffer-3) | 5.2 ft E, 60.2 ft N | Back room, behind TV |
Scan: 10s active / 15s interval. WireGuard back to hub. MQTT publish per advertisement.
YAML sources: /home/nate/ble-sniffer-*.yaml on docker2 (192.168.11.18).
| Board type | ESPHome board string | Known nodes |
|---|---|---|
| Standard ESP32 | nodemcu-32s | ble-sniffer-1, 2, 4, 5, sbm1 |
| ESP32-S3 | esp32-s3-devkitc-1 | ble-sniffer-3, sbm2, sbm3 |
Always detect chip type before flashing:
python3 -m esptool --port /dev/ttyACM0 chip_id
Required system dependency on docker2:
sudo apt install python3-venv python3-pip
Without this, ESPHome builds fail with "Missing the 'pip' binary" when a new
ESP-IDF platform version is downloaded.
Flash workflow:
cd /home/nate
esphome run ble-sniffer-3.yaml --device /dev/ttyACM0
Current state: Raw MACs stored in DB.
Planned: One-way hash at ingest in collector.py:
token = hashlib.sha256(mac.encode()).hexdigest()[:16]
Preserves address_type (public vs random) without storing actual identifier.
Provides auditable anonymization regardless of device randomization behavior.
Stronger legal defensibility than relying on device-level MAC randomization.
Problem: Raw captures include exterior devices (passing cars, adjacent buildings)
at low RSSI.
Planned filter: Require a device to be seen by 2 or more sensors within a rolling
time window before including in analysis. Single-sensor sightings are far more likely
to be transient exterior devices. Confidence increases further with 3rd sensor.
-- Devices seen by 2+ sensors in last 5 minutes
SELECT mac, COUNT(DISTINCT reporter) AS sensor_count
FROM advertisements
WHERE ts > NOW() - INTERVAL '5 minutes'
GROUP BY mac
HAVING COUNT(DISTINCT reporter) >= 2;
Indoor path-loss model (n=2.5, TxPower at 1m = -59 dBm):
| Zone | RSSI | Distance | Notes |
|---|---|---|---|
| Near sensor | > -70 dBm | < 10 ft | Strong, reliable for future trilateration |
| Mid-range | -70 to -78 dBm | 10-20 ft | Good usable signal |
| Far | -78 to -85 dBm | 20-35 ft | Marginal |
| Edge | -85 to -90 dBm | 35-50 ft | Weak; S3 blind spot risk |
| Noise/discard | < -90 dBm | > 50 ft | Drop |
MRB 2-sensor recommended thresholds:
File: /opt/docker/ble-platform/grafana/dashboards/ble.json
UID: ble-sniffer — Grafana 10.4.3
Panels added June 2, 2026:
| ID | Title | Type | Notes |
|---|---|---|---|
| 12 | Floor Plan — Live RSSI | Canvas | Sensors at floor plan coordinates with threshold color coding. Add floor plan background manually: Edit panel -> Canvas background -> paste SVG data URI |
| 13 | RSSI per Sensor — 5 min rolling avg | Timeseries | S1 green, S3 orange; per-minute aggregation |
| 14 | Sensor Geometry & RSSI Threshold Guide | Markdown text | Distance/threshold reference table |
| Site | Nodes | WireGuard IPs |
|---|---|---|
| PPBN / Mink River | ble-sniffer-1..5 | 10.99.0.2..6 |
| Sister Bay Marina | ble-sniffer-sbm1..3 | 10.99.0.7..9 |
| ZX Market | planned | TBD |