ESP32: add support for channel state information (CSI)
Channel state information basically gives the complex frequency response of the current channel (at the OFDM subcarriers) and was added to a recent ESP-IDF. It's very useful to researchers for doing things like localization, detecting motion, human activities, etc.
It's fairly easy to use in ESP-IDF: set a callback, configure and then enable. After every WiFi packet, the callback is passed some metadata (MAC address, RSSI, etc.) along with 64 complex numbers.
I imagine an API something like this:
WLAN.csi(callback=None, *, lltf_en=True, htltf_en=True, stbc_htltf2_en=True, ltf_merge_en=True, channel_filter_en=True, manu_scale=False, shift=0)
which would configure and enable CSI, and call callback(metadata, csi_list) where metadata = namedtuple('CSI data', 'mac_addr, rssi, rate, mcs, timestamp, ...') and csi_list is a list of complex numbers (or real, imag pairs).
The case of callback=None could disable CSI (or there could be explicit start() / stop() methods).
esp32: Add Wi-Fi CSI (Channel State Information) module
esp32: Add Wi-Fi CSI (Channel State Information) module
Summary
This PR adds Wi-Fi CSI (Channel State Information) support to the ESP32 port, providing low-level access to Channel State Information data. This enables applications like motion detection, indoor localization, and gesture recognition.
CSI provides detailed information about the Wi-Fi channel state by analyzing physical layer signals. This is exposed as methods on the network.WLAN object: csi_enable(), csi_disable(), csi_read(), csi_available(), and csi_dropped().
The implementation includes:
- Lock-free circular buffer for efficient frame management in ISR context
- Support for all ESP32 variants (ESP32, S2, S3, C3, C5, C6)
- ESP32-C6 Wi-Fi 6 (802.11ax) support with the new ESP-IDF 5.x CSI API
- Complete Python API:
csi_enable(),csi_disable(),csi_read(),csi_available(),csi_dropped()
Example Usage
import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("SSID", "password")
# Note: CSI capture requires an active Wi-Fi connection
# Configure and enable CSI capture (optional - default buffer is 16 frames)
# buffer_size: Number of CSI frames to store in circular buffer
# Each frame is ~552 bytes (metadata + up to 512 bytes of CSI data)
# Larger buffer = less frame drops, but more RAM usage
wlan.csi_enable(buffer_size=64) # Store up to 64 frames (~35KB RAM)
# Read CSI frames
frame = wlan.csi_read()
if frame:
print(f"RSSI: {frame['rssi']} dBm")
print(f"Channel: {frame['channel']}")
print(f"CSI samples: {len(frame['data'])}") # CSI raw data
print(f"MAC: {frame['mac'].hex()}")
# Monitor buffer statistics
print(f"Available frames: {wlan.csi_available()}") # Frames ready to read
print(f"Dropped frames: {wlan.csi_dropped()}") # Frames lost due to buffer overflow
# Disable when done
wlan.csi_disable()
Understanding the Circular Buffer
The CSI module uses a circular buffer to handle the asynchronous nature of Wi-Fi packet reception:
- Producer: Wi-Fi hardware captures CSI frames and stores them in the buffer (ISR context)
- Consumer: Python code reads frames from the buffer at its own pace
- buffer_size: Maximum number of complete CSI frames that can be queued (default: 16)
Buffer sizing guidelines:
- Small buffer (16-32): Lower RAM usage (~9-18KB), risk of frame drops if Python is slow
- Default buffer (16): Balanced approach (~9KB RAM), handles moderate traffic bursts
- Large buffer (64-128): Minimal frame drops (~35-70KB RAM), suitable for high-traffic scenarios
Memory calculation:
RAM usage ≈ buffer_size × 552 bytes per frame
Default: 16 frames × 552 bytes = ~9KB
Example: 64 frames × 552 bytes = ~35KB
Note: Each frame's data field can hold up to 512 bytes of CSI samples. The actual CSI data length varies based on Wi-Fi mode and configuration (typically 52-128 bytes for HT20).
Testing
Tested on:
- ESP32-S3: All CSI functionality working correctly
- ESP32-C6: All CSI functionality working correctly with Wi-Fi 6 support
Test results:
- Successfully captured CSI frames with RSSI=-48 dBm, Channel=4
- Circular buffer working correctly: 63 frames captured, 28 dropped during overflow test
- All API methods verified:
csi_enable(),csi_disable(),csi_read(),csi_available(),csi_dropped() - Wi-Fi 6 (802.11ax) CSI capture confirmed on ESP32-C6 using the new ESP-IDF 5.x API with
acquire_csi_*configuration fields - Buffer overflow handling:
csi_dropped()counter correctly tracks lost frames when buffer is full
Could not test on ESP32, ESP32-S2, ESP32-C3, ESP32-C5 due to hardware availability, but code includes conditional compilation for all variants based on ESP-IDF documentation and API compatibility.
Firmware compiles successfully for all ESP32_GENERIC boards (ESP32, S2, S3, C3, C5, C6).
API Design
The CSI API is implemented as direct methods on the WLAN object (not a singleton sub-object):
wlan.csi_enable(buffer_size=16, ...)- Configure and enable CSI capturewlan.csi_disable()- Disable CSI capture and clean up resourceswlan.csi_read()- Read a CSI frame (returns dict or None)wlan.csi_available()- Get number of frames ready to readwlan.csi_dropped()- Get count of dropped frames
This design choice provides:
- Consistency: CSI is Wi-Fi related, belongs with WLAN
- Context: Requires active Wi-Fi connection
- Discoverability: Users expect Wi-Fi features on WLAN object
- Simplicity: No need for separate singleton object
Trade-offs and Alternatives
Code Size Impact:
- Adds approximately 15KB to firmware when CSI is enabled
- Total application size: ~1.87MB (fits in 2MB partition with 8% free space)
- Conditional compilation via
MICROPY_PY_NETWORK_WLAN_CSIallows disabling if needed
Trade-off Justification:
The 15KB overhead is justified because:
- CSI enables entirely new application categories (sensing, localization, gesture recognition)
- The feature is optional and can be disabled via build configuration
- The implementation is efficient with lock-free ISR-safe design
- Similar features (Bluetooth, network protocols) have similar code size impact
Memory Usage:
- Default: 16 frames × 552 bytes = ~9KB RAM
- Configurable: 1-1024 frames (user controls trade-off)
- Can be reduced to minimum (1 frame = ~552 bytes) if needed
Files Changed
ports/esp32/modwifi_csi.c- Core implementation (705 lines)ports/esp32/modwifi_csi.h- Headers and definitions (120 lines)ports/esp32/network_wlan.c- CSI methods integrated into WLANports/esp32/esp32_common.cmake- Build system integrationports/esp32/mpconfigport.h- Feature flag (MICROPY_PY_NETWORK_WLAN_CSI)ports/esp32/boards/sdkconfig.base- ESP-IDF config (CONFIG_ESP_WIFI_CSI_ENABLED=y)docs/library/network.WLAN.rst- Complete API documentation (284 lines)examples/csi/csi_basic.py- Basic CSI capture exampleexamples/csi/csi_turbolence_monitor.py- Advanced turbulence calculation exampleexamples/csi/README.md- Comprehensive guide with troubleshooting
Total: 10 files changed (+1632 lines, -1 line)
Documentation
Comprehensive documentation has been added:
- API Reference: Complete method documentation in
docs/library/network.WLAN.rst - Examples: Two working examples with different use cases
- Guide: Detailed README with troubleshooting, memory usage, and platform-specific notes
- Platform Support: Clear documentation of differences between ESP32 variants
Platform-Specific Notes
ESP32-C6 / ESP32-C5 (Wi-Fi 6)
- Supports 802.11ax (Wi-Fi 6) CSI capture
- Uses new ESP-IDF 5.x CSI API
- Enhanced CSI capabilities with HE-LTF support
- Only
buffer_sizeparameter is configurable via Python API - Other parameters (
lltf_en,htltf_en, etc.) are accepted but ignored (handled internally)
ESP32 / ESP32-S2 / ESP32-S3 / ESP32-C3
- Supports 802.11b/g/n CSI capture
- Uses legacy CSI API
- All Python configuration parameters are supported
- Full
rx_ctrlstructure available in frames
Known Limitations
-
Wi-Fi Traffic Required: CSI data is extracted from received Wi-Fi packets. Without active traffic directed to the ESP32, no CSI frames will be captured. This is well documented with examples showing how to generate test traffic.
-
Buffer Overflow: If Python code reads frames slower than they arrive, frames will be dropped. The
csi_dropped()counter allows monitoring this, and buffer size is configurable. -
Platform Differences: ESP32-C5 and ESP32-C6 use a different ESP-IDF API internally, so some Python configuration parameters are not applicable. This is clearly documented.
-
Memory Usage: Larger buffers use significant RAM. The default (16 frames) is conservative, but users can adjust based on their needs.
Future Enhancements (Out of Scope)
- CSI data filtering/processing helpers
- Higher-level motion detection algorithms
- Multi-antenna support (currently single antenna)
- HT40 support (currently HT20 only)