Tuning Guide
Quick guide to tune ESPectre for reliable movement detection in your environment.
Note on Detection Algorithms: This guide focuses on MVS (the default detection algorithm). Filters (low-pass, Hampel) apply to both MVS and ML detectors.
Quick Start (5 minutes)
Note on Subcarrier Selection (MVS only): ESPectre automatically selects optimal subcarriers at boot using the NBVI algorithm. No manual configuration needed. ML mode uses fixed subcarriers.
1. Flash and Boot
After flashing your device with ESPHome:
# View logs to monitor calibration
esphome logs <your-config>.yaml
2. Wait for Band Calibration
On first boot, keep the room empty and still for 10 seconds. The system will:
1. Collect CSI packets during baseline (10 Ã window_size, default 750 packets)
2. Run NBVI calibration algorithm to select optimal subcarriers
3. Select 12 optimal subcarriers for motion detection
4. Calculate adaptive threshold based on baseline noise
Look for log messages like:
[I][Calibration]: â Calibration successful: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
3. Test Movement
Walk around the room while monitoring logs:
esphome logs <your-config>.yaml
Look for state changes:
- state=MOTION when moving
- state=IDLE when still
4. Adjust Threshold if Needed
By default, ESPectre uses an adaptive threshold calculated automatically during calibration based on baseline noise. This works well in most environments.
espectre:
segmentation_threshold: auto
| Value | Description |
|---|---|
auto |
Adaptive threshold - Minimizes false positives (default) |
min |
Maximum sensitivity (may have false positives) |
0.0-10.0 |
Fixed manual threshold |
Examples:
espectre:
segmentation_threshold: auto # Default, zero FP
# segmentation_threshold: min # Max sensitivity
# segmentation_threshold: 1.5 # Fixed manual value
Rule of thumb:
- Too many false positives â use auto or increase threshold (try 2.0-5.0)
- Missing movements â use min or decrease threshold (try 0.5-0.8)
After changing, re-flash:
esphome run <your-config>.yaml
Interactive tuning: You can also adjust the threshold in real-time using ESPectre - The Game. Connect via USB, drag the threshold slider, and see immediate visual feedback. Note that runtime adjustments are temporary (session-only) - the adaptive threshold is recalculated on every boot.
Understanding Parameters
The following parameters apply to the MVS detection algorithm.
Segmentation Threshold
What it does: Determines sensitivity for motion detection (MVS only).
Default: auto (adaptive threshold, minimizes false positives)
| Value | Sensitivity | Use Case |
|---|---|---|
| 0.5-1.0 | High | Detect subtle movements |
| 1.5-3.0 | Medium | General purpose, most environments |
| 3.0-5.0 | Low | Noisy environments, reduce false positives |
| 5.0-10.0 | Very Low | Only detect significant movements |
Configuration:
espectre:
segmentation_threshold: auto # or "min" or a number (0.0-10.0)
| Value | Formula | Effect |
|---|---|---|
auto |
Adaptive | Minimizes false positives (default) |
min |
Maximum sensitivity | Catches faint motion |
| number | Fixed | Manual override |
Note: Runtime adjustments via Home Assistant slider are temporary (session-only). The adaptive threshold is recalculated on every boot.
Detection Algorithm (mvs/ml)
What it does: Selects the motion detection algorithm.
Default: mvs
espectre:
detection_algorithm: mvs # or ml
| Algorithm | Description | Threshold Range | Best For |
|---|---|---|---|
mvs |
Moving Variance Segmentation | 0.0 - 10.0 | General purpose, adaptive |
ml |
Neural Network (MLP 12â16â8â1) | 0.0 - 10.0 (scaled metric) | Higher accuracy |
ML Detector Notes:
- Uses fixed subcarriers [12, 14, 16, 18, 20, 24, 28, 36, 40, 44, 48, 52] for consistency with training
- Motion decision uses a scaled ML metric with default threshold 5.0 (range 0.0-10.0, aligned with MVS UI)
- Pre-trained weights are embedded in the component (no external files needed)
- Architecture validated as optimal (12â16â8â1, 353 params, 1.4 KB) via 5-fold CV
- Training uses early stopping, dropout, class weights, and LR scheduling
Window Size (10-200 packets)
What it does: Number of turbulence samples used to calculate moving variance.
Default: 75 packets
| Value | Response | Stability | Use Case |
|---|---|---|---|
| 10-30 | Fast | Noisy | Quick response needed |
| 50-100 | Balanced | Good | Recommended |
| 100-200 | Slow | Very stable | Reduce flickering |
Configuration:
espectre:
segmentation_window_size: 75 # default
Optimal Window Configuration Guide
To detect general movement (walking, arm movement, standing up), you need to balance **sensitivity** (capturing even minimal movements) and **robustness** (ignoring noise). **Sampling Rate ($F_s$)** Maintaining $F_s = 100 \text{ Hz}$ is an excellent compromise between accuracy and computational load for detecting most human activities. **Moving Window Size ($N$)** For general movement detection, a window is recommended that captures transient action while being long enough to dampen high-frequency noise. | $T_{window}$ | $N$ (at 100 Hz) | Advantage for Presence Detection | |--------------|-----------------|----------------------------------| | $0.5$ seconds | $50$ packets | Extremely reactive, but too sensitive to noise. | | $1$ second | $100$ packets | **Recommended**. Optimal balance, captures $1-2$ steps or a complete gesture. | | $2$ seconds | $200$ packets | Slower to react, but very robust against false positives. | **Recommendation:** Start with $N=75$ packets (corresponding to $0.75$ seconds at 100 pps). This is the default and a good starting point for detecting activities like entering a room.Traffic Generator Rate (0-1000 pps)
What it does: Controls how many packets per second are sent for CSI measurement.
Default: 100 pps
| Rate | Use Case |
|---|---|
| 50 pps | Basic presence detection, minimal overhead |
| 100 pps | Recommended - Activity recognition |
| 600-1000 pps | Fast motion detection, precision localization |
| 0 pps | Disabled - use external WiFi traffic (see External Traffic Mode) |
Configuration:
espectre:
traffic_generator_rate: 100
Publish Interval (1-1000 packets)
What it does: Controls how many CSI packets are processed before updating Home Assistant sensors.
Default: Same as traffic_generator_rate (or 100 if traffic generator is disabled)
| Scenario | Configuration | Update Frequency |
|---|---|---|
| Default | traffic_generator_rate: 100 |
~1 update/sec |
| Faster updates | publish_interval: 50 |
~2 updates/sec |
| External traffic | traffic_generator_rate: 0, publish_interval: 100 |
Depends on traffic |
Configuration:
espectre:
traffic_generator_rate: 100
publish_interval: 50 # Optional: override publish rate
Note: Lower
publish_intervalvalues increase CPU usage and Home Assistant traffic but provide more responsive detection.
Understanding Sampling Rates (Nyquist-Shannon Theorem)
The traffic generator rate determines the maximum frequency of motion that can be accurately detected. According to the **Nyquist-Shannon Sampling Theorem**, the sampling rate (Fs) must be at least twice the maximum frequency of the signal (Fmax): $$F_s \geq 2 \times F_{max}$$ In Wi-Fi sensing, Fmax is the highest Doppler frequency generated by human movement reflected in the CSI signal. **Application Scenarios:** | Activity Type | Max Frequency (Fmax) | Minimum Sampling Rate (Fs) | Recommended Rate | |---------------|---------------------|---------------------------|------------------| | **Vital signs** (breathing, heartbeat) | < 5 Hz | âĨ 10 Hz | 10-30 pps | | **Activity recognition** (walking, sitting, gestures) | â 10-30 Hz | âĨ 60 Hz | 60-100 pps | | **Fast motion** (rapid gestures, precision localization) | â 300-400 Hz | âĨ 600 Hz | 600-1000 pps | **Key Takeaways:** - Higher rates enable detection of faster movements - Lower rates are sufficient for slow movements (vital signs, presence) - Choose the rate based on your application requirements - Higher rates increase CPU usage and network trafficAdaptive Threshold
Automatic Threshold Calibration
What it does: Automatically calculates the optimal detection threshold based on baseline noise characteristics. This minimizes false positives while maintaining high recall.
Status: Always enabled (automatic)
How it works:
1. During calibration, collects 10 Ã window_size CSI packets during baseline (default 750, room must be quiet)
2. Band selection uses NBVI algorithm to select optimal subcarriers
3. Threshold calculation depends on segmentation_threshold setting
| Mode | Formula | Description |
|---|---|---|
auto (default) |
P95 Ã 1.1 | Minimizes false positives |
min |
P100 Ã 1.0 | Maximum sensitivity |
Note: Band selection always uses NBVI algorithm. Only the threshold calculation varies.
Configuration:
espectre:
segmentation_threshold: auto # or "min" or a number
Benefits:
- Minimizes false positives with auto mode
- High recall (>98%) maintained across environments
- Same algorithm works on ESP32-S3, C6, C3, etc.
- No device-specific tuning required
- Fully automatic - no manual threshold adjustment needed
When to override: - Too many false positives â increase threshold (try 2.0-5.0) - Missing movements â decrease threshold (try 0.5-0.8)
Hampel Filter
Hampel Filter (Outlier Removal)
What it does: Removes statistical outliers from turbulence values using MAD (Median Absolute Deviation). This can help reduce false positives caused by sudden interference.
Applies to: Both MVS and ML detectors.
Default: Disabled
â ī¸ Note: The Hampel filter is disabled by default because MVS already provides robust motion detection with 0% false positives in typical environments. Enabling it reduces detection sensitivity (Recall drops from 98.1% to 96.3%). Only enable in environments with high electromagnetic interference causing sudden spikes.
Configuration:
espectre:
hampel_enabled: true
hampel_window: 7
hampel_threshold: 4.0
See SETUP.md for parameter details.
When to enable: - Environments with high electromagnetic interference causing sudden spikes - Industrial settings with heavy machinery - Proximity to microwave ovens or multiple WiFi access points - Experiencing unexplained false positives during baseline (room empty)
When to keep disabled (default): - Normal home/office environment - Maximum detection sensitivity needed
Gain Lock
AGC/FFT Gain Lock
What it does: Locks the AGC (Automatic Gain Control) and FFT gain values after initial calibration to eliminate amplitude variations caused by the WiFi hardware. This improves CSI stability and motion detection accuracy.
Default: auto
Configuration:
espectre:
gain_lock: auto # auto (default), enabled, disabled
| Mode | Description |
|---|---|
auto |
Enable gain lock but skip if signal too strong (AGC < 30). Uses CV normalization when skipped. Recommended. |
enabled |
Always force gain lock. May freeze if too close to AP. |
disabled |
Never lock gain. Uses CV normalization for stable detection. Works at any distance. |
How it works:
1. During the first 300 packets (~3 seconds), ESPectre collects AGC/FFT samples and calculates the median (more robust than mean against outliers)
2. These values are then "locked" (forced) to eliminate hardware-induced variations
3. In auto mode, if AGC < 30 (signal too strong), gain lock is skipped and CV normalization is enabled instead
4. In disabled mode, baseline is collected but never locked; CV normalization provides stable detection
CV normalization: When gain is not locked (skipped or disabled), spatial turbulence is calculated as the Coefficient of Variation (CV = std/mean) instead of raw standard deviation. This is gain-invariant: if all amplitudes are scaled by factor k (due to AGC), then Ī(kA)/Îŧ(kA) = Ī(A)/Îŧ(A). This maintains detection accuracy without hardware locking.
When to change from auto:
| Situation | Recommended Setting |
|---|---|
| Normal operation (3-8m from AP) | auto (default) |
| Testing very close to AP (< 2m) | disabled |
| Debugging calibration issues | disabled |
| Maximum CSI stability needed | enabled (if RSSI < -40 dB) |
Warning log when signal too strong:
[W][GainController]: Signal too strong (AGC=14 < 30) - skipping gain lock
[W][GainController]: Move sensor 2-3 meters from AP for optimal performance
Note: Gain lock is only available on ESP32-S3, ESP32-C3, ESP32-C5, and ESP32-C6. On ESP32 (original) and ESP32-S2, it's automatically skipped (not supported by hardware).
Low-Pass Filter
Low-Pass Filter (Noise Reduction)
What it does: Removes high-frequency noise from turbulence values using a 1st-order Butterworth IIR filter. This significantly reduces false positives in noisy RF environments.
Applies to: Both MVS and ML detectors.
Default: Disabled
âšī¸ Note: The low-pass filter is disabled by default for maximum simplicity. Enable it if you experience false positives in noisy RF environments.
Configuration:
espectre:
lowpass_enabled: true
lowpass_cutoff: 11.0
See SETUP.md for parameter details.
Cutoff frequency guide: - Lower (5-8 Hz): More aggressive filtering, reduces FP more but may miss fast movements - Default (11 Hz): Good balance (92% recall, <3% FP) - Higher (15-20 Hz): Less filtering, higher recall but more FP
When to adjust: - Increase cutoff if detecting fast movements (sports, rapid gestures) - Decrease cutoff in very noisy RF environments with persistent FP
Sensor Placement
Distance from Access Point
The distance between the ESP32 sensor and your WiFi access point (AP) significantly impacts CSI quality and system stability.
| Distance | RSSI | AGC | Status | Notes |
|---|---|---|---|---|
| < 0.5m | > -30 dB | 0-15 | System may freeze | Too close, signal saturated |
| 0.5-2m | -30 to -40 dB | 15-30 | Marginal | Works with gain_lock: disabled |
| 3-8m | -40 to -70 dB | 30-60 | Optimal | Best CSI quality and stability |
| 8-15m | -70 to -80 dB | 60-80 | Good | Still reliable detection |
| > 15m | < -80 dB | > 80 | Reduced quality | Weaker signal, more noise |
Why distance matters:
When the sensor is too close to the AP, the received signal is extremely strong, causing:
1. AGC saturation: The automatic gain control cannot reduce amplification enough
2. CSI distortion: Signal clipping leads to unreliable CSI data
3. Gain lock freeze: When phy_force_rx_gain() is called with a very low AGC value, the WiFi driver may fail to decode frames, halting CSI reception entirely
Symptoms of being too close: - System freezes after "Auto-Calibration Starting" log - Repeated "ping_sock: send error=0" messages - Low AGC values in logs (< 30) - High RSSI (> -40 dB)
Solution:
1. Move the sensor 2-3 meters away from the AP
2. Or set gain_lock: disabled (less optimal but works at any distance)
Checking your placement:
Look at the gain lock log after WiFi connection:
[I][GainController]: Gain locked: AGC=51, FFT=234 (after 300 packets)
- AGC > 30: Good placement, gain lock works correctly
- AGC < 30: Consider moving the sensor further from the AP
Troubleshooting
Too Many False Positives
Symptoms: Detects motion when room is empty.
Solutions (try in order):
-
Increase threshold:
yaml espectre: segmentation_threshold: 3.0 # Try 2.0-5.0 -
Increase window size (more stable):
yaml espectre: segmentation_window_size: 100 # Try 100-150 -
Enable low-pass filter (removes RF noise):
yaml espectre: lowpass_enabled: true lowpass_cutoff: 11.0 -
Enable Hampel filter (removes spikes from interference):
yaml espectre: hampel_enabled: true -
Check for interference sources:
- Fans, AC units, moving curtains
- Microwave ovens, other WiFi networks
- Bluetooth devices, cordless phones
-
Pets moving in the room
-
Re-calibrate: Reset calibration (see below) in a quiet room
Missing Movements
Symptoms: Doesn't detect when people move.
Solutions (try in order):
-
Decrease threshold:
yaml espectre: segmentation_threshold: 0.5 # Try 0.5-0.8 -
Decrease window size (faster response):
yaml espectre: segmentation_window_size: 30 # Try 25-40 -
Check sensor position:
- Optimal: 3-8m from router
- Avoid placing behind furniture or walls
-
Line of sight to monitored area helps
-
Verify traffic generator is active:
yaml espectre: traffic_generator_rate: 100 # Must be > 0
No CSI Packets
Symptoms: Logs show no CSI data or "CSI disabled".
Solutions:
-
Verify WiFi connection: Check logs for successful connection to AP
-
Check traffic generator:
yaml espectre: traffic_generator_rate: 100 # Must be > 0 -
Verify ESP-IDF configuration: Ensure
CONFIG_ESP_WIFI_CSI_ENABLED: yin sdkconfig -
Check router compatibility: Some mesh routers or WiFi 6E may have issues
-
If protocol/bandwidth logs show
unavailable: this can happen on some target/band mode API paths and does not automatically mean CSI is broken. Focus on CSI packet flow (pps, dropped packets, calibration progress) to assess runtime health.
System Freezes During Calibration
Symptoms: Device freezes after "Auto-Calibration Starting (file-based storage)" message. May show watchdog timeout or repeated "ping_sock: send error=0" messages.
Cause: Sensor is too close to the access point. When RSSI > -40 dB, the AGC value is very low (< 30), and forcing this gain causes the WiFi driver to fail decoding frames.
Solutions:
- Move the sensor further from the AP (recommended):
- Place at least 2-3 meters away
- Optimal distance: 3-8 meters
-
Check logs for AGC value > 30 after gain lock
-
Disable gain lock (workaround):
yaml espectre: gain_lock: disabledThis allows operation at any distance but with slightly less stable CSI. -
Use
automode (default, v2.4.0+):yaml espectre: gain_lock: auto # Default - skips gain lock if AGC < 30Inautomode, ESPectre automatically skips gain lock when the signal is too strong, logging a warning instead of freezing.
Diagnosis:
Check the gain lock log:
[I][GainController]: Gain locked: AGC=51, FFT=234 # Good - AGC > 30
vs.
[W][GainController]: Signal too strong (AGC=14 < 30) - skipping gain lock # Auto mode protection
Unstable Detection (Flickering)
Symptoms: Rapid flickering between IDLE and MOTION.
Solutions:
-
Increase threshold:
yaml espectre: segmentation_threshold: 2.0 -
Increase window size (smooths transitions):
yaml espectre: segmentation_window_size: 100 -
Enable low-pass filter (removes noise):
yaml espectre: lowpass_enabled: true
False Positives After WiFi Channel Change
Symptoms: Sudden MOTION detection when no one is moving, typically after router auto-channel switch.
Automatic handling: ESPectre v2.3.0+ automatically detects channel changes and resets the detection buffer. Look for this log message:
[W][CSIManager]: WiFi channel changed: 6 -> 11, resetting detection buffer
If you see frequent channel changes:
- Fix router channel: Disable auto-channel and set a fixed channel in your router settings
- Avoid DFS channels: Channels 52-144 (5GHz DFS) may switch unexpectedly due to radar detection
- Check for interference: Nearby networks on the same channel can cause instability
Runtime Recalibration (MVS only)
When needed: Recalibrate without reflashing (e.g., after moving furniture or changing room layout). This applies only to MVS mode; ML uses fixed subcarriers embedded in the model.
How to recalibrate from Home Assistant:
- Go to your ESPectre device in Home Assistant
- Find the Calibrate switch (
switch.espectre_calibrate) - Turn it ON to start calibration
- The switch will automatically turn OFF when calibration completes
Important: - Keep the room quiet and empty during calibration (~10 seconds) - The switch is disabled during calibration to prevent interruption - You cannot cancel calibration once started
Logs during recalibration:
[I][espectre]: Manual recalibration triggered
[I][espectre]: Starting band calibration...
[I][espectre]: Calibration completed successfully
Reset Calibration (MVS only)
When needed: Start completely fresh with new subcarrier selection and clear all saved settings. This applies only to MVS mode.
How to reset:
-
Erase flash completely:
bash esphome run <your-config>.yaml --device /dev/ttyUSB0 # Choose "Erase flash before uploading" if available -
Or use ESPHome dashboard: Clean Build Files then re-install
After reset: - Keep room quiet and empty for 10 seconds - NBVI band selection will automatically recalibrate - Check logs for "Calibration successful"
Monitoring
View Real-Time Logs
# Via USB
esphome logs <your-config>.yaml
# Via network (after first flash)
esphome logs <your-config>.yaml --device espectre.local
Home Assistant
After integration, monitor sensors in Home Assistant: - binary_sensor.espectre_motion_detected - Motion state - sensor.espectre_movement_score - Movement intensity - number.espectre_threshold - Adjustable detection threshold
Use History graphs to visualize detection patterns over time.
Tip: You can adjust the threshold directly from Home Assistant without re-flashing. Changes are session-only - the adaptive threshold is recalculated on every boot.
Quick Tips
- Start simple: Tune only the segmentation threshold first
- One change at a time: Adjust one parameter, re-flash, test for 5-10 minutes
- Document your settings: Note what works for your environment
- Seasonal adjustments: Retune when furniture changes or new interference sources appear
- Distance matters: Keep sensor 3-8m from router (RSSI between -40 and -70 dB for best results)
- Check AGC value: After boot, look for "Gain locked: AGC=XX" - values 30-60 are optimal
- Quiet calibration: Ensure no movement during first 10 seconds after boot
- Try the game: Use ESPectre - The Game for interactive threshold tuning with real-time visual feedback
Additional Resources
License
GPLv3 - See LICENSE for details.