Documentation / Hardware

ESP-NOW data: values between boards

One board reads a sensor; another shows it. No router, no broker, no internet in between.

What it is

Boards send each other the values of variables over ESP-NOW, the ESP32's own radio-to-radio link - the same link the two-way radio uses for a voice. A variable on one board is shared; a variable on another takes its value from it, and every widget bound to that variable, and every automation watching it, follows. It works on every ESP32 with Wi-Fi: the original, the S2, S3, C3, C5 and C6. The sending board need not have a screen at all, or run anything made here: a sketch of a few dozen lines sends to it.

It is written for the Arduino and ESP-IDF exports, and is part of Pro: free to design and preview with, and generating code for it needs Pro.

Three ways to reach other boards

Set on the widget, under It sends to:

SettingWho hears itPrivateKnows when nobody answers
The board it is paired withOne other board, paired once with the Pair action.Yes, encryptedYes
Every board with the same group nameAny number of boards built with exactly the same group name. No pairing.NoKnows when a board goes out of range
Anybody listeningEvery board on the Wi-Fi channel. A board set this way also hears every board on the channel, whatever group they send to.NoKnows when a board goes out of range

Pairing is the radio's: run the action ESP-NOW data: pair or forget on both boards within half a minute. Each finds the other, and keeps its address and a key in flash, so it survives a restart. The two halves of the key cross the air while the boards pair, so the key keeps out anybody who was not listening at that moment, and nobody who was - what Bluetooth calls "just works". If a board's flash is erased it has forgotten its partner: pair the two again.

The widget is an icon

Drop ESP-NOW data from the palette's Ready-made group. It is a small mark - two arrows passing - in the colour of what the link is doing, and it holds the link's settings. The first one in a design is the link; any others show the same thing.

The link isThe iconColour to start with
Not linkedNo board to send to yet.Grey
PairingFlashes: looking for another board that is pairing too.Blue
LinkedPaired, or on its group or channel, and quiet.Blue
ReceivingValues have just arrived.Green
No answerThe paired board is not acknowledging what is sent - or, on a group or broadcast, the boards that were in range have gone quiet: off, or out of range.Amber

Its inspector note adds a Pair button, and a Send button where one is needed, in one step each.

Sending a variable

On the Data tab, tick Send to other boards on a variable. It is sent under its key: the variable's name, or a key of its own typed beside the tick box - up to 15 characters, no spaces. When the shared values go is set on the widget, under Shared values go:

The action sends every shared variable, or one, from anything that fires an event: a button, a timer automation, a variable changing, a pin. A number goes as a number, a whole number as one, a yes or no as one byte, and text as text - up to 220 characters, which is what a packet has room for beside its key.

Taking a value from another board

Set a variable's Source to ESP-NOW data > A value another board sends and type the Key the other board sends it under. When a packet carries that key, the variable takes the value; a packet that leaves it out leaves it as it was. Each side may say it its own way: a number arriving for a text variable is written out, and text arriving for a number is read as one.

The link also says things of itself, as sources under the same heading: what it is doing (a number and a sentence), whether there is a board to send to, the paired board's name, the seconds since values last arrived - for showing a reading has gone stale - the packets heard and sent, and its quality.

Link quality

As the radio's: the share of the last eight seconds' packets that got through, marked down as the other board's signal weakens - by nothing at -50 dBm, by three quarters at -90. On a pair, a short packet goes once a second while nothing else is sent, and whether it is acknowledged is what quality and "no answer" go by. On a group or broadcast nothing is acknowledged, so quality is what is heard: a board that only sends there reads 0.

Who is in range

ESP-NOW itself says nothing of who heard a broadcast. So while it has nothing else to send, every board sends a short keep-alive once a second with its name in it. On a group or broadcast each board notes every board it hears, and its sentence says "Hearing Desk B", "3 boards in range", or "Desk B is out of range" once nothing has come from it for three seconds - twelve for a board that sends no keep-alive, such as the sketch below sending every five seconds. The other board's name is a source too.

The Wi-Fi channel

ESP-NOW rides on Wi-Fi's radio, on whatever channel that is on. In a design with no Wi-Fi of its own, the link is on the channel the widget names, and every board must name the same one. In a design that joins a network, it is that network's channel, whatever the widget says, and two boards on two networks will not hear each other.

With the two-way radio

A design can have both. ESP-NOW takes one receive callback, so the radio's module takes every packet and hands the data link those that are not a voice. They are on one channel - the radio's - and when both pair, they share one partner: a board pairs once, and either Pair action pairs both. A design with the radio alone is built exactly as it was before ESP-NOW data existed.

The packet

For anybody writing the other end themselves. Little-endian, as the ESP32 is; 250 bytes at most.

ByteWhat
0 - 2'L' 'W' 'D'
3Version: 1
4Kind: 1 a pairing announcement, 2 values, 3 a keep-alive (the head, then the sender's name, up to 20 characters and a zero; the name may be left out)
5 - 8Group: 32 bits of FNV-1a of the group name; 0 on a pair and on broadcast
9 - 10The sender's own count of its values and keep-alives, for counting what was lost
11How many values follow
12 onEach value: its type (1 a float, 2 a 32-bit whole number, 3 a yes or no, 4 text), its key's length (1 to 15), the key, then the value - 4 bytes, 1 byte, or for text a length byte and the text

A receiver reads values until the count is reached or the packet ends, and stops at a type it does not know. Values that do not fit one packet go in the next.

Sending from a board with no screen

A plain ESP32 sketch, with nothing but the ESP32 core, that sends a sensor's readings every five seconds. Set the design's widget to Anybody listening on the same Wi-Fi channel - or both to one group name - and give the design's variables the keys below.

#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <string.h>

#define WIFI_CHANNEL 1          // the widget's Wi-Fi channel
#define GROUP_NAME   ""         // "" for anybody listening, or the widget's group name
#define EVERY_MS     5000

static const uint8_t EVERYBODY[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
static uint8_t  packet[250];
static int      len = 0;
static uint8_t  count = 0;
static uint16_t seq = 0;
static uint32_t group = 0;

static uint32_t group_of(const char * s)
{
    uint32_t h = 0x811C9DC5u;
    while (*s) { h ^= (uint8_t)*s++; h *= 0x01000193u; }
    return h;
}

static void send_values(void)
{
    if (!count) return;
    packet[0] = 'L'; packet[1] = 'W'; packet[2] = 'D';
    packet[3] = 1;                      // version
    packet[4] = 2;                      // values
    memcpy(packet + 5, &group, 4);
    memcpy(packet + 9, &seq, 2);
    packet[11] = count;
    esp_now_send(EVERYBODY, packet, len);
    seq++;
    len = 0;
    count = 0;
}

static void put(uint8_t type, const char * key, const void * value, int vlen)
{
    int klen = strlen(key);
    if (klen < 1 || klen > 15) return;
    if (count && len + 2 + klen + vlen > (int)sizeof(packet)) send_values();
    if (!count) len = 12;
    packet[len++] = type;
    packet[len++] = (uint8_t)klen;
    memcpy(packet + len, key, klen);   len += klen;
    memcpy(packet + len, value, vlen); len += vlen;
    count++;
}
static void put_float(const char * key, float v)  { put(1, key, &v, 4); }
static void put_int(const char * key, int32_t v)  { put(2, key, &v, 4); }

void setup()
{
    WiFi.mode(WIFI_STA);
    esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
    WiFi.setSleep(false);
    esp_now_init();
    esp_now_peer_info_t everybody = {};
    memcpy(everybody.peer_addr, EVERYBODY, 6);
    everybody.ifidx = WIFI_IF_STA;
    esp_now_add_peer(&everybody);
    if (GROUP_NAME[0]) group = group_of(GROUP_NAME);
}

void loop()
{
    // Your sensor's readings here: a SEN66, say.
    put_float("pm2.5", 5.2);
    put_float("pm10", 6.4);
    put_float("rh", 45.5);
    put_float("temp", 21.3);
    put_int("co2", 612);
    send_values();
    delay(EVERY_MS);
}

Nine values with short keys - a SEN66's particles, humidity, temperature, VOC, NOx and CO2 - are about 110 bytes: one packet. A sketch of its own sends to a group or to anybody listening; pairing is between two LCD Wizard designs.

What it does not do