16 KiB
Cheatsheet
Environment Setup
source ~/esp/esp-idf/export.sh # Activate ESP-IDF (every shell)
Build & Flash
cd ~/git/esp32-hacking/get-started/csi_recv_router
idf.py set-target esp32 # Set chip target
idf.py menuconfig # Configure WiFi/UDP settings
idf.py build # Compile firmware
idf.py -p /dev/ttyUSB0 -b 460800 flash # Flash to device (460800 baud)
idf.py -p /dev/ttyUSB0 monitor # Serial monitor (Ctrl+] to exit)
idf.py -p /dev/ttyUSB0 -b 460800 flash monitor # Flash + monitor combined
idf.py fullclean # Clean build directory
idf.py reconfigure # Re-fetch managed components
Deployed Sensors
| Name | IP | mDNS | Location |
|---|---|---|---|
| muddy-storm | 192.168.129.29 | muddy-storm.local | Living Room |
| amber-maple | 192.168.129.30 | amber-maple.local | Office |
| hollow-acorn | 192.168.129.31 | hollow-acorn.local | Kitchen |
Target: 192.168.129.11:5500 (Pi) | Cmd port: 5501
Remote Management (esp-cmd)
esp-cmd <host> STATUS # Full device status (see STATUS Fields below)
esp-cmd <host> PROFILE # Heap, stack watermarks, CPU runtime stats
esp-cmd <host> IDENTIFY # LED solid 5s (find the device)
esp-cmd <host> RATE 50 # Set ping rate to 50 Hz (disables adaptive)
esp-cmd <host> POWER 15 # Set TX power to 15 dBm (NVS saved)
esp-cmd <host> ADAPTIVE ON # Enable adaptive sampling (NVS saved)
esp-cmd <host> ADAPTIVE OFF # Disable adaptive sampling
esp-cmd <host> THRESHOLD 0.005 # Set motion sensitivity (NVS saved)
esp-cmd <host> OTA http://pi:8070/fw # Trigger OTA update (use esp-ota instead)
esp-cmd <host> HOSTNAME mydevice # Set hostname (NVS saved, mDNS updated)
esp-cmd <host> SCANRATE 60 # BLE scan restart interval (5-300s)
esp-cmd <host> PROBERATE 5 # Probe dedup cooldown (1-300s)
esp-cmd <host> CSIMODE # Query current CSI output mode
esp-cmd <host> CSIMODE RAW # Full I/Q array (default, ~900 B/pkt)
esp-cmd <host> CSIMODE COMPACT # Features only (~200 B/pkt)
esp-cmd <host> CSIMODE HYBRID 10 # Compact + raw every Nth packet
esp-cmd <host> AUTH # Query auth status (on/off)
esp-cmd <host> AUTH mysecret123 # Enable HMAC auth (8-64 char secret)
esp-cmd <host> FLOODTHRESH # Query deauth flood threshold (5/10s)
esp-cmd <host> FLOODTHRESH 10 30 # Set: 10 deauths in 30s = flood
esp-cmd <host> CALIBRATE # Start baseline capture (default 10s, room must be empty)
esp-cmd <host> CALIBRATE 20 # Calibrate for 20 seconds
esp-cmd <host> CALIBRATE STATUS # Show baseline info (valid/invalid, nsub)
esp-cmd <host> CALIBRATE CLEAR # Delete baseline, disable presence
esp-cmd <host> PRESENCE # Query: on/off, baseline, threshold, score
esp-cmd <host> PRESENCE ON # Enable presence detection (needs baseline)
esp-cmd <host> PRESENCE OFF # Disable presence detection
esp-cmd <host> PRESENCE THRESHOLD 0.08 # Set presence threshold (0.001-1.0)
esp-cmd <host> REBOOT # Restart device
Host can be an IP or mDNS name (amber-maple.local).
esp-cmd amber-maple.local STATUS # Single device via mDNS
esp-cmd 192.168.129.29 IDENTIFY # Single device via IP
Fleet Management (esp-fleet)
esp-fleet status # Query all sensors at once
esp-fleet identify # Blink all LEDs
esp-fleet rate 50 # Set rate on all devices
esp-fleet reboot # Reboot entire fleet
esp-fleet ota # OTA update all (sequential)
esp-fleet ota /path/to/firmware.bin # OTA with custom firmware
OTA Updates (esp-ota)
esp-ota amber-maple.local # OTA with default build
esp-ota amber-maple.local -f fw.bin # OTA with custom firmware
esp-ota amber-maple.local --no-wait # Fire and forget
First flash after enabling OTA requires USB (partition table change). After that, all updates are OTA.
OTA Flow
- Verifies device is alive via STATUS
- Starts temp HTTP server on Pi (port 8070)
- Sends
OTA http://<pi>:8070/<fw>.binvia UDP - Device downloads, flashes, reboots
- Verifies device responds post-reboot
Rollback
If new firmware crashes or hangs, the 30s watchdog reboots and bootloader automatically rolls back to the previous firmware.
CSI Output Modes
| Mode | Payload | Size | BW @ 100 Hz |
|---|---|---|---|
| RAW (default) | "[I,Q,I,Q,...]" (128 values) |
~900 B | ~90 KB/s |
| COMPACT | "F:rms,std,max,idx,energy" |
~200 B | ~20 KB/s |
| HYBRID N | Compact every packet, raw every Nth | ~270 B avg (N=10) | ~27 KB/s |
Compact features (per packet, from 64 I/Q subcarrier pairs):
| Feature | Type | Description |
|---|---|---|
| amp_rms | float | RMS amplitude = sqrt(mean(I²+Q²)) |
| amp_std | float | Std dev of per-subcarrier amplitudes |
| amp_max | float | Peak subcarrier amplitude |
| amp_max_idx | uint8 | Index (0-63) of peak subcarrier |
| energy | uint32 | L1 norm (same as adaptive sampling) |
esp-cmd amber-maple.local CSIMODE COMPACT # Switch to compact
esp-cmd amber-maple.local CSIMODE HYBRID 10 # Raw every 10th packet
esp-cmd amber-maple.local CSIMODE RAW # Back to full I/Q
esp-cmd amber-maple.local CSIMODE # Query current mode
Mode is NVS-persisted and survives reboots.
Adaptive Sampling
When enabled, the device automatically adjusts ping rate based on CSI wander:
- Motion detected (wander > threshold): 100 pkt/s
- Idle (wander < threshold for 3s): 10 pkt/s
- Rate changes send
EVENT,<hostname>,motion=<0|1> rate=<hz> wander=<value>via UDP
esp-cmd amber-maple.local ADAPTIVE ON # Enable
esp-cmd amber-maple.local THRESHOLD 0.005 # Tune sensitivity
# Lower threshold = more sensitive, higher = less sensitive
# Good starting range: 0.001 - 0.01
Presence Detection
CSI-based presence detection compares live per-subcarrier amplitudes against a baseline captured with the room empty.
Setup
# 1. Ensure room is empty, CSI is flowing (200+ packets)
esp-cmd amber-maple.local CALIBRATE 10 # Capture 10s baseline
# Wait for: EVENT,amber-maple,calibrate=done packets=1000 nsub=52
# 2. Verify baseline
esp-cmd amber-maple.local CALIBRATE STATUS # Should show "valid nsub=52"
# 3. Enable presence detection
esp-cmd amber-maple.local PRESENCE ON # Starts scoring
# 4. Tune threshold (optional)
esp-cmd amber-maple.local PRESENCE THRESHOLD 0.08 # Higher = less sensitive
How It Works
- Baseline: Average per-subcarrier amplitude over N seconds (stored in NVS)
- Scoring: Normalized Euclidean distance:
sqrt(sum((live-baseline)^2) / sum(baseline^2)) - Window: Rolling average of 50 scores
- Threshold: Default 0.05 (5% normalized distance)
- Holdoff: 10s debounce between state transitions
- Events:
EVENT,<hostname>,presence=1 score=0.0832/EVENT,<hostname>,presence=0 score=0.0234 - Memory: ~1 KB total (baseline + accumulators + score buffer)
LED States
Default mode is quiet (LED off unless noteworthy). Use LED AUTO for constant blink.
| LED (quiet mode) | Meaning |
|---|---|
| Off | Normal operation |
| Solid | Motion or presence detected |
| Double blink | OTA in progress |
| Solid (5s) | IDENTIFY command active |
| LED (auto mode) | Meaning |
|---|---|
| Off | Not connected to WiFi |
| Slow blink (1 Hz) | Connected, no CSI activity |
| Fast blink (5 Hz) | CSI data flowing |
| Double blink | OTA in progress |
Sensor Discovery
esp-ctl discover # Find all sensors via mDNS
esp-ctl discover -t 5 # Longer browse (5s timeout)
esp-ctl status --discover # Status using discovered fleet
esp-ctl target --discover # Query targets via discovery
Sensors register their hostname via mDNS on boot.
HMAC Command Authentication
# Set auth secret on device (requires existing auth or serial access)
esp-ctl cmd amber-maple.local "AUTH mysecretkey123"
# Set env var so all tools sign commands automatically
export ESP_CMD_SECRET="mysecretkey123" # add to ~/.bashrc.secrets
# All esp-cmd/esp-ctl/esp-fleet/esp-ota commands auto-sign when ESP_CMD_SECRET is set
# Unsigned commands are rejected with "ERR AUTH required"
Protocol: HMAC:<32hex>:<uptime_s>:<cmd> — first 32 hex chars of HMAC-SHA256(secret, <uptime_s>:<cmd>).
Replay window: +/-5s from device uptime.
Serial Console (physical access)
Connect via USB serial (921600 baud) for auth management without network auth:
# Connect to serial console
idf.py -p /dev/ttyUSB0 monitor # or: screen /dev/ttyUSB0 921600
# Serial commands (type directly):
AUTH # Show full secret (unredacted)
AUTH <secret> # Set new secret (8-64 chars)
AUTH OFF # Clear secret (disable auth)
STATUS # Basic device info
HELP # List serial commands
Provisioning Tool
esp-provision # Auto-generate secret, set via serial
esp-provision mysecretkey123 # Set specific secret via serial
esp-provision --serial # Set via serial console (device running)
esp-provision --generate-only # Just print a random secret
esp-provision -p /dev/ttyACM0 # Use different serial port
OUI Vendor Lookup
esp-ctl oui --update # Download IEEE OUI database (~30k entries)
esp-ctl oui b0:be:76:a1:2d:c0 # Look up vendor for a MAC
Watch Daemon & OSINT
esp-ctl watch # Listen on :5500, store probes/BLE/alerts in DB
esp-ctl watch -c ~/my-config.yaml # Custom config (HA webhooks, known MACs)
esp-ctl watch -v # Verbose logging
esp-ctl osint probes # Probe SSID history table
esp-ctl osint probes --mac AA:BB:CC:DD:EE:FF # Filter by MAC
esp-ctl osint devices # All device sightings
esp-ctl osint devices -t ble # BLE only
esp-ctl osint mac AA:BB:CC:DD:EE:FF # Full profile for one MAC
esp-ctl osint stats # Summary counts
DB: ~/.local/share/esp-ctl/osint.db (SQLite with WAL).
Config: ~/.config/esp-ctl/watch.yaml (HA webhooks, known MACs file).
Test CSI Reception
nc -lu 5500 # Listen for CSI packets
socat UDP-RECV:5500 STDOUT # Alternative listener
nc -lu 5500 | head -1 # See one packet
nc -lu 5500 | wc -l # Count packets/sec (Ctrl+C)
esp-ctl listen -f probe -n 5 # Capture 5 probe requests (ESP32-C6+ only)
esp-ctl listen -f alert # Monitor deauth/disassoc alerts (ESP32-C6+ only)
Firmware Variants
| Firmware | Dir | Output | Needs |
|---|---|---|---|
| csi_recv_router | get-started/csi_recv_router/ |
UDP | WiFi router |
| csi_recv | get-started/csi_recv/ |
Serial | csi_send device |
| csi_send | get-started/csi_send/ |
N/A | csi_recv device |
Key Config (menuconfig)
| Path | Setting |
|---|---|
| Example Connection Configuration → SSID | WiFi network name |
| Example Connection Configuration → Password | WiFi password |
| CSI UDP Configuration → IP | 192.168.129.11 |
| CSI UDP Configuration → Port | 5500 |
| CSI UDP Configuration → Cmd port | 5501 |
| CSI UDP Configuration → Hostname | mDNS name (e.g., amber-maple) |
USB Flash Notes
- Use 460800 baud (
-b 460800) — 921600 causes connection failures on some boards - Hostname can be changed at runtime via
esp-ctl cmd <host> HOSTNAME <name> - First flash after enabling OTA partitions must be via USB
Data Packet Formats
All data packets include sensor hostname after the type tag:
CSI_DATA,<hostname>,seq,mac,rssi,rate,...,len,first_word,"[I,Q,...]" # RAW mode
CSI_DATA,<hostname>,seq,mac,rssi,rate,...,len,first_word,"F:rms,std,max,idx,energy" # COMPACT mode
BLE_DATA,<hostname>,mac,rssi,pub|rnd,name
EVENT,<hostname>,motion=0|1 rate=<hz> wander=<value>
EVENT,<hostname>,calibrate=done packets=<n> nsub=<n>
EVENT,<hostname>,presence=0|1 score=<float>
ALERT_DATA,<hostname>,deauth|disassoc,sender_mac,target_mac,rssi
ALERT_DATA,<hostname>,deauth_flood,<count>,<window_s>
PROBE_DATA,<hostname>,mac,rssi,ssid
CSI mode discriminator: quoted field starts with [ (raw) or F: (compact).
Note: On original ESP32, promiscuous mode (ALERT_DATA, PROBE_DATA) is disabled because it breaks CSI data collection at the driver level. These packet types are only generated on ESP32-C6 and newer chips.
STATUS Fields
Unauthenticated STATUS returns a minimal subset. Full fields require HMAC auth.
| Field | Example | Auth | Description |
|---|---|---|---|
| hostname | amber-maple | no | Device hostname |
| uptime | 1h23m | no | Human-readable uptime |
| uptime_s | 4980 | no | Raw uptime in seconds |
| rssi | -67 | no | Current AP RSSI (dBm) |
| channel | 11 | no | WiFi channel |
| version | 27aeddb |
no | Firmware git commit |
| motion | 0/1 | no | Motion detected |
| presence | on/off | no | Presence detection |
| heap | 108744 | yes | Free heap bytes |
| tx_power | 10 | yes | TX power (dBm) |
| rate | 100 | yes | Target CSI rate (Hz) |
| csi_rate | 97 | yes | Actual CSI rate (Hz, computed) |
| adaptive | on/off | yes | Adaptive sampling |
| ble | on/off | yes | BLE scanning |
| target | 192.168.129.11:5500 | yes | UDP destination |
| temp | 0.0 | yes | Chip temperature (ESP32-S2/C3/C6 only) |
| csi_count | 30002 | yes | Total CSI frames since boot |
| boots | 3 | yes | Boot count (NVS persisted) |
| rssi_min | -71 | yes | Lowest RSSI since boot |
| rssi_max | -62 | yes | Highest RSSI since boot |
| csi_mode | raw/compact/hybrid | yes | CSI output mode |
| hybrid_n | 10 | yes | Raw packet interval (hybrid mode) |
| auth | on/off | yes | HMAC command authentication |
| flood_thresh | 5/10 | yes | Deauth flood: count/window_seconds |
| powersave | on/off | yes | WiFi modem sleep |
| pr_score | 0.0432 | yes | Current presence score |
PROFILE Sections
| Section | Fields | Description |
|---|---|---|
| HEAP | free, min, dram, iram | Heap usage and watermarks |
| TASKS | stack_free per task | Per-task stack high watermark |
| CPU | % per task | FreeRTOS runtime stats (requires CONFIG_FREERTOS_USE_TRACE_FACILITY) |
Source Paths
| File | Purpose |
|---|---|
get-started/csi_recv_router/main/app_main.c |
Main firmware source |
get-started/csi_recv_router/main/Kconfig.projbuild |
UDP/cmd port config |
tools/esp-cmd |
Pi-side management CLI |
tools/esp-ota |
Pi-side OTA update tool |
tools/esp-fleet |
Fleet-wide command tool |
get-started/csi_recv_router/sdkconfig.defaults |
SDK defaults |
get-started/csi_recv_router/main/idf_component.yml |
Dependencies |
get-started/csi_recv_router/CMakeLists.txt |
Build config |
ESP-IDF Paths
| Path | Contents |
|---|---|
~/esp/esp-idf/ |
ESP-IDF v5.5.2 |
~/esp/esp-csi/ |
Original esp-csi examples |
~/.espressif/tools/ |
Xtensa toolchain |
USB Serial
ls /dev/ttyUSB* /dev/ttyACM* # Find connected devices
dmesg | tail # Check USB detection
sudo usermod -aG dialout $USER # Fix permissions (re-login)