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_interval values 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 traffic

Adaptive 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):

  1. Increase threshold: yaml espectre: segmentation_threshold: 3.0 # Try 2.0-5.0

  2. Increase window size (more stable): yaml espectre: segmentation_window_size: 100 # Try 100-150

  3. Enable low-pass filter (removes RF noise): yaml espectre: lowpass_enabled: true lowpass_cutoff: 11.0

  4. Enable Hampel filter (removes spikes from interference): yaml espectre: hampel_enabled: true

  5. Check for interference sources:

  6. Fans, AC units, moving curtains
  7. Microwave ovens, other WiFi networks
  8. Bluetooth devices, cordless phones
  9. Pets moving in the room

  10. Re-calibrate: Reset calibration (see below) in a quiet room

Missing Movements

Symptoms: Doesn't detect when people move.

Solutions (try in order):

  1. Decrease threshold: yaml espectre: segmentation_threshold: 0.5 # Try 0.5-0.8

  2. Decrease window size (faster response): yaml espectre: segmentation_window_size: 30 # Try 25-40

  3. Check sensor position:

  4. Optimal: 3-8m from router
  5. Avoid placing behind furniture or walls
  6. Line of sight to monitored area helps

  7. 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:

  1. Verify WiFi connection: Check logs for successful connection to AP

  2. Check traffic generator: yaml espectre: traffic_generator_rate: 100 # Must be > 0

  3. Verify ESP-IDF configuration: Ensure CONFIG_ESP_WIFI_CSI_ENABLED: y in sdkconfig

  4. Check router compatibility: Some mesh routers or WiFi 6E may have issues

  5. 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:

  1. Move the sensor further from the AP (recommended):
  2. Place at least 2-3 meters away
  3. Optimal distance: 3-8 meters
  4. Check logs for AGC value > 30 after gain lock

  5. Disable gain lock (workaround): yaml espectre: gain_lock: disabled This allows operation at any distance but with slightly less stable CSI.

  6. Use auto mode (default, v2.4.0+): yaml espectre: gain_lock: auto # Default - skips gain lock if AGC < 30 In auto mode, 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:

  1. Increase threshold: yaml espectre: segmentation_threshold: 2.0

  2. Increase window size (smooths transitions): yaml espectre: segmentation_window_size: 100

  3. 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:

  1. Fix router channel: Disable auto-channel and set a fixed channel in your router settings
  2. Avoid DFS channels: Channels 52-144 (5GHz DFS) may switch unexpectedly due to radar detection
  3. 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:

  1. Go to your ESPectre device in Home Assistant
  2. Find the Calibrate switch (switch.espectre_calibrate)
  3. Turn it ON to start calibration
  4. 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:

  1. Erase flash completely: bash esphome run <your-config>.yaml --device /dev/ttyUSB0 # Choose "Erase flash before uploading" if available

  2. 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

  1. Start simple: Tune only the segmentation threshold first
  2. One change at a time: Adjust one parameter, re-flash, test for 5-10 minutes
  3. Document your settings: Note what works for your environment
  4. Seasonal adjustments: Retune when furniture changes or new interference sources appear
  5. Distance matters: Keep sensor 3-8m from router (RSSI between -40 and -70 dB for best results)
  6. Check AGC value: After boot, look for "Gain locked: AGC=XX" - values 30-60 are optimal
  7. Quiet calibration: Ensure no movement during first 10 seconds after boot
  8. Try the game: Use ESPectre - The Game for interactive threshold tuning with real-time visual feedback

Additional Resources


License

GPLv3 - See LICENSE for details.