This was the first real slice of my final project. I got two XIAO ESP32-C3 boards talking to each other over ESP-NOW, and the receiver decides — based on how strong the radio signal coming in is — whether to switch a DC motor on or off through a MOSFET. That's the whole core idea of my final-project swarm: each robot listens for its neighbor's broadcasts, reads the RSSI, and chases when the signal is weak (neighbor is far) or stops when the signal is strong (neighbor is close). This week I just proved the loop works on a bench setup before scaling it to three robots.
The video below is the very first milestone of that work — two XIAOs on USB, sender pushing single-character commands ("F" for forward, "S" for stop) over ESP-NOW, receiver printing each one back out over serial as it arrives. Once I saw "received: F, S, F, S, ..." scrolling on the receiver's serial monitor I knew the radio loop was solid, and I built the RSSI + MOSFET logic shown further down on top of that same wiring.
The setup
Two boards on the bench:
Sender — XIAO ESP32-C3, MAC 58:8C:81:9D:2A:D4. Just broadcasts a tiny ESP-NOW packet (one uint32_t counter) every 200 ms at 5 Hz. No sensors, no logic — it's a heartbeat.
Receiver — XIAO ESP32-C3, MAC 58:8C:81:9F:BA:A8. Receives the packets, pulls the RSSI off each one, smooths it, and uses the smoothed value to switch a DC motor on or off through a P-channel MOSFET.
The MAC addresses are hard-coded into the sender (the receiver's MAC is the destination), which is fine for two boards but doesn't scale — the final-project robots all need to know each other's MACs, and that ends up being a constants block at the top of the sketch.
Why ESP-NOW (not WiFi)
ESP-NOW is a peer-to-peer protocol built into the ESP32 radio. No router, no SSID, no IP stack — both boards come up in WIFI_STA mode, register each other as peers by MAC address, and start exchanging frames directly. Latency is tiny (sub-ms) and the receive callback gives you the per-packet RSSI for free, which is what I actually care about. For a swarm of robots that have to find each other anywhere there's no router, this is exactly the right tool.
The MOSFET switching trick
The motor is switched by a P-channel MOSFET on the high side, with an external pull-up resistor between gate and VCC. That gives you a really clean state machine controllable from one XIAO pin:
Pin driven LOW → gate pulled low → P-MOSFET conducts → motor on.
Pin configured as INPUT (high impedance) → pull-up holds the gate at VCC → P-MOSFET off → motor off.
Crucially, this is boot-safe. The XIAO's GPIO defaults to INPUT at reset, so the pull-up keeps the MOSFET off all the way through power-on before setup() runs. No twitching motor at boot.
RSSI smoothing + hysteresis
Raw RSSI off a single packet is noisy — it can swing 10+ dB packet-to-packet from multipath, antenna orientation, or someone walking past. So the receiver keeps a 10-sample rolling window and acts on the average.
The decision is a hysteresis pair, not a single threshold:
Motor turns ON when avg RSSI drops below −45 dBm (neighbor is far — go chase).
Motor turns OFF when avg RSSI rises above −35 dBm (neighbor is close — caught up).
The 10 dB gap between the two thresholds is the deadband. Without it, RSSI hovering right at the threshold would chatter the motor on/off many times a second. Calibration is loose — at indoor short range, ~−28 dBm reads when the boards are basically touching and ~−50 dBm at about 2.5 m. These same constants carry over directly into the final-project follower.
There's also a 1-second watchdog: if no packet arrives for that long (sender dies, board reboots, bad antenna day) the receiver forces the motor off rather than running forever on stale data.