Articles › Microcontrollers and code
A toy remote with ESP-NOW: boards talking directly, no router needed
Send commands from one ESP32 to another with ESP-NOW, no home Wi-Fi. 250-byte messages, MicroPython sender and receiver, a signal-loss failsafe and long-range mode.
- ESP-NOW sends messages directly between ESP boards by MAC address, with no router. You can register up to 20 peers.
- Messages can be 250 bytes (ESP-NOW v1). Keep yours within that and it works with every board, ESP8266 included.
- Both boards must be on the same Wi-Fi channel, and the receiver must stop the motors itself when messages stop arriving.
- Long-range mode (RATE_LORA_250K/500K) needs PROTOCOL_LR on both sides and trades speed for distance.
ESP-NOW is an Espressif protocol that lets ESP32 boards send messages straight to each other over the Wi-Fi radio, with no router, no Bluetooth-style pairing, and almost no wait after power-on. It suits remote-control cars, robots and game buzzers you take outdoors. This lesson builds a four-button remote in MicroPython, with a failsafe that stops the car when the signal is lost.
If you have not wired buttons before, read stable buttons on the ESP32 first. For powering motors or servos see the servo lesson. Everything here runs at low voltage from USB or a small battery.
ESP-NOW compared with the alternatives
| ESP-NOW | Wi-Fi via router | Bluetooth LE | |
|---|---|---|---|
| Needs a router | no | yes | no |
| Ready to send | almost immediately | after joining the network | after pairing/connecting |
| Message size | 250 bytes (v1) | much larger | depends on settings |
| Talks to a phone | no | yes | yes |
| Best for | board to board | things that need the internet | things that talk to an app |
The main limit of ESP-NOW is that a phone cannot join in. The other end is always an ESP board.
Message size: 250 or 1470 bytes
The first version of ESP-NOW (v1) takes messages up to 250 bytes. From ESP-IDF 5.4, ESP32 boards also have v2, which allows up to 1470 bytes; check espnow.MAX_DATA_LEN. But v1 boards (including the ESP8266) truncate or drop anything over 250 bytes. A toy remote uses a few bytes, so this does not bite, but if you design your own protocol, staying within 250 bytes keeps every board compatible.
Worked example: how big is a remote message?
A four-button remote: forward, back, left, right.
- Pack the four buttons as bits in 1 byte (bit 0 = forward, bit 1 = back, bit 2 = left, bit 3 = right).
- Add a 1-byte sequence number cycling 0–255 so you can spot lost messages. That is 2 bytes per message.
- Send every 20 ms = 50 messages a second: 2 × 50 = 100 bytes per second.
- Forward and left held together: the first byte is 0b0101 = 5.
For a heavier job, Elektor’s walkie-talkie sends 8,000 audio samples a second in 250-byte messages: 8,000 ÷ 250 = 32 messages a second. Counting the full 255 bytes per packet, that is 32 × 255 × 8 = 65,280 bits per second. ESP-NOW’s default rate is 1 Mbit/s, and our remote uses far less.
Find the receiver’s MAC address
ESP-NOW addresses boards by MAC. Flash MicroPython onto the car’s board and run this in the REPL:
import network
sta = network.WLAN(network.WLAN.IF_STA)
sta.active(True)
print(sta.config('mac'))
You get something like b'\xaa\xbb\xcc\xdd\xee\xff'. Note it down for the remote’s code.
Sender code (the remote)
Pin numbers follow an ESP32-C3 pinout. On another board, change them to match its pinout and avoid strapping pins and pins wired to flash.
import network, espnow, time
from machine import Pin
sta = network.WLAN(network.WLAN.IF_STA)
sta.active(True)
sta.config(channel=6) # must match the receiver
e = espnow.ESPNow()
e.active(True)
CAR = b'\xaa\xbb\xcc\xdd\xee\xff' # the car board's MAC
e.add_peer(CAR)
pins = [Pin(n, Pin.IN, Pin.PULL_UP) for n in (4, 5, 6, 7)] # fwd back left right
seq = 0
while True:
buttons = 0
for i, p in enumerate(pins):
if p.value() == 0: # pull-up: pressed reads 0
buttons |= 1 << i
e.send(CAR, bytes([buttons, seq]), False)
seq = (seq + 1) & 0xFF
time.sleep_ms(20)
send(..., False) does not wait for a reply, so it can send continuously. With the default sync=True it waits for the receiver to acknowledge and returns False if no reply comes.
Receiver code (the car), with a failsafe
import network, espnow, time
from machine import Pin
sta = network.WLAN(network.WLAN.IF_STA)
sta.active(True)
sta.config(channel=6)
e = espnow.ESPNow()
e.active(True)
REMOTE = b'\x11\x22\x33\x44\x55\x66' # the remote's MAC
leds = [Pin(n, Pin.OUT) for n in (4, 5, 6, 7)] # stand-ins for motors
def drive(buttons):
for i, led in enumerate(leds):
led.value((buttons >> i) & 1)
last = time.ticks_ms()
while True:
host, msg = e.recv(100) # wait up to 100 ms
if msg and host == REMOTE and len(msg) == 2:
drive(msg[0])
last = time.ticks_ms()
elif time.ticks_diff(time.ticks_ms(), last) > 300:
drive(0) # nothing for 300 ms: stop
Try it with LEDs first; once it behaves, change drive() to control a motor driver. The host == REMOTE check stops a friend’s remote nearby from driving your car.
Anything that moves must stop by itself when the signal is lost. Never let the car keep running on the last command it heard.
Long-range mode
ESP32 chips (except the ESP32-C2) have an Espressif long-range mode that sends more slowly to reach further. Both sides need it:
sta.config(channel=6, protocol=network.WLAN.PROTOCOL_LR) # both sides
e.config(rate=espnow.RATE_LORA_250K) # sender only
The choices are RATE_LORA_250K and RATE_LORA_500K. The docs note that lower rates are generally more reliable over long distances. Two caveats: the rate setting does not work on the ESP32-C6, and this mode is not compatible with ordinary Wi-Fi devices, only with other ESP boards.
Common mistakes
- Mismatched channels. One board joined a router and moved to its channel, the other is still on channel 6, so nothing arrives. Set every board to the same channel.
- Forgetting
sta.active(True). A WLAN interface must be active before send/recv, even if it is not connected. - Forgetting
add_peer()before sending on an ESP32. - Messages dropping in bursts while on home Wi-Fi. Power saving is on. Turn it off with
sta.config(pm=sta.PM_NONE). - Confusion after a soft reset. MicroPython does not reset Wi-Fi on a soft reset, so deactivate STA and AP before setting them up at startup.
- No failsafe. The car drives out of range and keeps going.
- Motors on the same supply as the board. The voltage dips and the board resets. Power the motors separately and share ground.
Sources
FAQ
How far does ESP-NOW reach?
The MicroPython docs say it suits communication over hundreds of metres. Elektor's ESP-NOW walkie-talkie test reached about 150 m in the open. Real range depends on antennas, mounting height and obstacles.
Can I use ESP-NOW while connected to home Wi-Fi?
Yes, but the board moves to the router's channel, so every board must use that channel, and you should turn off power saving with sta.config(pm=sta.PM_NONE) or messages can be missed.
Do I need encryption?
Not for a typical toy. If you want it, ESP-NOW can encrypt with 16-byte keys (PMK and LMK) for up to 6 peers. Broadcast messages cannot be encrypted.
Circuits and code were tested on sample boards. Boards differ in pin layout, so check your board's pinout before you power anything.