MicroPython: ESP-NOW с ESP32 (начало работы)

Узнайте, как использовать протокол связи ESP-NOW с ESP32, запрограммированной на MicroPython. ESP-NOW — это протокол связи без установления соединения, созданный Espressif и предназначенный для передачи коротких пакетов данных. Это один из самых простых способов беспроводной связи между платами ESP32. В настоящее время прошивка MicroPython для ESP32 включает два встроенных модуля — espnow и aioespnow — которые предоставляют классы и функции для работы с ESP-NOW.

ESP-NOW с ESP32, запрограммированной на MicroPython

Используете Arduino IDE? Следуйте этому руководству: Начало работы с ESP-NOW (ESP32 с Arduino IDE).

Предварительные требования — прошивка MicroPython

Для выполнения этого руководства вам нужна прошивка MicroPython, установленная на ваших платах ESP32. Вам также понадобится IDE для написания и загрузки кода на плату. Мы рекомендуем использовать Thonny IDE:

Новичок в MicroPython? Ознакомьтесь с книгой: MicroPython Programming with ESP32 and ESP8266 eBook (2nd Edition)

Знакомство с ESP-NOW

ESP-NOW — это протокол беспроводной связи, разработанный Espressif, который позволяет нескольким платам ESP32 или ESP8266 обмениваться небольшими объёмами данных без использования Wi-Fi или Bluetooth. ESP-NOW не требует полного Wi-Fi-соединения (хотя контроллер Wi-Fi должен быть включён), что делает его идеальным для приложений с низким энергопотреблением и низкой задержкой, таких как сенсорные сети, пульты дистанционного управления или обмен данными между платами.

ESP-NOW — логотип ESP32

ESP-NOW использует модель связи без установления соединения, что означает, что устройства могут отправлять и получать данные без подключения к маршрутизатору или настройки точки доступа (в отличие от HTTP-связи между платами). Он поддерживает одноадресную (unicast — отправка данных на конкретное устройство по его MAC-адресу) и широковещательную (broadcast — отправка данных всем ближайшим устройствам с использованием широковещательного MAC-адреса) передачу сообщений.

ESP-NOW поддерживает следующие функции:

  • Поддержка как зашифрованной, так и незашифрованной одноадресной связи.

  • Прямая связь между 20 зарегистрированными пирами или 6 зашифрованными пирами.

  • Возможность одновременной связи с зашифрованными и незашифрованными устройствами.

  • Отправка пакетов данных размером до 250 байт.

ESP-NOW очень универсален, и вы можете организовать одностороннюю или двустороннюю связь в различных конфигурациях. Например:

ESP-NOW — односторонняя связь между двумя платами ESP-NOW — одна плата отправляет данные нескольким платам ESP-NOW — одна плата принимает данные от нескольких плат ESP-NOW — двусторонняя связь между несколькими платами
  • Одна плата ESP32 отправляет данные другой плате ESP32;

  • Двусторонняя связь ESP-NOW между двумя платами;

  • Одна ESP32 принимает данные от нескольких плат;

  • Одна плата ESP32 отправляет данные нескольким платам;

  • Несколько плат ESP32 отправляют и принимают данные друг от друга, создавая подобие сети.

Основные понятия ESP-NOW

Есть несколько базовых понятий, которые нужно понимать при работе с ESP-NOW:

  • Отправитель (Sender): устройство, которое отправляет данные с помощью ESP-NOW.

  • Приёмник (Receiver): устройство, которое получает данные, отправленные отправителем.

  • MAC-адрес: уникальный 6-байтовый аппаратный адрес, назначенный Wi-Fi-интерфейсу каждого устройства. ESP-NOW использует MAC-адреса для идентификации устройств. Если вы хотите отправить данные на конкретную плату, вам нужно знать её MAC-адрес.

  • Широковещательный MAC-адрес: MAC-адрес вида FF:FF:FF:FF:FF:FF используется для отправки сообщения всем ближайшим устройствам. Любой ESP32 или ESP8266 в зоне действия может его получить.

  • Пир (Peer): конкретное устройство (идентифицируемое по его MAC-адресу), которое вы регистрируете на своём ESP-NOW-устройстве, чтобы оно могло отправлять или получать данные от этого устройства.

  • Пакет (Packet): данные, передаваемые от одного устройства к другому. Пакеты ESP-NOW могут нести до 250 байт данных.

Требования для ESP-NOW

Перед использованием ESP-NOW необходимо учитывать несколько важных требований:

Один и тот же Wi-Fi-канал: все устройства, взаимодействующие через ESP-NOW, должны находиться на одном Wi-Fi-канале. Если это не так, они не смогут отправлять и получать пакеты друг от друга. Обычно это делается автоматически в коде, если только одно из устройств также не подключено к Wi-Fi-сети.

MAC-адрес: для отправки сообщений конкретному устройству отправитель должен заранее знать MAC-адрес приёмника.

Регистрация пиров: вы должны зарегистрировать пира перед отправкой ему данных, за исключением использования широковещательного MAC-адреса на платах ESP32.

Wi-Fi: Wi-Fi-интерфейс (режим станции или точки доступа) должен быть активен, даже если он не подключён к сети.

ESP32: Получение MAC-адреса платы

Для связи через ESP-NOW вам нужно знать MAC-адрес ваших плат. Чтобы получить MAC-адрес платы, скопируйте следующий код в Thonny IDE и запустите его на вашей плате.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp-now-esp32/

import network

wlan = network.WLAN(network.STA_IF)
wlan.active(True)

# Get MAC address (returns bytes)
mac = wlan.config('mac')

# Convert to human-readable format
mac_address = ':'.join('%02x' % b for b in mac)

print("MAC Address:", mac_address)

Просмотр исходного кода

После запуска кода MAC-адрес платы должен отобразиться в терминале (Shell).

Thonny IDE — получение MAC-адреса платы ESP32 на MicroPython

Рекомендуем наклеить метку или стикер на каждую плату, чтобы можно было чётко их различать.

ESP32 ESP-NOW MicroPython — связь «точка-точка»

ESP-NOW: Односторонняя связь «точка-точка» (модуль espnow)

Для начала работы с беспроводной связью ESP-NOW мы создадим простой проект, который показывает, как отправить сообщение с одной ESP32 на другую. Одна ESP32 будет отправителем, а другая ESP32 — приёмником.

ESP32 ESP-NOW — односторонняя связь

Код отправителя ESP32 (ESP-NOW с MicroPython)

Следующий код отправляет сообщение каждую секунду на плату-приёмник ESP32. Скопируйте его в Thonny IDE, затем запустите или загрузите на плату.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp-now-esp32/

import network
import espnow
import time

# Stats tracking
last_stats_time = time.time()
stats_interval = 10  # Print stats every 10 seconds

# Initialize Wi-Fi in station mode
sta = network.WLAN(network.STA_IF)
sta.active(True)
#sta.config(channel=1)  # Set channel explicitly if packets are not delivered
sta.disconnect()

# Initialize ESP-NOW
e = espnow.ESPNow()
try:
    e.active(True)
except OSError as err:
    print("Failed to initialize ESP-NOW:", err)
    raise

# Receiver's MAC address
receiver_mac = b'\x30\xae\xa4\xf6\x7d\x4c'
#receiver_mac = b'\xff\xff\xff\xff\xff\xff' #broadcast

# Add peer
try:
    e.add_peer(receiver_mac)
except OSError as err:
    print("Failed to add peer:", err)
    raise

def print_stats():
    stats = e.stats()
    print("\nESP-NOW Statistics:")
    print(f"  Packets Sent: {stats[0]}")
    print(f"  Packets Delivered: {stats[1]}")
    print(f"  Packets Dropped (TX): {stats[2]}")
    print(f"  Packets Received: {stats[3]}")
    print(f"  Packets Dropped (RX): {stats[4]}")

# Main loop to send messages
message_count = 0
while True:
    try:
        # Create a sample message with a counter
        message = f"Hello! ESP-NOW message #{message_count}"
        # Send the message with acknowledgment
        try:
            if e.send(receiver_mac, message, True):
                print(f"Sent message: {message}")
            else:
                print("Failed to send message (send returned False)")
        except OSError as err:
            print(f"Failed to send message (OSError: {err})")

        message_count += 1

        # Print stats every 10 seconds
        if time.time() - last_stats_time >= stats_interval:
            print_stats()
            last_stats_time = time.time()

        time.sleep(1)  # Send every 1 second

    except OSError as err:
        print("Error:", err)
        time.sleep(5)

    except KeyboardInterrupt:
        print("Stopping sender...")
        e.active(False)
        sta.active(False)
        break

Просмотр исходного кода

Вам нужно вставить MAC-адрес платы-приёмника или использовать широковещательный MAC-адрес.

В моём случае MAC-адрес платы-приёмника — 30:AE:A4:F6:7D:4C. Его нужно преобразовать в формат bytes следующим образом:

  • 30:AE:A4:F6:7D:4C > b'\x30\xae\xa4\xf6\x7d\x4c'

Сделайте то же самое для MAC-адреса вашей платы-приёмника.

# Receiver's MAC address
receiver_mac = b'\x30\xae\xa4\xf6\x7d\x4c'

Как работает код?

Давайте кратко разберём, как работает код и наиболее важные функции ESP-NOW.

Импорт модулей

Сначала импортируем необходимые модули. В этом примере мы используем модуль espnow для работы с функциями и классами ESP-NOW.

import network
import espnow
import time
Инициализация Wi-Fi-интерфейса

Затем нужно инициализировать Wi-Fi (даже если мы его не используем) для работы ESP-NOW. Можно использовать режим станции (STA_IF) или точки доступа (AP_IF).

# Initialize Wi-Fi in station mode
sta = network.WLAN(network.STA_IF)
sta.active(True)
#sta.config(channel=1)  # Set channel explicitly if packets are not delivered
sta.disconnect()

Позже, если при тестировании пакеты не доставляются, возможно, потребуется вручную указать Wi-Fi-канал. Он должен быть одинаковым на платах отправителя и приёмника.

#sta.config(channel=1)  # Set channel explicitly if packets are not delivered
Инициализация ESP-NOW

Затем можно инициализировать ESP-NOW. Сначала создайте экземпляр espnow с именем e. Затем активируйте его с помощью метода active(), передав True в качестве аргумента.

# Initialize ESP-NOW
e = espnow.ESPNow()
try:
    e.active(True)
except OSError as err:
    print("Failed to initialize ESP-NOW:", err)
    raise

Чтобы деактивировать ESP-NOW, передайте False в метод active(). Если вызвать метод active() без аргументов, он вернёт текущее состояние ESP-NOW.

Мы активируем ESP-NOW внутри конструкции try / except, чтобы перехватить ошибки при неудачной инициализации.

Добавление пира ESP-NOW

Добавьте MAC-адрес приёмника (или используйте широковещательный MAC-адрес для отправки сообщения всем устройствам в зоне действия):

# Receiver's MAC address
receiver_mac = b'\x30\xae\xa4\xf6\x7d\x4c'
#receiver_mac = b'\xff\xff\xff\xff\xff\xff' #broadcast

Затем добавляем MAC-адрес приёмника в качестве пира с помощью метода add_peer().

# Add peer
try:
    e.add_peer(receiver_mac)
except OSError as err:
    print("Failed to add peer:", err)
    raise
Вывод статистики ESP-NOW

Затем создаём функцию print_stats(), которую будем вызывать позже в коде. Она выводит текущую статистику пакетов ESP-NOW. Для получения количества отправленных/полученных/потерянных пакетов вызываем метод stats() объекта ESP-NOW e.

Он возвращает 5-кортеж, содержащий количество отправленных/полученных/потерянных пакетов:

(tx_pkts, tx_responses, tx_failures, rx_packets, rx_dropped_packets)

Затем выводим каждый из этих результатов:

print("\nESP-NOW Statistics:")
print(f"  Packets Sent: {stats[0]}")
print(f"  Packets Delivered: {stats[1]}")
print(f"  Packets Dropped (TX): {stats[2]}")
print(f"  Packets Received: {stats[3]}")
print(f"  Packets Dropped (RX): {stats[4]}")
Отправка сообщения ESP-NOW

Создаём основной цикл, который отправляет сообщение со счётчиком (message_count) каждую секунду.

message_count = 0
while True:
    try:
        # Create a sample message with a counter
        message = f"Hello! ESP-NOW message #{message_count}"

Отправляем сообщение с помощью метода send() объекта ESP-NOW e. Эта функция принимает в качестве аргументов MAC-адрес платы-приёмника, сообщение и последний параметр — логическое значение (True — отправить сообщение пиру и ждать ответа; False — вернуться немедленно).

try:
    if e.send(receiver_mac, message, True):
        print(f"Sent message: {message}")
    else:
        print("Failed to send message (send returned False)")
except OSError as err:
    print(f"Failed to send message (OSError: {err})")

После отправки сообщения увеличиваем счётчик и проверяем, не пора ли вывести новую статистику.

# Print stats every 10 seconds
if time.time() - last_stats_time >= stats_interval:
    print_stats()
    last_stats_time = time.time()

time.sleep(1)  # Send every 1 second
Подведём итог…

Подведём итог — вот основные шаги:

  • Инициализировать Wi-Fi-интерфейс;

  • Инициализировать ESP-NOW;

  • Добавить пира;

  • Отправить сообщение.

Запуск кода

После подключения платы к компьютеру и установления соединения с Thonny IDE вы можете загрузить код как main.py на плату или запустить его, нажав зелёную кнопку Run.

Thonny IDE — кнопка запуска скрипта MicroPython

Он начнёт выводить сообщения в терминал (Shell). Отправка сообщений будет неудачной, потому что мы ещё не подготовили приёмник.

ESP32 ESP-NOW — ошибка отправки пакета на MicroPython

Код приёмника ESP32 (ESP-NOW с MicroPython)

Следующий код прослушивает входящие пакеты ESP-NOW от пира (платы-отправителя, которую мы запрограммировали ранее). Откройте ещё одно окно Thonny IDE (независимо от первого).

Включение двух экземпляров Thonny IDE

Перейдите в Tools > Options и снимите галочку с опции Allow only single Thonny instance.

Скопируйте код в Thonny IDE, затем запустите или загрузите его на плату.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp-now-esp32/

import network
import espnow
import time

# Stats tracking
last_stats_time = time.time()
stats_interval = 10  # Print stats every 10 seconds

# Initialize Wi-Fi in station mode
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.config(channel=1)  # Set channel explicitly if packets are not received
sta.disconnect()

# Initialize ESP-NOW
e = espnow.ESPNow()
try:
    e.active(True)
except OSError as err:
    print("Failed to initialize ESP-NOW:", err)
    raise

# Sender's MAC address
sender_mac = b'\x30\xae\xa4\x07\x0d\x64'  # Sender MAC

# Add peer (sender) for unicast reliability
# You don't need to add peer for broadcast
#try:
#    e.add_peer(sender_mac)
#except OSError as err:
#    print("Failed to add peer:", err)
#    raise

def print_stats():
    stats = e.stats()
    print("\nESP-NOW Statistics:")
    print(f"  Packets Sent: {stats[0]}")
    print(f"  Packets Delivered: {stats[1]}")
    print(f"  Packets Dropped (TX): {stats[2]}")
    print(f"  Packets Received: {stats[3]}")
    print(f"  Packets Dropped (RX): {stats[4]}")

print("Listening for ESP-NOW messages...")
while True:
    try:
        # Receive message (host MAC, message, timeout of 10 seconds)
        host, msg = e.recv(10000)

        # Print stats every 10 seconds
        if time.time() - last_stats_time >= stats_interval:
            print_stats()
            last_stats_time = time.time()

    except OSError as err:
        print("Error:", err)
        time.sleep(5)

    except KeyboardInterrupt:
        print("Stopping receiver...")
        e.active(False)
        sta.active(False)
        break

Просмотр исходного кода

Как работает код?

Код аналогичен коду платы-отправителя. Единственное отличие — в цикле, где мы прослушиваем входящие пакеты.

Указание MAC-адреса платы-отправителя и добавление его в качестве пира является необязательным. Однако это рекомендуется для лучшей надёжности, особенно если пакеты не принимаются.

В моём случае MAC-адрес платы-отправителя — 30:AE:A4:07:0D:64. Его нужно преобразовать в формат bytes следующим образом:

  • 30:AE:A4:07:0D:64 > b'\x30\xae\xa4\x07\x0d\x64'

Сделайте то же самое для MAC-адреса вашей платы-отправителя.

# Sender's MAC address
sender_mac = b'\x30\xae\xa4\x07\x0d\x64'  # Sender MAC

Если вы хотите добавить отправителя в качестве пира, раскомментируйте следующую часть кода:

#try:
#    e.add_peer(sender_mac)
#except OSError as err:
#    print("Failed to add peer:", err)
#    raise
Приём сообщений ESP-NOW

Для приёма сообщений используем метод recv() объекта ESP-NOW e.

host, msg = e.recv(10000)

Этот метод ожидает входящее сообщение и возвращает MAC-адрес пира (сохраняется в переменной host) и сообщение (сохраняется в переменной msg). Для приёма сообщения от пира не обязательно регистрировать его (с помощью add_peer()).

В качестве аргумента он принимает таймаут в миллисекундах. Таймаут — это время, в течение которого функция будет ожидать входящее сообщение, прежде чем сдаться и вернуть управление. Он может иметь следующие значения:

  • 0: Без таймаута. Немедленный возврат, если данных нет.

  • > 0: указать значение таймаута в миллисекундах;

  • < 0: без таймаута: ожидать новых сообщений бесконечно;

  • None: использовать значение таймаута по умолчанию, установленное в ESPNOW.config().

Когда мы получаем новое сообщение, выводим MAC-адрес хоста и само сообщение.

if msg:
    print(f"Received from {host.hex()}: {msg.decode()}")

Запуск кода

Откройте новое окно Thonny IDE и установите соединение с платой-приёмником. Загрузите и/или запустите код приёмника на этой новой плате.

Она начнёт принимать пакеты ESP-NOW от другой платы.

ESP32 с MicroPython — приёмник ESP-NOW

В это же время вы увидите, что плата-отправитель теперь выводит сообщения об успешной отправке.

ESP32 MicroPython — отправитель ESP-NOW

Каждые 10 секунд отправитель и приёмник выводят статистику ESP-NOW в терминал.

Вы можете объединить функции, используемые в обоих скетчах, и установить двустороннюю связь. Также можно добавить больше плат в вашу систему и создать сеть.

ESP-NOW: Односторонняя связь «точка-точка» (модуль aioespnow)

Существует ещё один модуль, который поддерживает ESP-NOW асинхронно с использованием модуля asyncio — это модуль aioespnow. В этом разделе мы создадим тот же пример, что и в предыдущем, но с использованием модуля aioespnow, который поддерживает асинхронное программирование.

Перед продолжением рекомендуем ознакомиться с асинхронным программированием на MicroPython: MicroPython: ESP32/ESP8266 — Асинхронное программирование — Запуск нескольких задач.

Код отправителя ESP32 (ESP-NOW с MicroPython)

Следующий код делает то же самое, что и в предыдущем примере, но использует модуль aioespnow для асинхронного программирования.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp-now-esp32/

import network
import aioespnow
import asyncio
import time

# Stats tracking
last_stats_time = time.time()
stats_interval = 10  # Print stats every 10 seconds

# Initialize Wi-Fi in station mode
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.disconnect()

# Initialize AIOESPNow
e = aioespnow.AIOESPNow()
try:
    e.active(True)
except OSError as err:
    print("Failed to initialize AIOESPNow:", err)
    raise

# Receiver's MAC address (broadcast for testing)
#receiver_mac = b'\x30\x0e\xa4\xf6\x7d\x4c'
receiver_mac = b'\xff\xff\xff\xff\xff\xff'

# Add peer
try:
    e.add_peer(receiver_mac)
except OSError as err:
    print("Failed to add peer:", err)
    raise

def print_stats():
    stats = e.stats()
    print("\nESP-NOW Statistics:")
    print(f"  Packets Sent: {stats[0]}")
    print(f"  Packets Delivered: {stats[1]}")
    print(f"  Packets Dropped (TX): {stats[2]}")
    print(f"  Packets Received: {stats[3]}")
    print(f"  Packets Dropped (RX): {stats[4]}")

# Async function to send messages
async def send_messages(e, peer):
    message_count = 0
    while True:
        try:
            message = f"Hello! AIOESPNow message #{message_count}"
            if await e.asend(peer, message, sync=True):
                print(f"Sent message: {message}")
            else:
                print("Failed to send message")

            message_count += 1

            # Print stats every 10 seconds
            if time.time() - last_stats_time >= stats_interval:
                print_stats()

            await asyncio.sleep(1)  # Send every 1 second

        except OSError as err:
            print("Error:", err)
            await asyncio.sleep(5)

# Main async function
async def main(e, peer):
    await send_messages(e, peer)

# Run the async program
try:
    asyncio.run(main(e, receiver_mac))
except KeyboardInterrupt:
    print("Stopping sender...")
    e.active(False)
    sta.active(False)

Просмотр исходного кода

Код приёмника ESP32 (ESP-NOW с MicroPython)

Аналогично, следующий код принимает входящие пакеты ESP-NOW от платы-отправителя, но использует модуль aioespnow для асинхронного программирования.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp-now-esp32/

import network
import aioespnow
import asyncio
import time

# Initialize Wi-Fi in station mode
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.config(channel=1)  # Set channel explicitly if packets are not received
sta.disconnect()

# Initialize AIOESPNow
e = aioespnow.AIOESPNow()
try:
    e.active(True)
except OSError as err:
    print("Failed to initialize AIOESPNow:", err)
    raise

# Sender's MAC address
sender_mac = b'\x30\xae\xa4\x07\x0d\x64'  # Sender MAC for unicast

# Add peer (sender) for unicast reliability
# You don't need to add peer for broadcast
#try:
#    e.add_peer(sender_mac)
#except OSError as err:
#    print("Failed to add peer:", err)
#    raise

# Async function to receive messages
async def receive_messages(e):
    while True:
        try:
            async for mac, msg in e:
                print(f"Received from {mac.hex()}: {msg.decode()}")
        except OSError as err:
            print("Error:", err)
            await asyncio.sleep(5)

# Async function to print stats periodically
async def print_stats(e):
    global last_stats_time
    last_stats_time = time.time()
    stats_interval = 10  # Print stats every 10 seconds
    while True:
        if time.time() - last_stats_time >= stats_interval:
            stats = e.stats()
            print("\nESP-NOW Statistics:")
            print(f"  Packets Sent: {stats[0]}")
            print(f"  Packets Delivered: {stats[1]}")
            print(f"  Packets Dropped (TX): {stats[2]}")
            print(f"  Packets Received: {stats[3]}")
            print(f"  Packets Dropped (RX): {stats[4]}")
            last_stats_time = time.time()
        await asyncio.sleep(1)  # Check every second

# Main async function
async def main(e):
    # Run receive and stats tasks concurrently
    await asyncio.gather(receive_messages(e), print_stats(e))

# Run the async program
try:
    asyncio.run(main(e))
except KeyboardInterrupt:
    print("Stopping receiver...")
    e.active(False)
    sta.active(False)

Просмотр исходного кода

Для получения дополнительной информации о модулях, функциях и классах ESP-NOW для MicroPython, ознакомьтесь с документацией.

Заключение

В этом руководстве мы познакомили вас с основами протокола связи ESP-NOW с ESP32, запрограммированной на MicroPython.

ESP-NOW — очень универсальный протокол, который может быть полезен для беспроводной передачи данных между платами, таких как показания датчиков или команды для управления выходами. Вы можете добавить несколько плат ESP32 в вашу систему и создать сеть плат, обменивающихся данными друг с другом.

Если вас интересует эта тема, мы можем создать больше руководств по ESP-NOW с MicroPython. Сообщите нам в комментариях.

Для дальнейшего изучения MicroPython ознакомьтесь с нашими ресурсами: