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.
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.
After flashing your device with ESPHome:
# View logs to monitor calibration
esphome logs <your-config>.yamlOn first boot, keep the room empty and still for 10 seconds. The system will:
- Collect CSI packets during baseline (10 ×
window_size, default 1000 packets) - Run NBVI calibration algorithm to select optimal subcarriers
- Select 12 optimal subcarriers for motion detection
- Calculate adaptive threshold based on baseline noise
Look for log messages like:
[I][Calibration]: ✓ Calibration successful: [<12 auto-selected subcarriers>]
NOTE: In ML mode, calibration is skipped and logs show fixed defaults.
Walk around the room while monitoring logs:
esphome logs <your-config>.yamlLook for state changes:
state=MOTIONwhen movingstate=IDLEwhen still
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 valueRule of thumb:
- Too many false positives → use
autoor increase threshold (try 2.0-5.0) - Missing movements → use
minor decrease threshold (try 0.5-0.8)
After changing, re-flash:
esphome run <your-config>.yamlInteractive 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.
The following parameters apply to the MVS detection algorithm.
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.
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 detector | 0.0 - 10.0 (scaled metric) | Calibration-free boot |
What it does: Number of turbulence samples used to calculate moving variance.
Default: 100 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: 100 # defaultOptimal 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 (
Maintaining
Moving Window Size (
For general movement detection, a window is recommended that captures transient action while being long enough to dampen high-frequency noise.
|
|
Advantage for Presence Detection | |
|---|---|---|
|
|
|
Extremely reactive, but too sensitive to noise. |
|
|
|
Recommended. Optimal balance, captures |
|
|
|
Slower to react, but very robust against false positives. |
Recommendation: Start with
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: 100What it does: Controls how often ESPectre publishes the movement score and periodic logs.
The motion binary sensor is no longer tied to this cadence: it is published
immediately on IDLE <-> MOTION state changes.
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 rateNote: Lower
publish_intervalvalues increase Home Assistant traffic and dashboard refresh frequency, but the internal motion detection cadence is controlled separately byevaluation_interval.
What it does: Controls how often the detector state machine is evaluated
internally. This cadence feeds the binary sensor edge detection and the
motion_on_hits / motion_off_hits counters.
Default: 25
Configuration:
espectre:
evaluation_interval: 25
motion_on_hits: 3
motion_off_hits: 3How to think about it:
- Lower values react faster but evaluate more often
- Higher values are cheaper but add latency
3hits withevaluation_interval: 25means roughly75packets of consistent evidence before changing state
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):
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 traffic
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:
- During calibration, collects 10 ×
window_sizeCSI packets during baseline (default 1000, room must be quiet) - Band selection uses NBVI algorithm to select optimal subcarriers
- Threshold calculation depends on
segmentation_thresholdsetting
| 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 numberBenefits:
- Minimizes false positives with
automode - 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)
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: Enabled (threshold: 5.0 MAD, window: 7)
The Hampel filter removes outlier spikes in turbulence values. With threshold 5.0 it only replaces extreme outliers (>5 MAD from median), preserving motion sensitivity while eliminating false positives caused by transient interference.
Configuration:
espectre:
hampel_enabled: true # default
hampel_window: 7 # sliding window size (3-11)
hampel_threshold: 5.0 # MAD multiplier, higher = less aggressiveSee SETUP.md for parameter details.
When to disable:
- If you observe reduced sensitivity in very low-SNR environments
- When maximum detection sensitivity is needed and the environment has no transient interference
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:
- During the first 300 packets (~3 seconds), ESPectre collects AGC/FFT samples and calculates the median (more robust than mean against outliers)
- These values are then "locked" (forced) to eliminate hardware-induced variations
- In
automode, if AGC < 30 (signal too strong), gain lock is skipped and CV normalization is enabled instead - In
disabledmode, 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).
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.0See 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
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:
- AGC saturation: The automatic gain control cannot reduce amplification enough
- CSI distortion: Signal clipping leads to unreliable CSI data
- 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:
- Move the sensor 2-3 meters away from the AP
- 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
Symptoms: Detects motion when room is empty.
Solutions (try in order):
-
Increase threshold:
espectre: segmentation_threshold: 3.0 # Try 2.0-5.0
-
Increase window size (more stable):
espectre: segmentation_window_size: 100 # Try 100-150
-
Enable low-pass filter (removes RF noise):
espectre: lowpass_enabled: true lowpass_cutoff: 11.0
-
Enable Hampel filter (removes spikes from interference):
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
Symptoms: Doesn't detect when people move.
Solutions (try in order):
-
Decrease threshold:
espectre: segmentation_threshold: 0.5 # Try 0.5-0.8
-
Decrease window size (faster response):
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:
espectre: traffic_generator_rate: 100 # Must be > 0
Symptoms: Logs show no CSI data or "CSI disabled".
Solutions:
-
Verify WiFi connection: Check logs for successful connection to AP
-
Check traffic generator:
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.
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):
espectre: gain_lock: disabled
This allows operation at any distance but with slightly less stable CSI.
-
Use
automode (default, v2.4.0+):espectre: gain_lock: auto # Default - skips gain lock if AGC < 30
In
automode, 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
Symptoms: Rapid flickering between IDLE and MOTION.
Solutions:
-
Increase threshold:
espectre: segmentation_threshold: 2.0
-
Increase window size (smooths transitions):
espectre: segmentation_window_size: 100
-
Enable low-pass filter (removes noise):
espectre: lowpass_enabled: true
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
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 (~13 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
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:
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"
# Via USB
esphome logs <your-config>.yaml
# Via network (after first flash)
esphome logs <your-config>.yaml --device espectre.localAfter 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.
- 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 ~13 seconds after boot
- Try the game: Use ESPectre - The Game for interactive threshold tuning with real-time visual feedback
GPLv3 - See LICENSE for details.