Библиотека RF24
Обновлено
- В менеджере библиотек
- RF24 by TMRh20
- Версия
- 1.6.2 · всего 39 версий, первая — 1.0.0
- Платы
- любые — автор не ограничивал
Архивы — копии из официального индекса Arduino, контрольная сумма совпадает. Сверено 19.10.2026.
Радиомодули nRF24L01+: связь двух и более плат на 2,4 ГГц пакетами до 32 байт.
nRF24L01+ — дешёвый приёмопередатчик на 2,4 ГГц с дальностью от десятков метров до километра у версии с усилителем. Библиотека RF24 прячет регистры чипа за понятной моделью: у каждого получателя есть адрес-«труба», отправитель открывает трубу на запись и шлёт структуру, получатель слушает и читает.
Пакет — до 32 байт. Удобнее всего слать структуру целиком: sizeof(data) с обеих сторон должен совпадать.
Как скачать и установить
- Через менеджер — самый простой путь. В Arduino IDE: Инструменты → Управлять библиотеками (Ctrl+Shift+I). Введите
RF24и найдите в списке строку «RF24 by TMRh20» — одноимённых библиотек бывает несколько, отличаются они автором. Нажмите «Установить». - Конкретная версия — если в чужом проекте указана не последняя. В той же карточке менеджера рядом с кнопкой есть выпадающий список версий: выберите нужную и нажмите «Установить».
- Из архива — если нет интернета на рабочем компьютере или менеджер не открывается. Скачайте ZIP выше, затем Скетч → Подключить библиотеку → Добавить .ZIP библиотеку и укажите файл. Распаковывать не нужно.
Файл RF24.h отдельно не скачивают: он внутри библиотеки и появляется после установки. Если среда пишет RF24.h: No such file or directory — библиотека не установлена или установлена не та. Как устроен менеджер и куда складываются файлы — в общей статье про библиотеки.
Строка подключения:
#include <SPI.h>
#include <RF24.h>Создание объекта — обычно до setup():
RF24 radio(9, 10); // CE, CSN
const byte address[6] = "NODE1"; // адрес трубы, 5 символов
radio.begin(); // в setup()
radio.openWritingPipe(address); // передатчик
// или
radio.openReadingPipe(1, address); // приёмник
radio.startListening();Подключение к Arduino

- VCC
- UNO, Nano: 3,3 ВMega: 3,3 В
Питание. Только 3,3 В — от 5 В чип сгорает.
- GND
- UNO, Nano: GNDMega: GND
Земля.
- CE
- UNO, Nano: 9Mega: 9
Включает приём или передачу. Любой цифровой вывод — первый аргумент
RF24 radio(9, 10). - CSN
- UNO, Nano: 10Mega: 10
Выбор модуля на шине SPI. Любой цифровой — второй аргумент.
- SCK
- UNO, Nano: 13Mega: 52
Такт SPI — только этот вывод.
- MOSI
- UNO, Nano: 11Mega: 51
Данные к модулю — только этот вывод.
- MISO
- UNO, Nano: 12Mega: 50
Данные от модуля — только этот вывод.
- IRQ
- UNO, Nano: —Mega: —
Сигнал прерывания. В обычных скетчах не подключают.
Конденсатор обязателен
Электролит 10–100 мкФ между VCC и GND прямо на выводах модуля. Без него модуль сбрасывается в момент передачи: begin() проходит, а write() через раз отвечает false.
Сигналы 5 В — можно, питание 5 В — нельзя
Входы nRF24L01+ терпят 5-вольтовую логику, поэтому SPI от UNO подключают напрямую, без делителей. А вот VCC — только к 3,3 В.
Версия с антенной и усилителем
PA+LNA в передаче берёт до 115 мА — вывода 3,3 В платы не хватает. Её ставят на переходник с собственным стабилизатором: он питается от 5 В, а модуль получает свои 3,3 В.
Методы
Только то, что нужно в обычном скетче. Сигнатуры сверены с версией 1.6.2.
Запуск
bool begin()
Включить модуль и выставить настройки по умолчанию: канал 76, 1 Мбит/с, 15 повторов по 1,5 мс, пакет 32 байта. Вызывают в setup() раньше любых других методов.
Возвращает: true — модуль ответил по SPI. false — не отвечает: питание не 3,3 В, перепутаны CE и CSN или отходит провод.
if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }bool isChipConnected()
Проверить, что чип на месте, в любой момент работы — begin() смотрит только при запуске.
Возвращает: true — регистры чипа читаются.
if (!radio.isChipConnected()) Serial.println("Модуль отвалился");bool isPVariant()
Узнать, какой чип на модуле: nRF24L01+ или старый без плюса. От этого зависит, доступны ли 250 кбит/с и testRPD().
Возвращает: true — nRF24L01+ или совместимый клон.
Serial.println(radio.isPVariant() ? "nRF24L01+" : "без плюса");
Адреса и режим
void openWritingPipe(const uint8_t* address)
Задать адрес получателя — туда уйдут все следующие write().
- address
- пять байт адреса. Удобно записать строкой:
const byte address[6] = "NODE1"— шесть байт с нулём в конце, библиотека берёт первые пять. В старых скетчах адрес записан числом,0xF0F0F0F0E1LL: так ещё работает, но в версии 1.6 этот вариант помечен устаревшим.
radio.openWritingPipe(address);
void openReadingPipe(uint8_t number, const uint8_t* address)
Слушать адрес на одной из труб. Труб шесть, поэтому один приёмник принимает до шести передатчиков сразу.
- number
- номер трубы 0–5. Трубу 0 модуль занимает под подтверждения, пока передаёт, но библиотека сама возвращает ей адрес приёма при каждом
startListening()— так что открывать её на приём можно. - address
- у трубы 1 — полный адрес из пяти байт. У труб 2–5 свой только первый байт, остальные четыре берутся от трубы 1: "1NODE", "2NODE" и так далее.
radio.openReadingPipe(1, address);
void closeReadingPipe(uint8_t pipe)
Перестать слушать трубу. Можно звать и для трубы, которая не открывалась, — ошибки не будет.
- pipe
- номер трубы 0–5; другие числа молча игнорируются.
radio.closeReadingPipe(2);
void setAddressWidth(uint8_t a_width)
Длина адреса. Короче адрес — короче пакет в эфире, но выше шанс принять шум за свой пакет. Задаётся одинаково на обеих платах.
- a_width
- 3, 4 или 5 байт, по умолчанию 5.
radio.setAddressWidth(4);
void startListening()
Перейти в приём. Только после него available() начинает видеть пакеты.
radio.startListening();
void stopListening()
Выйти из приёма. Перед write() обязательно: модуль, который слушает, не передаёт.
radio.stopListening();
Передача и приём
bool write(const void* buf, uint8_t len)
Отправить пакет и дождаться подтверждения от приёмника. Пока идут повторы, скетч стоит — до 60–70 мс при настройках по умолчанию.
- buf
- что отправить:
&переменнаяили имя массива. - len
- сколько байт — обычно
sizeof(переменной), не больше 32.
Возвращает: true — приёмник подтвердил получение. false — подтверждения нет после всех повторов. Если подтверждения выключены setAutoAck(false), всегда true, даже когда никто не принял.
bool ok = radio.write(&p, sizeof(p));
bool available()
Проверить, есть ли принятый пакет. Опрашивают в loop(); в очереди модуля помещается три пакета, дальше новые теряются.
Возвращает: true — в очереди есть пакет.
if (radio.available()) radio.read(&p, sizeof(p));
bool available(uint8_t* pipe_num)
То же, плюс номер трубы, по которой пришёл пакет, — так приёмник узнаёт, от какого передатчика.
- pipe_num
- адрес переменной, куда записать номер трубы 0–5.
Возвращает: true — в очереди есть пакет, номер трубы записан.
uint8_t pipe; if (radio.available(&pipe)) Serial.println(pipe);
void read(void* buf, uint8_t len)
Забрать пакет из очереди в переменную. Ничего не возвращает — есть ли пакет, проверяют заранее через available().
- buf
- куда положить:
&переменнаятого же типа, что шлёт передатчик. - len
sizeof(переменной). Меньше пакета — пакет останется в очереди; больше — хвост заполнится повтором последнего байта.
radio.read(&p, sizeof(p));
bool rxFifoFull()
Проверить, не забита ли очередь приёма. Когда заняты все три места, новые пакеты модуль отбрасывает — значит, loop() читает медленнее, чем шлют.
Возвращает: true — все три места заняты.
if (radio.rxFifoFull()) Serial.println("Не успеваю читать");Ответ приёмника
void enableAckPayload()
Разрешить приёмнику класть данные в подтверждение. Так передатчик получает ответ, не переключая обе платы между приёмом и передачей. Вызывают на обеих платах в setup().
radio.enableDynamicPayloads(); radio.enableAckPayload();
bool writeAckPayload(uint8_t pipe, const void* buf, uint8_t len)
Зарядить ответ на приёмнике. Он уйдёт со следующим принятым пакетом, поэтому первый ответ заряжают заранее, в setup(), а после каждого приёма заряжают новый. Передатчик читает ответ обычными available() и read() сразу после write().
- pipe
- труба, по которой придёт пакет от передатчика, — та же, что в
openReadingPipe(). - buf
- что ответить:
&переменная. - len
sizeof(переменной), до 32 байт.
Возвращает: true — ответ встал в очередь. false — очередь занята (ждать отправки могут не больше трёх ответов) или не вызван enableAckPayload().
radio.writeAckPayload(1, &reply, sizeof(reply));
void enableDynamicPayloads()
Пакеты переменной длины на всех трубах: в эфир уходит столько байт, сколько передано в write(), а не всегда 32. Для ответов приёмника обязательно. Включают на обеих платах.
radio.enableDynamicPayloads();
uint8_t getDynamicPayloadSize()
Длина пришедшего пакета — когда включены пакеты переменной длины. Зовут после available() и до read().
Возвращает: число байт в пакете, 1–32.
uint8_t n = radio.getDynamicPayloadSize();
Настройка связи — одинаково на обеих платах
void setPALevel(uint8_t level, bool lnaEnable = 1)
Мощность передатчика. Для отладки на столе — минимальная: на максимальной модули в полуметре друг от друга глушат приём. Для дальности — максимальная, но тогда без конденсатора по питанию модуль сбрасывается.
- level
- RF24_PA_MIN (−18 дБм), RF24_PA_LOW (−12), RF24_PA_HIGH (−6), RF24_PA_MAX (0 дБм). Если не задать — остаётся та, с которой чип включился, обычно максимальная.
- lnaEnable
- усилитель приёма. Влияет только на клоны на чипе Si24R1, обычно не указывают.
radio.setPALevel(RF24_PA_LOW);
bool setDataRate(rf24_datarate_e speed)
Скорость в эфире. Чем медленнее, тем дальше: при той же мощности 250 кбит/с бьёт заметно дальше 2 Мбит/с.
- speed
- RF24_1MBPS — по умолчанию, RF24_2MBPS — быстрее и ближе, RF24_250KBPS — медленнее и дальше.
Возвращает: true — скорость принята. false — модуль её не умеет: 250 кбит/с есть только у nRF24L01 с плюсом.
radio.setDataRate(RF24_250KBPS);
void setChannel(uint8_t channel)
Частота: 2400 МГц плюс номер канала. Wi-Fi занимает нижнюю часть диапазона, поэтому помехи уходят на каналах от 100.
- channel
- 0–125, по умолчанию 76 (2476 МГц).
radio.setChannel(108);
void setRetries(uint8_t delay, uint8_t count)
Сколько раз повторять пакет без подтверждения и с какой паузой. Больше повторов — надёжнее, но write() дольше держит скетч.
- delay
- 0–15, пауза (delay + 1) × 250 мкс. По умолчанию 5 — 1,5 мс.
- count
- 0–15, по умолчанию 15. 0 — не повторять вовсе.
radio.setRetries(5, 15);
void setPayloadSize(uint8_t size)
Размер пакета. Короче пакет — быстрее доходит. Должен совпадать на передатчике и приёмнике, иначе пакеты не принимаются.
- size
- 1–32 байта, по умолчанию 32.
radio.setPayloadSize(sizeof(Packet));
void setAutoAck(bool enable)
Включить или выключить подтверждения на всех трубах. Без подтверждений нет повторов и ответов приёмника, а write() всегда отвечает true. Выключают для широковещания на много приёмников и в сканере эфира.
- enable
- true — подтверждать, по умолчанию; false — не подтверждать. Должно совпадать на обеих платах.
radio.setAutoAck(false);
void setAutoAck(uint8_t pipe, bool enable)
То же для одной трубы. Трубу 0 модуль использует при передаче, поэтому, если передатчику нужны подтверждения, на ней их не выключают.
- pipe
- номер трубы 0–5.
- enable
- true — подтверждать, false — нет.
radio.setAutoAck(2, false);
void setCRCLength(rf24_crclength_e length)
Длина контрольной суммы пакета. По ней модуль отбрасывает пакеты, испорченные помехой.
- length
- RF24_CRC_16 — по умолчанию, RF24_CRC_8 — пакет на байт короче, RF24_CRC_DISABLED — без проверки. Выключить не получится, пока включены подтверждения.
radio.setCRCLength(RF24_CRC_8);
Текущие настройки
uint8_t getChannel()
Прочитать канал из чипа. Удобно сверить две платы, когда связи нет.
Возвращает: 0–125.
Serial.println(radio.getChannel());
uint8_t getPALevel()
Прочитать мощность из чипа.
Возвращает: RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH или RF24_PA_MAX — числа 0–3.
if (radio.getPALevel() == RF24_PA_MAX) Serial.println("Максимум");rf24_datarate_e getDataRate()
Прочитать скорость из чипа.
Возвращает: RF24_1MBPS, RF24_2MBPS или RF24_250KBPS.
if (radio.getDataRate() != RF24_250KBPS) Serial.println("Скорость не та");uint8_t getPayloadSize()
Прочитать размер пакета, заданный setPayloadSize().
Возвращает: 1–32.
Serial.println(radio.getPayloadSize());
Питание от батарейки
void powerDown()
Усыпить модуль. В сне он берёт около 0,9 мкА вместо 13,5 мА в режиме приёма. Настройки при этом не теряются.
radio.powerDown();
void powerUp()
Разбудить модуль после powerDown(). Просыпается до 5 мс — библиотека выжидает это время сама.
radio.powerUp();
Эфир: помехи и сканер
bool testRPD()
Есть ли на канале сигнал сильнее −64 дБм — чей угодно, не только наш. Так строят сканер эфира: слушают канал 128 мкс и проверяют. Только у nRF24L01+.
Возвращает: true — на канале кто-то есть.
radio.setChannel(ch); radio.startListening(); delayMicroseconds(128); radio.stopListening(); bool busy = radio.testRPD();
bool testCarrier()
То же для старого чипа без плюса: была ли несущая за время последнего прослушивания.
Возвращает: true — несущая была.
bool busy = radio.testCarrier();
Быстрая передача и очереди
bool writeFast(const void* buf, uint8_t len)
Поставить пакет в очередь передачи и не ждать подтверждения — пока в очереди есть место, скетч не стоит. Для потока данных; после серии обязательно txStandBy().
- buf
- что отправить:
&переменная. - len
sizeof(переменной), до 32 байт.
Возвращает: true — пакет в очереди. false — предыдущий пакет так и не доставлен; зовите txStandBy().
for (int i = 0; i < 10; i++) radio.writeFast(&p, sizeof(p)); bool ok = radio.txStandBy();
bool txStandBy()
Дождаться, пока очередь передачи опустеет, и перевести модуль в покой. Вариант txStandBy(1000) повторяет недоставленное до тайм-аута в миллисекундах.
Возвращает: true — все пакеты доставлены. false — какой-то не подтверждён.
if (!radio.txStandBy()) Serial.println("Часть не дошла");uint8_t flush_tx()
Очистить очередь передачи — все три места. Нужно, если пакет застрял и мешает следующим.
Возвращает: регистр состояния чипа; обычно не нужен.
radio.flush_tx();
uint8_t flush_rx()
Очистить очередь приёма: выбросить непрочитанные пакеты, например накопившиеся за время настройки.
Возвращает: регистр состояния чипа; обычно не нужен.
radio.flush_rx();
Отладка
void printDetails()
Напечатать все регистры чипа. Печатает через printf, поэтому без #include <printf.h> и printf_begin() в setup() молчит. Нули или FF во всех строках — чип не отвечает.
printf_begin(); radio.printDetails();
void printPrettyDetails()
То же словами: канал, скорость, мощность, адреса труб. Удобнее для чтения, но занимает больше памяти. Условие то же — printf_begin().
radio.printPrettyDetails();
uint8_t getARC()
Сколько повторов понадобилось последнему write(). Грубая оценка связи: 0 — отлично, под 15 — на пределе.
Возвращает: 0–15.
radio.write(&p, sizeof(p)); Serial.println(radio.getARC());
Примеры
Связь двух плат
#include <SPI.h>
#include <RF24.h>
RF24 radio(9, 10);
const byte address[6] = "NODE1";
const bool TRANSMITTER = true; // на второй плате — false
// типы фиксированной ширины: у UNO и ESP32 int разной длины
struct Packet { uint32_t ms; int16_t value; };
void setup() {
Serial.begin(9600);
if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }
radio.setPALevel(RF24_PA_LOW);
radio.setDataRate(RF24_250KBPS);
if (TRANSMITTER) {
radio.openWritingPipe(address);
radio.stopListening();
} else {
radio.openReadingPipe(1, address);
radio.startListening();
}
}
void loop() {
if (TRANSMITTER) {
Packet p = { millis(), (int16_t)analogRead(A0) };
bool ok = radio.write(&p, sizeof(p));
Serial.println(ok ? "Доставлено" : "Нет ответа");
delay(500);
} else if (radio.available()) {
Packet p;
radio.read(&p, sizeof(p));
Serial.print(p.ms); Serial.print(": "); Serial.println(p.value);
}
}Один скетч на обе платы: меняется только константа TRANSMITTER. Так проще держать структуру пакета одинаковой.
Монитор порта:
Передатчик: Доставлено Доставлено Нет ответа ← приёмник выключен или далеко Приёмник — время отправки и показание A0: 10503: 512 11006: 511 11509: 514
Приём от нескольких передатчиков
#include <SPI.h>
#include <RF24.h>
RF24 radio(9, 10);
// у передатчиков различается только первый байт адреса
const byte addresses[][6] = { "1NODE", "2NODE", "3NODE" };
const uint8_t MY_ID = 0; // 0 — приёмник, 1, 2, 3 — передатчики
struct Packet { uint8_t id; int16_t value; };
void setup() {
Serial.begin(9600);
if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }
radio.setPALevel(RF24_PA_LOW);
if (MY_ID == 0) {
for (uint8_t i = 0; i < 3; i++) radio.openReadingPipe(i + 1, addresses[i]);
radio.startListening();
} else {
radio.openWritingPipe(addresses[MY_ID - 1]);
radio.stopListening();
}
}
void loop() {
if (MY_ID == 0) {
uint8_t pipe;
if (radio.available(&pipe)) {
Packet p;
radio.read(&p, sizeof(p));
Serial.print("Труба "); Serial.print(pipe);
Serial.print(", плата "); Serial.print(p.id);
Serial.print(": "); Serial.println(p.value);
}
} else {
Packet p = { MY_ID, (int16_t)analogRead(A0) };
radio.write(&p, sizeof(p));
delay(1000 + MY_ID * 70); // разный период — передатчики реже совпадают в эфире
}
}Один приёмник слушает три трубы, у каждого передатчика свой адрес. Номер трубы из available(&pipe) говорит, кто прислал пакет, даже если id в пакете не передавать. Труб шесть — столько же передатчиков можно подключить к одному приёмнику.
Монитор порта:
Труба 1, плата 1: 512 Труба 2, плата 2: 347 Труба 3, плата 3: 1023 Труба 1, плата 1: 515
Ответ приёмника без смены ролей
#include <SPI.h>
#include <RF24.h>
RF24 radio(9, 10);
const byte address[6] = "NODE1";
const bool TRANSMITTER = true; // на второй плате — false
int16_t counter = 0; // передатчик: что шлём
int16_t reply = 0; // приёмник: чем отвечаем
void setup() {
Serial.begin(9600);
if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }
radio.setPALevel(RF24_PA_LOW);
radio.enableDynamicPayloads(); // ответы бывают только переменной длины
radio.enableAckPayload();
if (TRANSMITTER) {
radio.openWritingPipe(address);
radio.stopListening();
} else {
radio.openReadingPipe(1, address);
radio.writeAckPayload(1, &reply, sizeof(reply)); // первый ответ — заранее
radio.startListening();
}
}
void loop() {
if (TRANSMITTER) {
counter++;
if (!radio.write(&counter, sizeof(counter))) {
Serial.println("Нет связи");
} else if (radio.available()) {
int16_t answer;
radio.read(&answer, sizeof(answer));
Serial.print("Отправил "); Serial.print(counter);
Serial.print(", ответ "); Serial.println(answer);
} else {
Serial.println("Доставлено, ответа нет");
}
delay(1000);
} else if (radio.available()) {
int16_t got;
radio.read(&got, sizeof(got));
reply = got * 10; // уйдёт со следующим пакетом
radio.writeAckPayload(1, &reply, sizeof(reply));
}
}Приёмник отвечает прямо в подтверждении — передатчику не нужно переходить в приём. Ответ заряжается заранее и уходит со следующим пакетом, поэтому в выводе он отстаёт на шаг: на 2 приходит ответ на 1. Если ответы не нужны свежими, это не мешает; если нужны — передатчик шлёт второй короткий пакет-запрос.
Монитор порта:
Отправил 1, ответ 0 Отправил 2, ответ 10 Отправил 3, ответ 20 Отправил 4, ответ 30
Сканер эфира: какой канал выбрать
#include <SPI.h>
#include <RF24.h>
RF24 radio(9, 10);
const uint8_t CHANNELS = 126;
uint8_t hits[CHANNELS];
void setup() {
Serial.begin(115200);
if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }
radio.setAutoAck(false); // не отвечать на чужие пакеты
radio.startListening();
radio.stopListening();
radio.flush_rx();
// шапка: номер канала сверху вниз — сотни, десятки, единицы
for (uint8_t i = 0; i < CHANNELS; i++) Serial.print(i / 100);
Serial.println();
for (uint8_t i = 0; i < CHANNELS; i++) Serial.print(i / 10 % 10);
Serial.println();
for (uint8_t i = 0; i < CHANNELS; i++) Serial.print(i % 10);
Serial.println();
}
void loop() {
memset(hits, 0, sizeof(hits));
for (uint8_t pass = 0; pass < 20; pass++) {
for (uint8_t ch = 0; ch < CHANNELS; ch++) {
radio.setChannel(ch);
radio.startListening();
delayMicroseconds(128);
radio.stopListening();
if (radio.testRPD()) hits[ch]++;
}
}
// строка на проход: «-» — тихо, цифра — сколько раз из 20 канал был занят (9 — девять и больше)
for (uint8_t ch = 0; ch < CHANNELS; ch++) {
if (hits[ch] == 0) Serial.print('-');
else Serial.print(min(hits[ch], (uint8_t)9));
}
Serial.println();
}Нужна одна плата с модулем. Столбец — канал, строка — один проход по всем 126: прочерк — тихо, цифра — сколько раз из двадцати канал был занят. Выбирайте канал, где столбец из прочерков. testRPD() есть только у nRF24L01+; на старом чипе замените на testCarrier(). В выводе ниже — как выглядит роутер на 6-м канале Wi-Fi: он занимает 2426–2448 МГц, то есть каналы 26–48.
Монитор порта:
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111111 000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345 --------------------------216339999999999999353-1----------------------------------------------------------------------------- --------------------------1166399999999999994361------------------------------------------------------------------------------ --------------------------11363999999999999943422-----------------------------------------------------------------------------
На чём попадаются
begin() возвращает false или printDetails() печатает нули
Модуль не виден по SPI. Питание строго 3,3 В, CE и CSN как в конструкторе, провода короткие.
Работает рядом, теряет пакеты на расстоянии
Просадка питания при передаче. Конденсатор 10–100 мкФ прямо на выводах питания модуля — обязательная деталь.
Приёмник молчит
Адреса, канал и скорость должны совпадать на обеих платах, а приёмник — вызвать startListening().
Пакет приходит с мусором
Разный размер структуры на платах — например, int на UNO и на ESP32 разной длины. Используйте типы фиксированной ширины: int16_t, uint32_t.