Learn/esp32wificsiesp-nowblelorammwaverust

ESP32 Radio in Rust: Wi-Fi CSI, ESP-NOW, BLE, LoRa and mmWave

Field note on rusty_esp_signal: Wi-Fi CSI presence, authenticated ESP-NOW, Wi-Fi provisioning over Bluetooth, LoRa P2P and mmWave radar, with honest status for each.

Signed by M·
Brick-built garden with small antenna towers exchanging glowing orbs across a hedge at dusk
What does rusty_esp_signal do for ESP32 radios?

It is the radio application layer for Janus ESP32 devices: presence from Wi-Fi channel state, an authenticated ESP-NOW link, Wi-Fi station policy, Bluetooth provisioning, LoRa point-to-point framing and an LD2410 mmWave parser. Bluetooth provisioning has run on a real ESP32-CAM; the other radios are host-verified and their firmware builds.

rusty_esp_signal is the ESP32 radio layer of the Janus family in memory-safe Rust: presence sensing from Wi-Fi channel state (CSI), an authenticated ESP-NOW link for chips with no access point, Wi-Fi station policy and provisioning, Bluetooth GATT provisioning, LoRa point-to-point framing, and a parser for LD2410 mmWave radar. One radio has run end to end on silicon: a browser provisioned an ESP32-CAM over Bluetooth and it joined Wi-Fi in 7.5 seconds. The rest are verified on the host against outside references, with firmware that builds and has not yet been flashed.

The radios live in one package as feature-gated modules, not separate repos. This field note covers each one under the Sense and Connect pillars, with the honest status beside it.

Sensing with ESP32 radio signals

An ESP32 radio already measures how each Wi-Fi subcarrier of the channel changes as things move. Presence detection reads that, with no camera and no microphone.

Wi-Fi CSI presence radar

The detector turns each CSI frame into per-subcarrier amplitudes in fixed point and watches their "wander", a coefficient of variation over a one-second window, with hysteresis. It was judged against a public labelled dataset from the Universidad de Cuenca, two ESP32-C6 boards at 50 Hz:

  • fixture: label: empty room; frames judged present: 17.4%
  • fixture: label: person walking; frames judged present: 86.1%
  • held out: label: empty room; frames judged present: 0%
  • held out: label: person walking; frames judged present: 60.8%

The held-out pair was never used to choose a threshold. The walking figures are a lower bound, since pauses are correctly read as absent under a per-file label. Later work added phase, a breathing estimator and a suspected-fall detector, all host-tested on captures. Heart rate is described in the ledger as "a band to try, not a number to trust". No accuracy on a room of our own is claimed yet; that needs a recording. The home-facing side of presence is covered in rusty_esp_sense.

mmWave radar over UART

For rooms where Wi-Fi CSI sensing is not enough, or where you want distance to a target rather than a yes or no, the HiLink LD2410 mmWave module reports moving and still targets over a serial line. The parser and command builder are pure Rust and follow the wire: where the vendor datasheet's field offsets disagree with the vendor's own tool, the tests say where. It is verified against the protocol document's example frames, not yet against a live module. The planned check is ten minutes of reports matched against the module's own serial tool. Pairing an mmWave module with Wi-Fi CSI on the same ESP32 radio board is the kind of layered presence sensor a hallway or bathroom might want, without a lens in the room.

Connecting without a vendor cloud

An ESP32 radio is only useful at home if the device can get onto your network and talk to its neighbours without someone else's server.

Wi-Fi lifecycle and Bluetooth provisioning

StationPolicy handles joining, reconnect back-off and falling back to provisioning when a network disappears. A failed join must not spend the device: it has to stay askable.

Provisioning is where this package first met silicon. A device with no credentials advertises a GATT service, and a Chrome page using Web Bluetooth, served locally by espino, writes one TLV with the network and passphrase. On an AI-Thinker ESP32-CAM the device joined 7.5 seconds after the write, and the next boot joined alone in 9.5 seconds with Bluetooth off. The passphrase is never readable back, never printed in debug output, and never touches a server. The 31-byte advertisement limit forced the name into the scan response, a fix that came out of that run. The 24-hour reconnect soak is still open.

ESP-NOW and LoRa point-to-point

For chips with no access point, ESP-NOW sends connectionless 2.4 GHz frames. The Janus envelope signs the session and MACs every frame: a Noise-KK-shaped handshake over P-256, HKDF-SHA256 keys, and a 16-byte HMAC-SHA256 tag with a 64-frame replay window. Hello, accept and confirm are 84, 100 and 18 bytes, leaving 227 bytes of payload in a 250-byte datagram. Replayed, tampered, reflected and stranger frames are all refused on the host, matching golden vectors from an independent Python implementation.

LoRa uses an SX1262 through lora-phy, with airtime matching Semtech's published calculator values. Both links have firmware that builds; neither has run between two boards. The self-driving c6-s1-link test is ready for when two C6 boards are on the bench.

Putting ESP32 radio to work at home

Radios are where a smart home usually hands its data to someone else. Here the device owns its keys and its links.

Firmware that builds on stable Rust

Track B firmware for the RISC-V ESP32-C6 builds on stable Rust with no extra toolchain installer: c6-mesh-node (ESP-NOW, CSI, LD2410, station), c6-lora-p2p and c6-ble-provision. The two-board ESP-NOW test builds one source into two roles:

cargo build --release --no-default-features --features role-responder
cargo build --release --no-default-features --features role-initiator

There is also an ESP-IDF build of Bluetooth provisioning for the XIAO ESP32-S3 Sense, so a Wi-Fi camera can be set up from a phone without a second firmware track; it builds and has not yet run on that board. A CSI backend for ESP-IDF lets a camera also watch the room with its ESP32 radio, and its first on-board capture is still to come.

Honest status by radio

Here is where each ESP32 radio stands today. On silicon: Bluetooth provisioning. Host-verified with firmware building: CSI presence, ESP-NOW, LoRa, LD2410, station policy. Out of v1: Thread and Zigbee, which belong to the home computer's border router plan. The Wi-Fi and Bluetooth controllers themselves stay Espressif's; this package remakes the application layer above them, not the radio blobs. Each ESP32 radio session ties to the device identity in rusty_esp_mid, and mesh traffic rides rusty_esp_iroh. For the bigger picture, read a smart home without the cloud. The source is at github.com/Remade-With-Rust/rusty_esp_signal, and Espressif documents the underlying radios in the ESP-IDF programming guide and the esp-rs book.

FAQ

Quick answers for builders evaluating this technology.

Can Wi-Fi really detect presence in a room?

Yes, from channel state information. On a public labelled ESP32-C6 dataset the detector read an empty room as present 17.4% of the time and a walking person 86.1%. A held-out empty room read 0% and a held-out walk 60.8%. Our own room recording has not been made yet.

How does provisioning work without an app?

The device advertises over Bluetooth and a Chrome page using Web Bluetooth writes the Wi-Fi credentials. An ESP32-CAM joined 7.5 seconds after the write, and the next boot joined alone in 9.5 seconds with Bluetooth off. The passphrase never touches a server or a log.

Is the ESP-NOW link encrypted?

Each session is authenticated with a three-message handshake over P-256 with HKDF-SHA256, and every frame carries a 16-byte HMAC-SHA256 tag with a replay window. That leaves 227 bytes of payload in a 250-byte ESP-NOW datagram. The two-board test has not run yet.

Has LoRa run on real radios?

Not yet. The LoRa firmware for an ESP32-C6 with an SX1262 builds, and time-on-air matches Semtech's published calculator values. The range, RSSI and packet-error table needs two nodes and has not been taken.

Which mmWave sensor is supported?

The HiLink LD2410 family over UART. The parser is verified against the protocol document's example frames, and where the vendor datasheet's field offsets disagree with the vendor's own tool, the code follows the wire. It has not yet been checked against a live module.