An AtomVM Erlang/Elixir library + ESP-IDF component implementing ESP-NOW peer-to-peer wireless communication.
- Port-based driver with OTP-style message passing
- Singleton driver model: only one active port at a time
- Async RX/TX notifications delivered to owner process
- Peer management: add, modify, delete, check existence
%% Open/close
{ok, Port} = espnow:open([{channel, 1}]).
ok = espnow:close(Port).
%% Send data (broadcast or unicast)
ok = espnow:send(Port, broadcast, <<"hello">>).
ok = espnow:send(Port, <<Mac:6/binary>>, <<"hello">>).
%% Peer management
ok = espnow:add_peer(Port, Mac, Channel).
ok = espnow:mod_peer(Port, Mac, NewChannel).
ok = espnow:del_peer(Port, Mac).
true | false = espnow:peer_exists(Port, Mac).
%% Channel info
Channel = espnow:get_channel(Port).The owner process receives async messages:
{espnow, rx, FromMacBin, DataBin} %% Received data
{espnow, tx, broadcast | MacBin, StatusInt} %% TX confirmation| Option | Default | Description |
|---|---|---|
{channel, 0..14} |
0 |
WiFi channel (0 = don't change) |
{owner, pid()} |
self() |
Process to receive RX/TX messages |
ESP-NOW and WiFi networking can work simultaneously - they share the same radio hardware.
| Scenario | Channel Behavior |
|---|---|
| ESP-NOW only | Can freely set channel via {channel, N} option |
| Connected to AP (STA) | Channel locked to AP's channel |
| Soft-AP mode | Channel from AP config |
| STA + AP mode | Station channel takes priority |
- Use channel 0 for peers when connected to an AP - this auto-uses the current channel
- Query current channel with
espnow:get_channel/1to see what channel you're on - Set explicit channel only when running ESP-NOW standalone (not connected to WiFi)
This driver automatically detects if WiFi has already been initialized by AtomVM's network module. If WiFi is running, it will reuse the existing configuration rather than reinitializing.
- ESP-IDF
>= 5.0 - AtomVM with ESP-IDF support
start() ->
{ok, Port} = espnow:open([{channel, 1}]),
ok = espnow:send(Port, broadcast, <<"discovery">>),
loop(Port).
loop(Port) ->
receive
{espnow, rx, FromMac, Data} ->
io:format("Received ~p from ~p~n", [Data, FromMac]),
%% Add peer for unicast reply
_ = espnow:add_peer(Port, FromMac, 0),
ok = espnow:send(Port, FromMac, <<"ack">>),
loop(Port);
{espnow, tx, _To, _Status} ->
loop(Port)
end.def start do
{:ok, port} = :espnow.open([{:channel, 1}])
:ok = :espnow.send(port, :broadcast, "discovery")
loop(port)
end
defp loop(port) do
receive do
{:espnow, :rx, from_mac, data} ->
IO.puts("Received #{inspect(data)} from #{inspect(from_mac)}")
_ = :espnow.add_peer(port, from_mac, 0)
:ok = :espnow.send(port, from_mac, "ack")
loop(port)
{:espnow, :tx, _to, _status} ->
loop(port)
end
endFor instructions on building AtomVM with this component, see the AtomVM Build Instructions.
Add this component to your AtomVM ESP32 build's components/ directory.
Apache License 2.0