Browse Source

esp32,esp8266: Add support for the Espressif ESP-NOW protocol.

ESP-NOW is a proprietary wireless communication protocol which supports
connectionless communication between ESP32 and ESP8266 devices, using
vendor specific WiFi frames.  This commit adds support for this protocol
through a new `espnow` module.

This commit builds on original work done by @nickzoic, @shawwwn and with
contributions from @zoland.  Features include:
- Use of (extended) ring buffers in py/ringbuf.[ch] for robust IO.
- Signal strength (RSSI) monitoring.
- Core support in `_espnow` C module, extended by `espnow.py` module.
- Asyncio support via `aioespnow.py` module (separate to this commit).
- Docs provided at `docs/library/espnow.rst`.

Methods available in espnow.ESPNow class are:
- active(True/False)
- config(): set rx buffer size, read timeout and tx rate
- recv()/irecv()/recvinto() to read incoming messages from peers
- send() to send messages to peer devices
- any() to test if a message is ready to read
- irq() to set callback for received messages
- stats() returns transfer stats:
    (tx_pkts, tx_pkt_responses, tx_failures, rx_pkts, lost_rx_pkts)
- add_peer(mac, ...) registers a peer before sending messages
- get_peer(mac) returns peer info: (mac, lmk, channel, ifidx, encrypt)
- mod_peer(mac, ...) changes peer info parameters
- get_peers() returns all peer info tuples
- peers_table supports RSSI signal monitoring for received messages:
    {peer1: [rssi, time_ms], peer2: [rssi, time_ms], ...}

ESP8266 is a pared down version of the ESP32 ESPNow support due to code
size restrictions and differences in the low-level API.  See docs for
details.

Also included is a test suite in tests/multi_espnow.  This tests basic
espnow data transfer, multiple transfers, various message sizes, encrypted
messages (pmk and lmk), and asyncio support.

Initial work is from https://github.com/micropython/micropython/pull/4115.
Initial import of code is from:
https://github.com/nickzoic/micropython/tree/espnow-4115.
pull/6515/head
Glenn Moloney 4 years ago
committed by Damien George
parent
commit
7fa322afb8
  1. 917
      docs/library/espnow.rst
  2. 5
      docs/library/index.rst
  3. 1
      ports/esp32/boards/manifest.py
  4. 9
      ports/esp32/main.c
  5. 1
      ports/esp32/main/CMakeLists.txt
  6. 884
      ports/esp32/modespnow.c
  7. 30
      ports/esp32/modespnow.h
  8. 1
      ports/esp32/modnetwork.h
  9. 30
      ports/esp32/modules/espnow.py
  10. 3
      ports/esp32/mpconfigport.h
  11. 12
      ports/esp32/network_wlan.c
  12. 10
      ports/esp8266/Makefile
  13. 1
      ports/esp8266/boards/GENERIC/mpconfigboard.mk
  14. 1
      ports/esp8266/boards/GENERIC_1M/mpconfigboard.mk
  15. 1
      ports/esp8266/boards/esp8266_common.ld
  16. 1
      ports/esp8266/boards/manifest.py
  17. 8
      ports/esp8266/main.c
  18. 507
      ports/esp8266/modespnow.c
  19. 28
      ports/esp8266/modespnow.h
  20. 37
      ports/esp8266/modules/espnow.py
  21. 57
      tests/multi_espnow/10_simple_data.py
  22. 6
      tests/multi_espnow/10_simple_data.py.exp
  23. 93
      tests/multi_espnow/20_send_echo.py
  24. 21
      tests/multi_espnow/20_send_echo.py.exp
  25. 130
      tests/multi_espnow/30_lmk_echo.py
  26. 8
      tests/multi_espnow/30_lmk_echo.py.exp
  27. 113
      tests/multi_espnow/40_recv_test.py
  28. 14
      tests/multi_espnow/40_recv_test.py.exp
  29. 114
      tests/multi_espnow/50_esp32_rssi_test.py
  30. 10
      tests/multi_espnow/50_esp32_rssi_test.py.exp
  31. 117
      tests/multi_espnow/60_irq_test.py
  32. 8
      tests/multi_espnow/60_irq_test.py.exp
  33. 110
      tests/multi_espnow/80_uasyncio_client.py
  34. 18
      tests/multi_espnow/80_uasyncio_client.py.exp
  35. 96
      tests/multi_espnow/81_uasyncio_server.py
  36. 11
      tests/multi_espnow/81_uasyncio_server.py.exp
  37. 108
      tests/multi_espnow/90_memory_test.py
  38. 25
      tests/multi_espnow/90_memory_test.py.exp

917
docs/library/espnow.rst

@ -0,0 +1,917 @@
:mod:`espnow` --- support for the ESP-NOW wireless protocol
===========================================================
.. module:: espnow
:synopsis: ESP-NOW wireless protocol support
This module provides an interface to the `ESP-NOW <https://www.espressif.com/
en/products/software/esp-now/overview>`_ protocol provided by Espressif on
ESP32 and ESP8266 devices (`API docs <https://docs.espressif.com/
projects/esp-idf/en/latest/api-reference/network/esp_now.html>`_).
Table of Contents:
------------------
- `Introduction`_
- `Configuration`_
- `Sending and Receiving Data`_
- `Peer Management`_
- `Callback Methods`_
- `Exceptions`_
- `Constants`_
- `Wifi Signal Strength (RSSI) - (ESP32 Only)`_
- `Supporting asyncio`_
- `Broadcast and Multicast`_
- `ESPNow and Wifi Operation`_
- `ESPNow and Sleep Modes`_
Introduction
------------
ESP-NOW is a connection-less wireless communication protocol supporting:
- Direct communication between up to 20 registered peers:
- Without the need for a wireless access point (AP),
- Encrypted and unencrypted communication (up to 6 encrypted peers),
- Message sizes up to 250 bytes,
- Can operate alongside Wifi operation (:doc:`network.WLAN<network.WLAN>`) on
ESP32 and ESP8266 devices.
It is especially useful for small IoT networks, latency sensitive or power
sensitive applications (such as battery operated devices) and for long-range
communication between devices (hundreds of metres).
This module also supports tracking the Wifi signal strength (RSSI) of peer
devices.
A simple example would be:
**Sender:** ::
import network
import espnow
# A WLAN interface must be active to send()/recv()
sta = network.WLAN(network.STA_IF) # Or network.AP_IF
sta.active(True)
sta.disconnect() # For ESP8266
e = espnow.ESPNow()
e.active(True)
peer = b'\xbb\xbb\xbb\xbb\xbb\xbb' # MAC address of peer's wifi interface
e.add_peer(peer) # Must add_peer() before send()
e.send(peer, "Starting...")
for i in range(100):
e.send(peer, str(i)*20, True)
e.send(peer, b'end')
**Receiver:** ::
import network
import espnow
# A WLAN interface must be active to send()/recv()
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.disconnect() # Because ESP8266 auto-connects to last Access Point
e = espnow.ESPNow()
e.active(True)
while True:
host, msg = e.recv()
if msg: # msg == None if timeout in recv()
print(host, msg)
if msg == b'end':
break
class ESPNow
------------
Constructor
-----------
.. class:: ESPNow()
Returns the singleton ESPNow object. As this is a singleton, all calls to
`espnow.ESPNow()` return a reference to the same object.
.. note::
Some methods are available only on the ESP32 due to code size
restrictions on the ESP8266 and differences in the Espressif API.
Configuration
-------------
.. method:: ESPNow.active([flag])
Initialise or de-initialise the ESPNow communication protocol depending on
the value of the ``flag`` optional argument.
.. data:: Arguments:
- *flag*: Any python value which can be converted to a boolean type.
- ``True``: Prepare the software and hardware for use of the ESPNow
communication protocol, including:
- initialise the ESPNow data structures,
- allocate the recv data buffer,
- invoke esp_now_init() and
- register the send and recv callbacks.
- ``False``: De-initialise the Espressif ESPNow software stack
(esp_now_deinit()), disable callbacks, deallocate the recv
data buffer and deregister all peers.
If *flag* is not provided, return the current status of the ESPNow
interface.
.. data:: Returns:
``True`` if interface is currently *active*, else ``False``.
.. method:: ESPNow.config(param=value, ...)
ESPNow.config('param') (ESP32 only)
Set or get configuration values of the ESPNow interface. To set values, use
the keyword syntax, and one or more parameters can be set at a time. To get
a value the parameter name should be quoted as a string, and just one
parameter is queried at a time.
**Note:** *Getting* parameters is not supported on the ESP8266.
.. data:: Options:
*rxbuf*: (default=526) Get/set the size in bytes of the internal
buffer used to store incoming ESPNow packet data. The default size is
selected to fit two max-sized ESPNow packets (250 bytes) with associated
mac_address (6 bytes), a message byte count (1 byte) and RSSI data plus
buffer overhead. Increase this if you expect to receive a lot of large
packets or expect bursty incoming traffic.
**Note:** The recv buffer is allocated by `ESPNow.active()`. Changing
this value will have no effect until the next call of
`ESPNow.active(True)<ESPNow.active()>`.
*timeout_ms*: (default=300,000) Default timeout (in milliseconds)
for receiving ESPNOW messages. If *timeout_ms* is less than zero, then
wait forever. The timeout can also be provided as arg to
`recv()`/`irecv()`/`recvinto()`.
*rate*: (ESP32 only, IDF>=4.3.0 only) Set the transmission speed for
espnow packets. Must be set to a number from the allowed numeric values
in `enum wifi_phy_rate_t
<https://docs.espressif.com/projects/esp-idf/en/v4.4.1/esp32/
api-reference/network/esp_wifi.html#_CPPv415wifi_phy_rate_t>`_.
.. data:: Returns:
``None`` or the value of the parameter being queried.
.. data:: Raises:
- ``OSError(num, "ESP_ERR_ESPNOW_NOT_INIT")`` if not initialised.
- ``ValueError()`` on invalid configuration options or values.
Sending and Receiving Data
--------------------------
A wifi interface (``network.STA_IF`` or ``network.AP_IF``) must be
`active()<network.WLAN.active>` before messages can be sent or received,
but it is not necessary to connect or configure the WLAN interface.
For example::
import network
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.disconnect() # For ESP8266
**Note:** The ESP8266 has a *feature* that causes it to automatically reconnect
to the last wifi Access Point when set `active(True)<network.WLAN.active>` (even
after reboot/reset). This reduces the reliability of receiving ESP-NOW messages
(see `ESPNow and Wifi Operation`_). You can avoid this by calling
`disconnect()<network.WLAN.disconnect>` after
`active(True)<network.WLAN.active>`.
.. method:: ESPNow.send(mac, msg[, sync])
ESPNow.send(msg) (ESP32 only)
Send the data contained in ``msg`` to the peer with given network ``mac``
address. In the second form, ``mac=None`` and ``sync=True``. The peer must
be registered with `ESPNow.add_peer()<ESPNow.add_peer()>` before the
message can be sent.
.. data:: Arguments:
- *mac*: byte string exactly ``espnow.ADDR_LEN`` (6 bytes) long or
``None``. If *mac* is ``None`` (ESP32 only) the message will be sent
to all registered peers, except any broadcast or multicast MAC
addresses.
- *msg*: string or byte-string up to ``espnow.MAX_DATA_LEN`` (250)
bytes long.
- *sync*:
- ``True``: (default) send ``msg`` to the peer(s) and wait for a
response (or not).
- ``False`` send ``msg`` and return immediately. Responses from the
peers will be discarded.
.. data:: Returns:
``True`` if ``sync=False`` or if ``sync=True`` and *all* peers respond,
else ``False``.
.. data:: Raises:
- ``OSError(num, "ESP_ERR_ESPNOW_NOT_INIT")`` if not initialised.
- ``OSError(num, "ESP_ERR_ESPNOW_NOT_FOUND")`` if peer is not registered.
- ``OSError(num, "ESP_ERR_ESPNOW_IF")`` the wifi interface is not
`active()<network.WLAN.active>`.
- ``OSError(num, "ESP_ERR_ESPNOW_NO_MEM")`` internal ESP-NOW buffers are
full.
- ``ValueError()`` on invalid values for the parameters.
**Note**: A peer will respond with success if its wifi interface is
`active()<network.WLAN.active>` and set to the same channel as the sender,
regardless of whether it has initialised it's ESP-Now system or is
actively listening for ESP-Now traffic (see the Espressif ESP-Now docs).
.. method:: ESPNow.recv([timeout_ms])
Wait for an incoming message and return the ``mac`` address of the peer and
the message. **Note**: It is **not** necessary to register a peer (using
`add_peer()<ESPNow.add_peer()>`) to receive a message from that peer.
.. data:: Arguments:
- *timeout_ms*: (Optional): May have the following values.
- ``0``: No timeout. Return immediately if no data is available;
- ``> 0``: Specify a timeout value in milliseconds;
- ``< 0``: Do not timeout, ie. wait forever for new messages; or
- ``None`` (or not provided): Use the default timeout value set with
`ESPNow.config()`.
.. data:: Returns:
- ``(None, None)`` if timeout is reached before a message is received, or
- ``[mac, msg]``: where:
- ``mac`` is a bytestring containing the address of the device which
sent the message, and
- ``msg`` is a bytestring containing the message.
.. data:: Raises:
- ``OSError(num, "ESP_ERR_ESPNOW_NOT_INIT")`` if not initialised.
- ``OSError(num, "ESP_ERR_ESPNOW_IF")`` if the wifi interface is not
`active()<network.WLAN.active>`.
- ``ValueError()`` on invalid *timeout_ms* values.
`ESPNow.recv()` will allocate new storage for the returned list and the
``peer`` and ``msg`` bytestrings. This can lead to memory fragmentation if
the data rate is high. See `ESPNow.irecv()` for a memory-friendly
alternative.
.. method:: ESPNow.irecv([timeout_ms])
Works like `ESPNow.recv()` but will re-use internal bytearrays to store the
return values: ``[mac, msg]``, so that no new memory is allocated on each
call.
.. data:: Arguments:
*timeout_ms*: (Optional) Timeout in milliseconds (see `ESPNow.recv()`).
.. data:: Returns:
- As for `ESPNow.recv()`, except that ``msg`` is a bytearray, instead of
a bytestring. On the ESP8266, ``mac`` will also be a bytearray.
.. data:: Raises:
- See `ESPNow.recv()`.
**Note:** You may also read messages by iterating over the ESPNow object,
which will use the `irecv()` method for alloc-free reads, eg: ::
import espnow
e = espnow.ESPNow(); e.active(True)
for mac, msg in e:
print(mac, msg)
if mac is None: # mac, msg will equal (None, None) on timeout
break
.. method:: ESPNow.recvinto(data[, timeout_ms])
Wait for an incoming message and return the length of the message in bytes.
This is the low-level method used by both `recv()<ESPNow.recv()>` and
`irecv()` to read messages.
.. data:: Arguments:
*data*: A list of at least two elements, ``[peer, msg]``. ``msg`` must
be a bytearray large enough to hold the message (250 bytes). On the
ESP8266, ``peer`` should be a bytearray of 6 bytes. The MAC address of
the sender and the message will be stored in these bytearrays (see Note
on ESP32 below).
*timeout_ms*: (Optional) Timeout in milliseconds (see `ESPNow.recv()`).
.. data:: Returns:
- Length of message in bytes or 0 if *timeout_ms* is reached before a
message is received.
.. data:: Raises:
- See `ESPNow.recv()`.
**Note:** On the ESP32:
- It is unnecessary to provide a bytearray in the first element of the
``data`` list because it will be replaced by a reference to a unique
``peer`` address in the **peer device table** (see `ESPNow.peers_table`).
- If the list is at least 4 elements long, the rssi and timestamp values
will be saved as the 3rd and 4th elements.
.. method:: ESPNow.any()
Check if data is available to be read with `ESPNow.recv()`.
For more sophisticated querying of available characters use `select.poll()`::
import select
import espnow
e = espnow.ESPNow()
poll = select.poll()
poll.register(e, select.POLLIN)
poll.poll(timeout)
.. data:: Returns:
``True`` if data is available to be read, else ``False``.
.. method:: ESPNow.stats() (ESP32 only)
.. data:: Returns:
A 5-tuple containing the number of packets sent/received/lost:
``(tx_pkts, tx_responses, tx_failures, rx_packets, rx_dropped_packets)``
Incoming packets are *dropped* when the recv buffers are full. To reduce
packet loss, increase the ``rxbuf`` config parameters and ensure you are
reading messages as quickly as possible.
**Note**: Dropped packets will still be acknowledged to the sender as
received.
Peer Management
---------------
The Espressif ESP-Now software requires that other devices (peers) must be
*registered* before we can `send()<ESPNow.send()>` them messages. It is
**not** necessary to *register* a peer to receive a message from that peer.
.. method:: ESPNow.set_pmk(pmk)
Set the Primary Master Key (PMK) which is used to encrypt the Local Master
Keys (LMK) for encrypting ESPNow data traffic. If this is not set, a
default PMK is used by the underlying Espressif esp_now software stack.
**Note:** messages will only be encrypted if *lmk* is also set in
`ESPNow.add_peer()` (see `Security
<https://docs.espressif.com/projects/esp-idf/en/latest/
esp32/api-reference/network/esp_now.html#security>`_ in the Espressif API
docs).
.. data:: Arguments:
*pmk*: Must be a byte string, bytearray or string of length
`espnow.KEY_LEN` (16 bytes).
.. data:: Returns:
``None``
.. data:: Raises:
``ValueError()`` on invalid *pmk* values.
.. method:: ESPNow.add_peer(mac, [lmk], [channel], [ifidx], [encrypt])
ESPNow.add_peer(mac, param=value, ...) (ESP32 only)
Add/register the provided *mac* address as a peer. Additional parameters
may also be specified as positional or keyword arguments:
.. data:: Arguments:
- *mac*: The MAC address of the peer (as a 6-byte byte-string).
- *lmk*: The Local Master Key (LMK) key used to encrypt data
transfers with this peer (unless the *encrypt* parameter is set to
``False``). Must be:
- a byte-string or bytearray or string of length ``espnow.KEY_LEN``
(16 bytes), or
- any non ``True`` python value (default= ``b''``), signifying an
*empty* key which will disable encryption.
- *channel*: The wifi channel (2.4GHz) to communicate with this peer.
Must be an integer from 0 to 14. If channel is set to 0 the current
channel of the wifi device will be used. (default=0)
- *ifidx*: (ESP32 only) Index of the wifi interface which will be
used to send data to this peer. Must be an integer set to
``network.STA_IF`` (=0) or ``network.AP_IF`` (=1).
(default=0/``network.STA_IF``). See `ESPNow and Wifi Operation`_
below for more information.
- *encrypt*: (ESP32 only) If set to ``True`` data exchanged with
this peer will be encrypted with the PMK and LMK. (default =
``False``)
**ESP8266**: Keyword args may not be used on the ESP8266.
**Note:** The maximum number of peers which may be registered is 20
(`espnow.MAX_TOTAL_PEER_NUM`), with a maximum of 6
(`espnow.MAX_ENCRYPT_PEER_NUM`) of those peers with encryption enabled
(see `ESP_NOW_MAX_ENCRYPT_PEER_NUM <https://docs.espressif.com/
projects/esp-idf/en/latest/esp32/api-reference/network/
esp_now.html#c.ESP_NOW_MAX_ENCRYPT_PEER_NUM>`_ in the Espressif API
docs).
.. data:: Raises:
- ``OSError(num, "ESP_ERR_ESPNOW_NOT_INIT")`` if not initialised.
- ``OSError(num, "ESP_ERR_ESPNOW_EXIST")`` if *mac* is already
registered.
- ``OSError(num, "ESP_ERR_ESPNOW_FULL")`` if too many peers are
already registered.
- ``ValueError()`` on invalid keyword args or values.
.. method:: ESPNow.del_peer(mac)
Deregister the peer associated with the provided *mac* address.
.. data:: Returns:
``None``
.. data:: Raises:
- ``OSError(num, "ESP_ERR_ESPNOW_NOT_INIT")`` if not initialised.
- ``OSError(num, "ESP_ERR_ESPNOW_NOT_FOUND")`` if *mac* is not
registered.
- ``ValueError()`` on invalid *mac* values.
.. method:: ESPNow.get_peer(mac) (ESP32 only)
Return information on a registered peer.
.. data:: Returns:
``(mac, lmk, channel, ifidx, encrypt)``: a tuple of the "peer
info" associated with the given *mac* address.
.. data:: Raises:
- ``OSError(num, "ESP_ERR_ESPNOW_NOT_INIT")`` if not initialised.
- ``OSError(num, "ESP_ERR_ESPNOW_NOT_FOUND")`` if *mac* is not
registered.
- ``ValueError()`` on invalid *mac* values.
.. method:: ESPNow.peer_count() (ESP32 only)
Return the number of registered peers:
- ``(peer_num, encrypt_num)``: where
- ``peer_num`` is the number of peers which are registered, and
- ``encrypt_num`` is the number of encrypted peers.
.. method:: ESPNow.get_peers() (ESP32 only)
Return the "peer info" parameters for all the registered peers (as a tuple
of tuples).
.. method:: ESPNow.mod_peer(mac, lmk, [channel], [ifidx], [encrypt]) (ESP32 only)
ESPNow.mod_peer(mac, 'param'=value, ...) (ESP32 only)
Modify the parameters of the peer associated with the provided *mac*
address. Parameters may be provided as positional or keyword arguments
(see `ESPNow.add_peer()`).
Callback Methods
----------------
.. method:: ESPNow.irq(callback) (ESP32 only)
Set a callback function to be called *as soon as possible* after a message has
been received from another ESPNow device. The callback function will be called
with the `ESPNow` instance object as an argument, eg: ::
def recv_cb(e):
print(e.irecv(0))
e.irq(recv_cb)
The `irq()<ESPNow.irq()>` callback method is an alternative method for
processing incoming espnow messages, especially if the data rate is moderate
and the device is *not too busy* but there are some caveats:
- The scheduler stack *can* overflow and callbacks will be missed if
packets are arriving at a sufficient rate or if other MicroPython components
(eg, bluetooth, machine.Pin.irq(), machine.timer, i2s, ...) are exercising
the scheduler stack. This method may be less reliable for dealing with
bursts of messages, or high throughput or on a device which is busy dealing
with other hardware operations.
- For more information on *scheduled* function callbacks see:
`micropython.schedule()<micropython.schedule>`.
Constants
---------
.. data:: espnow.MAX_DATA_LEN(=250)
espnow.KEY_LEN(=16)
espnow.ADDR_LEN(=6)
espnow.MAX_TOTAL_PEER_NUM(=20)
espnow.MAX_ENCRYPT_PEER_NUM(=6)
Exceptions
----------
If the underlying Espressif ESPNow software stack returns an error code,
the MicroPython ESPNow module will raise an ``OSError(errnum, errstring)``
exception where ``errstring`` is set to the name of one of the error codes
identified in the
`Espressif ESP-Now docs
<https://docs.espressif.com/projects/esp-idf/en/latest/
api-reference/network/esp_now.html#api-reference>`_. For example::
try:
e.send(peer, 'Hello')
except OSError as err:
if len(err.args) < 2:
raise err
if err.args[1] == 'ESP_ERR_ESPNOW_NOT_INIT':
e.active(True)
elif err.args[1] == 'ESP_ERR_ESPNOW_NOT_FOUND':
e.add_peer(peer)
elif err.args[1] == 'ESP_ERR_ESPNOW_IF':
network.WLAN(network.STA_IF).active(True)
else:
raise err
Wifi Signal Strength (RSSI) - (ESP32 only)
------------------------------------------
The ESPNow object maintains a **peer device table** which contains the signal
strength and timestamp of the last received message from all hosts. The **peer
device table** can be accessed using `ESPNow.peers_table` and can be used to
track device proximity and identify *nearest neighbours* in a network of peer
devices. This feature is **not** available on ESP8266 devices.
.. data:: ESPNow.peers_table
A reference to the **peer device table**: a dict of known peer devices
and rssi values::
{peer: [rssi, time_ms], ...}
where:
- ``peer`` is the peer MAC address (as `bytes`);
- ``rssi`` is the wifi signal strength in dBm (-127 to 0) of the last
message received from the peer; and
- ``time_ms`` is the time the message was received (in milliseconds since
system boot - wraps every 12 days).
Example::
>>> e.peers_table
{b'\xaa\xaa\xaa\xaa\xaa\xaa': [-31, 18372],
b'\xbb\xbb\xbb\xbb\xbb\xbb': [-43, 12541]}
**Note**: the ``mac`` addresses returned by `recv()` are references to
the ``peer`` key values in the **peer device table**.
**Note**: RSSI and timestamp values in the device table are updated only
when the message is read by the application.
Supporting asyncio
------------------
A supplementary module (`aioespnow`) is available to provide
:doc:`asyncio<uasyncio>` support.
**Note:** Asyncio support is available on all ESP32 targets as well as those
ESP8266 boards which include the asyncio module (ie. ESP8266 devices with at
least 2MB flash memory).
A small async server example::
import network
import aioespnow
import uasyncio as asyncio
# A WLAN interface must be active to send()/recv()
network.WLAN(network.STA_IF).active(True)
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
e.active(True)
peer = b'\xbb\xbb\xbb\xbb\xbb\xbb'
e.add_peer(peer)
# Send a periodic ping to a peer
async def heartbeat(e, peer, period=30):
while True:
if not await e.asend(peer, b'ping'):
print("Heartbeat: peer not responding:", peer)
else:
print("Heartbeat: ping", peer)
await asyncio.sleep(period)
# Echo any received messages back to the sender
async def echo_server(e):
async for mac, msg in e:
print("Echo:", msg)
try:
await e.asend(mac, msg)
except OSError as err:
if len(err.args) > 1 and err.args[1] == 'ESP_ERR_ESPNOW_NOT_FOUND':
e.add_peer(mac)
await e.asend(mac, msg)
async def main(e, peer, timeout, period):
asyncio.create_task(heartbeat(e, peer, period))
asyncio.create_task(echo_server(e))
await asyncio.sleep(timeout)
asyncio.run(main(e, peer, 120, 10))
.. module:: aioespnow
:synopsis: ESP-NOW :doc:`uasyncio` support
.. class:: AIOESPNow()
The `AIOESPNow` class inherits all the methods of `ESPNow<espnow.ESPNow>`
and extends the interface with the following async methods.
.. method:: async AIOESPNow.arecv()
Asyncio support for `ESPNow.recv()`. Note that this method does not take a
timeout value as argument.
.. method:: async AIOESPNow.airecv()
Asyncio support for `ESPNow.irecv()`. Note that this method does not take a
timeout value as argument.
.. method:: async AIOESPNow.asend(mac, msg, sync=True)
async AIOESPNow.asend(msg)
Asyncio support for `ESPNow.send()`.
.. method:: AIOESPNow._aiter__() / async AIOESPNow.__anext__()
`AIOESPNow` also supports reading incoming messages by asynchronous
iteration using ``async for``; eg::
e = AIOESPNow()
e.active(True)
async def recv_till_halt(e):
async for mac, msg in e:
print(mac, msg)
if msg == b'halt':
break
asyncio.run(recv_till_halt(e))
Broadcast and Multicast
-----------------------
All active ESP-Now clients will receive messages sent to their MAC address and
all devices (**except ESP8266 devices**) will also receive messages sent to the
*broadcast* MAC address (``b'\xff\xff\xff\xff\xff\xff'``) or any multicast
MAC address.
All ESP-Now devices (including ESP8266 devices) can also send messages to the
broadcast MAC address or any multicast MAC address.
To `send()<ESPNow.send()>` a broadcast message, the broadcast (or
multicast) MAC address must first be registered using
`add_peer()<ESPNow.add_peer()>`. `send()<ESPNow.send()>` will always return
``True`` for broadcasts, regardless of whether any devices receive the
message. It is not permitted to encrypt messages sent to the broadcast
address or any multicast address.
**Note**: `ESPNow.send(None, msg)<ESPNow.send()>` will send to all registered
peers *except* the broadcast address. To send a broadcast or multicast
message, you must specify the broadcast (or multicast) MAC address as the
peer. For example::
bcast = b'\xff' * 6
e.add_peer(bcast)
e.send(bcast, "Hello World!")
ESPNow and Wifi Operation
-------------------------
ESPNow messages may be sent and received on any `active()<network.WLAN.active>`
`WLAN<network.WLAN()>` interface (``network.STA_IF`` or ``network.AP_IF``), even
if that interface is also connected to a wifi network or configured as an access
point. When an ESP32 or ESP8266 device connects to a Wifi Access Point (see
`ESP32 Quickref <../esp32/quickref.html#networking>`__) the following things
happen which affect ESPNow communications:
1. Wifi Power-saving Mode is automatically activated and
2. The radio on the esp device changes wifi ``channel`` to match the channel
used by the Access Point.
**Wifi Power-saving Mode:** (see `Espressif Docs <https://docs.espressif.com/
projects/esp-idf/en/latest/esp32/api-guides/
wifi.html#esp32-wi-fi-power-saving-mode>`_) The power saving mode causes the
device to turn off the radio periodically (typically for hundreds of
milliseconds), making it unreliable in receiving ESPNow messages. This can be
resolved by either of:
1. Turning on the AP_IF interface, which will disable the power saving mode.
However, the device will then be advertising an active wifi access point.
- You **may** also choose to send your messages via the AP_IF interface, but
this is not necessary.
- ESP8266 peers must send messages to this AP_IF interface (see below).
2. Configuring ESPNow clients to retry sending messages.
**Receiving messages from an ESP8266 device:** Strangely, an ESP32 device
connected to a wifi network using method 1 or 2 above, will receive ESP-Now
messages sent to the STA_IF MAC address from another ESP32 device, but will
**reject** messages from an ESP8266 device!!!. To receive messages from an
ESP8266 device, the AP_IF interface must be set to ``active(True)`` **and**
messages must be sent to the AP_IF MAC address.
**Managing wifi channels:** Any other espnow devices wishing to communicate with
a device which is also connected to a Wifi Access Point MUST use the same
channel. A common scenario is where one espnow device is connected to a wifi
router and acts as a proxy for messages from a group of sensors connected via
espnow:
**Proxy:** ::
import network, time, espnow
sta, ap = wifi_reset() # Reset wifi to AP off, STA on and disconnected
sta.connect('myssid', 'mypassword')
while not sta.isconnected(): # Wait until connected...
time.sleep(0.1)
ap.active(True) # Disable power-saving mode
# Print the wifi channel used AFTER finished connecting to access point
print("Proxy running on channel:", sta.config("channel"))
e = espnow.ESPNow(); e.active(True)
for peer, msg in e:
# Receive espnow messages and forward them to MQTT broker over wifi
**Sensor:** ::
import network, espnow
sta, ap = wifi_reset() # Reset wifi to AP off, STA on and disconnected
sta.config(channel=6) # Change to the channel used by the proxy above.
peer = b'0\xaa\xaa\xaa\xaa\xaa' # MAC address of proxy
e = espnow.ESPNow(); e.active(True);
e.add_peer(peer)
while True:
msg = read_sensor()
e.send(peer, msg)
time.sleep(1)
Other issues to take care with when using ESPNow with wifi are:
- **Set WIFI to known state on startup:** MicroPython does not reset the wifi
peripheral after a soft reset. This can lead to unexpected behaviour. To
guarantee the wifi is reset to a known state after a soft reset make sure you
deactivate the STA_IF and AP_IF before setting them to the desired state at
startup, eg.::
import network, time
def wifi_reset(): # Reset wifi to AP_IF off, STA_IF on and disconnected
sta = network.WLAN(network.STA_IF); sta.active(False)
ap = network.WLAN(network.AP_IF); ap.active(False)
sta.active(True)
while not sta.active():
time.sleep(0.1)
sta.disconnect() # For ESP8266
while sta.isconnected():
time.sleep(0.1)
return sta, ap
sta, ap = wifi_reset()
Remember that a soft reset occurs every time you connect to the device REPL
and when you type ``ctrl-D``.
- **STA_IF and AP_IF always operate on the same channel:** the AP_IF will change
channel when you connect to a wifi network; regardless of the channel you set
for the AP_IF (see `Attention Note 3
<https://docs.espressif.com/
projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html
#_CPPv419esp_wifi_set_config16wifi_interface_tP13wifi_config_t>`_
). After all, there is really only one wifi radio on the device, which is
shared by the STA_IF and AP_IF virtual devices.
- **Disable automatic channel assignment on your wifi router:** If the wifi
router for your wifi network is configured to automatically assign the wifi
channel, it may change the channel for the network if it detects interference
from other wifi routers. When this occurs, the ESP devices connected to the
wifi network will also change channels to match the router, but other
ESPNow-only devices will remain on the previous channel and communication will
be lost. To mitigate this, either set your wifi router to use a fixed wifi
channel or configure your devices to re-scan the wifi channels if they are
unable to find their expected peers on the current channel.
- **MicroPython re-scans wifi channels when trying to reconnect:** If the esp
device is connected to a Wifi Access Point that goes down, MicroPython will
automatically start scanning channels in an attempt to reconnect to the
Access Point. This means espnow messages will be lost while scanning for the
AP. This can be disabled by ``sta.config(reconnects=0)``, which will also
disable the automatic reconnection after losing connection.
- Some versions of the ESP IDF only permit sending ESPNow packets from the
STA_IF interface to peers which have been registered on the same wifi
channel as the STA_IF::
ESPNOW: Peer channel is not equal to the home channel, send fail!
ESPNow and Sleep Modes
----------------------
The `machine.lightsleep([time_ms])<machine.lightsleep>` and
`machine.deepsleep([time_ms])<machine.deepsleep>` functions can be used to put
the ESP32 and peripherals (including the WiFi and Bluetooth radios) to sleep.
This is useful in many applications to conserve battery power. However,
applications must disable the WLAN peripheral (using
`active(False)<network.WLAN.active>`) before entering light or deep sleep (see
`Sleep Modes <https://docs.espressif.com/
projects/esp-idf/en/latest/esp32/api-reference/system/sleep_modes.html>`_).
Otherwise the WiFi radio may not be initialised properly after wake from
sleep. If the ``STA_IF`` and ``AP_IF`` interfaces have both been set
`active(True)<network.WLAN.active()>` then both interfaces should be set
`active(False)<network.WLAN.active()>` before entering any sleep mode.
**Example:** deep sleep::
import network, machine, espnow
sta, ap = wifi_reset() # Reset wifi to AP off, STA on and disconnected
peer = b'0\xaa\xaa\xaa\xaa\xaa' # MAC address of peer
e = espnow.ESPNow()
e.active(True)
e.add_peer(peer) # Register peer on STA_IF
print('Sending ping...')
if not e.send(peer, b'ping'):
print('Ping failed!')
e.active(False)
sta.active(False) # Disable the wifi before sleep
print('Going to sleep...')
machine.deepsleep(10000) # Sleep for 10 seconds then reboot
**Example:** light sleep::
import network, machine, espnow
sta, ap = wifi_reset() # Reset wifi to AP off, STA on and disconnected
sta.config(channel=6)
peer = b'0\xaa\xaa\xaa\xaa\xaa' # MAC address of peer
e = espnow.ESPNow()
e.active(True)
e.add_peer(peer) # Register peer on STA_IF
while True:
print('Sending ping...')
if not e.send(peer, b'ping'):
print('Ping failed!')
sta.active(False) # Disable the wifi before sleep
print('Going to sleep...')
machine.lightsleep(10000) # Sleep for 10 seconds
sta.active(True)
sta.config(channel=6) # Wifi loses config after lightsleep()

5
docs/library/index.rst

@ -155,6 +155,11 @@ The following libraries are specific to the ESP8266 and ESP32.
esp.rst
esp32.rst
.. toctree::
:maxdepth: 1
espnow.rst
Libraries specific to the RP2040
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1
ports/esp32/boards/manifest.py

@ -5,6 +5,7 @@ include("$(MPY_DIR)/extmod/uasyncio")
require("bundle-networking")
# Require some micropython-lib modules.
# require("aioespnow")
require("dht")
require("ds18x20")
require("neopixel")

9
ports/esp32/main.c

@ -67,6 +67,10 @@
#include "extmod/modbluetooth.h"
#endif
#if MICROPY_ESPNOW
#include "modespnow.h"
#endif
// MicroPython runs as a task under FreeRTOS
#define MP_TASK_PRIORITY (ESP_TASK_PRIO_MIN + 1)
#define MP_TASK_STACK_SIZE (16 * 1024)
@ -190,6 +194,11 @@ soft_reset_exit:
mp_bluetooth_deinit();
#endif
#if MICROPY_ESPNOW
espnow_deinit(mp_const_none);
MP_STATE_PORT(espnow_singleton) = NULL;
#endif
machine_timer_deinit_all();
#if MICROPY_PY_THREAD

1
ports/esp32/main/CMakeLists.txt

@ -84,6 +84,7 @@ set(MICROPY_SOURCE_PORT
${PROJECT_DIR}/mpthreadport.c
${PROJECT_DIR}/machine_rtc.c
${PROJECT_DIR}/machine_sdcard.c
${PROJECT_DIR}/modespnow.c
)
set(MICROPY_SOURCE_QSTR

884
ports/esp32/modespnow.c

@ -0,0 +1,884 @@
/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2017-2020 Nick Moore
* Copyright (c) 2018 shawwwn <shawwwn1@gmail.com>
* Copyright (c) 2020-2021 Glenn Moloney @glenn20
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "esp_now.h"
#include "esp_wifi.h"
#include "esp_wifi_types.h"
#include "py/runtime.h"
#include "py/mphal.h"
#include "py/mperrno.h"
#include "py/obj.h"
#include "py/objstr.h"
#include "py/objarray.h"
#include "py/stream.h"
#include "py/binary.h"
#include "py/ringbuf.h"
#include "mpconfigport.h"
#include "mphalport.h"
#include "modnetwork.h"
#include "modespnow.h"
#ifndef MICROPY_ESPNOW_RSSI
// Include code to track rssi of peers
#define MICROPY_ESPNOW_RSSI 1
#endif
#ifndef MICROPY_ESPNOW_EXTRA_PEER_METHODS
// Include mod_peer(),get_peer(),peer_count()
#define MICROPY_ESPNOW_EXTRA_PEER_METHODS 1
#endif
// Relies on gcc Variadic Macros and Statement Expressions
#define NEW_TUPLE(...) \
({mp_obj_t _z[] = {__VA_ARGS__}; mp_obj_new_tuple(MP_ARRAY_SIZE(_z), _z); })
static const uint8_t ESPNOW_MAGIC = 0x99;
// ESPNow packet format for the receive buffer.
// Use this for peeking at the header of the next packet in the buffer.
typedef struct {
uint8_t magic; // = ESPNOW_MAGIC
uint8_t msg_len; // Length of the message
#if MICROPY_ESPNOW_RSSI
uint32_t time_ms; // Timestamp (ms) when packet is received
int8_t rssi; // RSSI value (dBm) (-127 to 0)
#endif // MICROPY_ESPNOW_RSSI
} __attribute__((packed)) espnow_hdr_t;
typedef struct {
espnow_hdr_t hdr; // The header
uint8_t peer[6]; // Peer address
uint8_t msg[0]; // Message is up to 250 bytes
} __attribute__((packed)) espnow_pkt_t;
// The maximum length of an espnow packet (bytes)
static const size_t MAX_PACKET_LEN = (
(sizeof(espnow_pkt_t) + ESP_NOW_MAX_DATA_LEN));
// Enough for 2 full-size packets: 2 * (6 + 7 + 250) = 526 bytes
// Will allocate an additional 7 bytes for buffer overhead
static const size_t DEFAULT_RECV_BUFFER_SIZE = (2 * MAX_PACKET_LEN);
// Default timeout (millisec) to wait for incoming ESPNow messages (5 minutes).
static const size_t DEFAULT_RECV_TIMEOUT_MS = (5 * 60 * 1000);
// Time to wait (millisec) for responses from sent packets: (2 seconds).
static const size_t DEFAULT_SEND_TIMEOUT_MS = (2 * 1000);
// Number of milliseconds to wait for pending responses to sent packets.
// This is a fallback which should never be reached.
static const mp_uint_t PENDING_RESPONSES_TIMEOUT_MS = 100;
static const mp_uint_t PENDING_RESPONSES_BUSY_POLL_MS = 10;
// The data structure for the espnow_singleton.
typedef struct _esp_espnow_obj_t {
mp_obj_base_t base;
ringbuf_t *recv_buffer; // A buffer for received packets
size_t recv_buffer_size; // The size of the recv_buffer
mp_int_t recv_timeout_ms; // Timeout for recv()
volatile size_t rx_packets; // # of received packets
size_t dropped_rx_pkts; // # of dropped packets (buffer full)
size_t tx_packets; // # of sent packets
volatile size_t tx_responses; // # of sent packet responses received
volatile size_t tx_failures; // # of sent packet responses failed
size_t peer_count; // Cache the # of peers for send(sync=True)
mp_obj_t recv_cb; // Callback when a packet is received
mp_obj_t recv_cb_arg; // Argument passed to callback
#if MICROPY_ESPNOW_RSSI
mp_obj_t peers_table; // A dictionary of discovered peers
#endif // MICROPY_ESPNOW_RSSI
} esp_espnow_obj_t;
const mp_obj_type_t esp_espnow_type;
// ### Initialisation and Config functions
//
// Return a pointer to the ESPNow module singleton
// If state == INITIALISED check the device has been initialised.
// Raises OSError if not initialised and state == INITIALISED.
static esp_espnow_obj_t *_get_singleton() {
return MP_STATE_PORT(espnow_singleton);
}
static esp_espnow_obj_t *_get_singleton_initialised() {
esp_espnow_obj_t *self = _get_singleton();
// assert(self);
if (self->recv_buffer == NULL) {
// Throw an espnow not initialised error
check_esp_err(ESP_ERR_ESPNOW_NOT_INIT);
}
return self;
}
// Allocate and initialise the ESPNow module as a singleton.
// Returns the initialised espnow_singleton.
STATIC mp_obj_t espnow_make_new(const mp_obj_type_t *type, size_t n_args,
size_t n_kw, const mp_obj_t *all_args) {
// The espnow_singleton must be defined in MICROPY_PORT_ROOT_POINTERS
// (see mpconfigport.h) to prevent memory allocated here from being
// garbage collected.
// NOTE: on soft reset the espnow_singleton MUST be set to NULL and the
// ESP-NOW functions de-initialised (see main.c).
esp_espnow_obj_t *self = MP_STATE_PORT(espnow_singleton);
if (self != NULL) {
return self;
}
self = m_new_obj(esp_espnow_obj_t);
self->base.type = &esp_espnow_type;
self->recv_buffer_size = DEFAULT_RECV_BUFFER_SIZE;
self->recv_timeout_ms = DEFAULT_RECV_TIMEOUT_MS;
self->recv_buffer = NULL; // Buffer is allocated in espnow_init()
self->recv_cb = mp_const_none;
#if MICROPY_ESPNOW_RSSI
self->peers_table = mp_obj_new_dict(0);
// Prevent user code modifying the dict
mp_obj_dict_get_map(self->peers_table)->is_fixed = 1;
#endif // MICROPY_ESPNOW_RSSI
// Set the global singleton pointer for the espnow protocol.
MP_STATE_PORT(espnow_singleton) = self;
return self;
}
// Forward declare the send and recv ESPNow callbacks
STATIC void send_cb(const uint8_t *mac_addr, esp_now_send_status_t status);
STATIC void recv_cb(const uint8_t *mac_addr, const uint8_t *data, int len);
// ESPNow.init(): Initialise the data buffers and ESP-NOW functions.
// Initialise the Espressif ESPNOW software stack, register callbacks and
// allocate the recv data buffers.
// Returns None.
static mp_obj_t espnow_init(mp_obj_t _) {
esp_espnow_obj_t *self = _get_singleton();
if (self->recv_buffer == NULL) { // Already initialised
self->recv_buffer = m_new_obj(ringbuf_t);
ringbuf_alloc(self->recv_buffer, self->recv_buffer_size);
esp_initialise_wifi(); // Call the wifi init code in network_wlan.c
check_esp_err(esp_now_init());
check_esp_err(esp_now_register_recv_cb(recv_cb));
check_esp_err(esp_now_register_send_cb(send_cb));
}
return mp_const_none;
}
// ESPNow.deinit(): De-initialise the ESPNOW software stack, disable callbacks
// and deallocate the recv data buffers.
// Note: this function is called from main.c:mp_task() to cleanup before soft
// reset, so cannot be declared STATIC and must guard against self == NULL;.
mp_obj_t espnow_deinit(mp_obj_t _) {
esp_espnow_obj_t *self = _get_singleton();
if (self != NULL && self->recv_buffer != NULL) {
check_esp_err(esp_now_unregister_recv_cb());
check_esp_err(esp_now_unregister_send_cb());
check_esp_err(esp_now_deinit());
self->recv_buffer->buf = NULL;
self->recv_buffer = NULL;
self->peer_count = 0; // esp_now_deinit() removes all peers.
self->tx_packets = self->tx_responses;
}
return mp_const_none;
}
STATIC mp_obj_t espnow_active(size_t n_args, const mp_obj_t *args) {
esp_espnow_obj_t *self = _get_singleton();
if (n_args > 1) {
if (mp_obj_is_true(args[1])) {
espnow_init(self);
} else {
espnow_deinit(self);
}
}
return self->recv_buffer != NULL ? mp_const_true : mp_const_false;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_active_obj, 1, 2, espnow_active);
// ESPNow.config(['param'|param=value, ..])
// Get or set configuration values. Supported config params:
// buffer: size of buffer for rx packets (default=514 bytes)
// timeout: Default read timeout (default=300,000 milliseconds)
STATIC mp_obj_t espnow_config(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
esp_espnow_obj_t *self = _get_singleton();
enum { ARG_get, ARG_buffer, ARG_timeout_ms, ARG_rate };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
{ MP_QSTR_buffer, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} },
{ MP_QSTR_timeout_ms, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MIN} },
{ MP_QSTR_rate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} },
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args,
MP_ARRAY_SIZE(allowed_args), allowed_args, args);
if (args[ARG_buffer].u_int >= 0) {
self->recv_buffer_size = args[ARG_buffer].u_int;
}
if (args[ARG_timeout_ms].u_int != INT_MIN) {
self->recv_timeout_ms = args[ARG_timeout_ms].u_int;
}
if (args[ARG_rate].u_int >= 0) {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 0)
esp_initialise_wifi(); // Call the wifi init code in network_wlan.c
check_esp_err(esp_wifi_config_espnow_rate(ESP_IF_WIFI_STA, args[ARG_rate].u_int));
check_esp_err(esp_wifi_config_espnow_rate(ESP_IF_WIFI_AP, args[ARG_rate].u_int));
#else
mp_raise_ValueError(MP_ERROR_TEXT("rate option not supported"));
#endif
}
if (args[ARG_get].u_obj == MP_OBJ_NULL) {
return mp_const_none;
}
#define QS(x) (uintptr_t)MP_OBJ_NEW_QSTR(x)
// Return the value of the requested parameter
uintptr_t name = (uintptr_t)args[ARG_get].u_obj;
if (name == QS(MP_QSTR_buffer)) {
return mp_obj_new_int(self->recv_buffer_size);
} else if (name == QS(MP_QSTR_timeout_ms)) {
return mp_obj_new_int(self->recv_timeout_ms);
} else {
mp_raise_ValueError(MP_ERROR_TEXT("unknown config param"));
}
#undef QS
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(espnow_config_obj, 1, espnow_config);
// ESPNow.irq(recv_cb)
// Set callback function to be invoked when a message is received.
STATIC mp_obj_t espnow_irq(size_t n_args, const mp_obj_t *args) {
esp_espnow_obj_t *self = _get_singleton();
mp_obj_t recv_cb = args[1];
if (recv_cb != mp_const_none && !mp_obj_is_callable(recv_cb)) {
mp_raise_ValueError(MP_ERROR_TEXT("invalid handler"));
}
self->recv_cb = recv_cb;
self->recv_cb_arg = (n_args > 2) ? args[2] : mp_const_none;
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_irq_obj, 2, 3, espnow_irq);
// ESPnow.stats(): Provide some useful stats.
// Returns a tuple of:
// (tx_pkts, tx_responses, tx_failures, rx_pkts, dropped_rx_pkts)
STATIC mp_obj_t espnow_stats(mp_obj_t _) {
const esp_espnow_obj_t *self = _get_singleton();
return NEW_TUPLE(
mp_obj_new_int(self->tx_packets),
mp_obj_new_int(self->tx_responses),
mp_obj_new_int(self->tx_failures),
mp_obj_new_int(self->rx_packets),
mp_obj_new_int(self->dropped_rx_pkts));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(espnow_stats_obj, espnow_stats);
#if MICROPY_ESPNOW_RSSI
// ### Maintaining the peer table and reading RSSI values
//
// We maintain a peers table for several reasons, to:
// - support monitoring the RSSI values for all peers; and
// - to return unique bytestrings for each peer which supports more efficient
// application memory usage and peer handling.
// Get the RSSI value from the wifi packet header
static inline int8_t _get_rssi_from_wifi_pkt(const uint8_t *msg) {
// Warning: Secret magic to get the rssi from the wifi packet header
// See espnow.c:espnow_recv_cb() at https://github.com/espressif/esp-now/
// In the wifi packet the msg comes after a wifi_promiscuous_pkt_t
// and a espnow_frame_format_t.
// Backtrack to get a pointer to the wifi_promiscuous_pkt_t.
static const size_t sizeof_espnow_frame_format = 39;
wifi_promiscuous_pkt_t *wifi_pkt =
(wifi_promiscuous_pkt_t *)(msg - sizeof_espnow_frame_format -
sizeof(wifi_promiscuous_pkt_t));
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 2, 0)
return wifi_pkt->rx_ctrl.rssi - 100; // Offset rssi for IDF 4.0.2
#else
return wifi_pkt->rx_ctrl.rssi;
#endif
}
// Lookup a peer in the peers table and return a reference to the item in the
// peers_table. Add peer to the table if it is not found (may alloc memory).
// Will not return NULL.
static mp_map_elem_t *_lookup_add_peer(esp_espnow_obj_t *self, const uint8_t *peer) {
// We do not want to allocate any new memory in the case that the peer
// already exists in the peers_table (which is almost all the time).
// So, we use a byte string on the stack and look that up in the dict.
mp_map_t *map = mp_obj_dict_get_map(self->peers_table);
mp_obj_str_t peer_obj = {{&mp_type_bytes}, 0, ESP_NOW_ETH_ALEN, peer};
mp_map_elem_t *item = mp_map_lookup(map, &peer_obj, MP_MAP_LOOKUP);
if (item == NULL) {
// If not found, add the peer using a new bytestring
map->is_fixed = 0; // Allow to modify the dict
mp_obj_t new_peer = mp_obj_new_bytes(peer, ESP_NOW_ETH_ALEN);
item = mp_map_lookup(map, new_peer, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
item->value = mp_obj_new_list(2, NULL);
map->is_fixed = 1; // Relock the dict
}
return item;
}
// Update the peers table with the new rssi value from a received pkt and
// return a reference to the item in the peers_table.
static mp_map_elem_t *_update_rssi(const uint8_t *peer, int8_t rssi, uint32_t time_ms) {
esp_espnow_obj_t *self = _get_singleton_initialised();
// Lookup the peer in the device table
mp_map_elem_t *item = _lookup_add_peer(self, peer);
mp_obj_list_t *list = MP_OBJ_TO_PTR(item->value);
list->items[0] = MP_OBJ_NEW_SMALL_INT(rssi);
list->items[1] = mp_obj_new_int(time_ms);
return item;
}
#endif // MICROPY_ESPNOW_RSSI
// Return C pointer to byte memory string/bytes/bytearray in obj.
// Raise ValueError if the length does not match expected len.
static uint8_t *_get_bytes_len_rw(mp_obj_t obj, size_t len, mp_uint_t rw) {
mp_buffer_info_t bufinfo;
mp_get_buffer_raise(obj, &bufinfo, rw);
if (bufinfo.len != len) {
mp_raise_ValueError(MP_ERROR_TEXT("invalid buffer length"));
}
return (uint8_t *)bufinfo.buf;
}
static uint8_t *_get_bytes_len(mp_obj_t obj, size_t len) {
return _get_bytes_len_rw(obj, len, MP_BUFFER_READ);
}
static uint8_t *_get_bytes_len_w(mp_obj_t obj, size_t len) {
return _get_bytes_len_rw(obj, len, MP_BUFFER_WRITE);
}
// Return C pointer to the MAC address.
// Raise ValueError if mac_addr is wrong type or is not 6 bytes long.
static const uint8_t *_get_peer(mp_obj_t mac_addr) {
return mp_obj_is_true(mac_addr)
? _get_bytes_len(mac_addr, ESP_NOW_ETH_ALEN) : NULL;
}
// Copy data from the ring buffer - wait if buffer is empty up to timeout_ms
// 0: Success
// -1: Not enough data available to complete read (try again later)
// -2: Requested read is larger than buffer - will never succeed
static int ringbuf_get_bytes_wait(ringbuf_t *r, uint8_t *data, size_t len, mp_int_t timeout_ms) {
mp_uint_t start = mp_hal_ticks_ms();
int status = 0;
while (((status = ringbuf_get_bytes(r, data, len)) == -1)
&& (timeout_ms < 0 || (mp_uint_t)(mp_hal_ticks_ms() - start) < (mp_uint_t)timeout_ms)) {
MICROPY_EVENT_POLL_HOOK;
}
return status;
}
// ESPNow.recvinto(buffers[, timeout_ms]):
// Waits for an espnow message and copies the peer_addr and message into
// the buffers list.
// Arguments:
// buffers: (Optional) list of bytearrays to store return values.
// timeout_ms: (Optional) timeout in milliseconds (or None).
// Buffers should be a list: [bytearray(6), bytearray(250)]
// If buffers is 4 elements long, the rssi and timestamp values will be
// loaded into the 3rd and 4th elements.
// Default timeout is set with ESPNow.config(timeout=milliseconds).
// Return (None, None) on timeout.
STATIC mp_obj_t espnow_recvinto(size_t n_args, const mp_obj_t *args) {
esp_espnow_obj_t *self = _get_singleton_initialised();
mp_int_t timeout_ms = ((n_args > 2 && args[2] != mp_const_none)
? mp_obj_get_int(args[2]) : self->recv_timeout_ms);
mp_obj_list_t *list = MP_OBJ_TO_PTR(args[1]);
if (!mp_obj_is_type(list, &mp_type_list) || list->len < 2) {
mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recvinto(): Invalid argument"));
}
mp_obj_array_t *msg = MP_OBJ_TO_PTR(list->items[1]);
if (mp_obj_is_type(msg, &mp_type_bytearray)) {
msg->len += msg->free; // Make all the space in msg array available
msg->free = 0;
}
#if MICROPY_ESPNOW_RSSI
uint8_t peer_buf[ESP_NOW_ETH_ALEN];
#else
uint8_t *peer_buf = _get_bytes_len_w(list->items[0], ESP_NOW_ETH_ALEN);
#endif // MICROPY_ESPNOW_RSSI
uint8_t *msg_buf = _get_bytes_len_w(msg, ESP_NOW_MAX_DATA_LEN);
// Read the packet header from the incoming buffer
espnow_hdr_t hdr;
if (ringbuf_get_bytes_wait(self->recv_buffer, (uint8_t *)&hdr, sizeof(hdr), timeout_ms) < 0) {
return MP_OBJ_NEW_SMALL_INT(0); // Timeout waiting for packet
}
int msg_len = hdr.msg_len;
// Check the message packet header format and read the message data
if (hdr.magic != ESPNOW_MAGIC
|| msg_len > ESP_NOW_MAX_DATA_LEN
|| ringbuf_get_bytes(self->recv_buffer, peer_buf, ESP_NOW_ETH_ALEN) < 0
|| ringbuf_get_bytes(self->recv_buffer, msg_buf, msg_len) < 0) {
mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recv(): buffer error"));
}
if (mp_obj_is_type(msg, &mp_type_bytearray)) {
// Set the length of the message bytearray.
size_t size = msg->len + msg->free;
msg->len = msg_len;
msg->free = size - msg_len;
}
#if MICROPY_ESPNOW_RSSI
// Update rssi value in the peer device table
mp_map_elem_t *entry = _update_rssi(peer_buf, hdr.rssi, hdr.time_ms);
list->items[0] = entry->key; // Set first element of list to peer
if (list->len >= 4) {
list->items[2] = MP_OBJ_NEW_SMALL_INT(hdr.rssi);
list->items[3] = mp_obj_new_int(hdr.time_ms);
}
#endif // MICROPY_ESPNOW_RSSI
return MP_OBJ_NEW_SMALL_INT(msg_len);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_recvinto_obj, 2, 3, espnow_recvinto);
// Test if data is available to read from the buffers
STATIC mp_obj_t espnow_any(const mp_obj_t _) {
esp_espnow_obj_t *self = _get_singleton_initialised();
return ringbuf_avail(self->recv_buffer) ? mp_const_true : mp_const_false;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(espnow_any_obj, espnow_any);
// Used by espnow_send() for sends() with sync==True.
// Wait till all pending sent packet responses have been received.
// ie. self->tx_responses == self->tx_packets.
static void _wait_for_pending_responses(esp_espnow_obj_t *self) {
mp_uint_t start = mp_hal_ticks_ms();
mp_uint_t t;
while (self->tx_responses < self->tx_packets) {
if ((t = mp_hal_ticks_ms() - start) > PENDING_RESPONSES_TIMEOUT_MS) {
mp_raise_OSError(MP_ETIMEDOUT);
}
if (t > PENDING_RESPONSES_BUSY_POLL_MS) {
// After 10ms of busy waiting give other tasks a look in.
MICROPY_EVENT_POLL_HOOK;
}
}
}
// ESPNow.send(peer_addr, message, [sync (=true), size])
// ESPNow.send(message)
// Send a message to the peer's mac address. Optionally wait for a response.
// If peer_addr == None or any non-true value, send to all registered peers.
// If sync == True, wait for response after sending.
// If size is provided it should be the number of bytes in message to send().
// Returns:
// True if sync==False and message sent successfully.
// True if sync==True and message is received successfully by all recipients
// False if sync==True and message is not received by at least one recipient
// Raises: EAGAIN if the internal espnow buffers are full.
STATIC mp_obj_t espnow_send(size_t n_args, const mp_obj_t *args) {
esp_espnow_obj_t *self = _get_singleton_initialised();
// Check the various combinations of input arguments
const uint8_t *peer = (n_args > 2) ? _get_peer(args[1]) : NULL;
mp_obj_t msg = (n_args > 2) ? args[2] : (n_args == 2) ? args[1] : MP_OBJ_NULL;
bool sync = n_args <= 3 || args[3] == mp_const_none || mp_obj_is_true(args[3]);
// Get a pointer to the data buffer of the message
mp_buffer_info_t message;
mp_get_buffer_raise(msg, &message, MP_BUFFER_READ);
if (sync) {
// Flush out any pending responses.
// If the last call was sync==False there may be outstanding responses
// still to be received (possible many if we just had a burst of
// unsync send()s). We need to wait for all pending responses if this
// call has sync=True.
_wait_for_pending_responses(self);
}
int saved_failures = self->tx_failures;
// Send the packet - try, try again if internal esp-now buffers are full.
esp_err_t err;
mp_uint_t start = mp_hal_ticks_ms();
while ((ESP_ERR_ESPNOW_NO_MEM == (err = esp_now_send(peer, message.buf, message.len)))
&& (mp_uint_t)(mp_hal_ticks_ms() - start) < (mp_uint_t)DEFAULT_SEND_TIMEOUT_MS) {
MICROPY_EVENT_POLL_HOOK;
}
check_esp_err(err); // Will raise OSError if e != ESP_OK
// Increment the sent packet count. If peer_addr==NULL msg will be
// sent to all peers EXCEPT any broadcast or multicast addresses.
self->tx_packets += ((peer == NULL) ? self->peer_count : 1);
if (sync) {
// Wait for and tally all the expected responses from peers
_wait_for_pending_responses(self);
}
// Return False if sync and any peers did not respond.
return mp_obj_new_bool(!(sync && self->tx_failures != saved_failures));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_send_obj, 2, 4, espnow_send);
// ### The ESP_Now send and recv callback routines
//
// Callback triggered when a sent packet is acknowledged by the peer (or not).
// Just count the number of responses and number of failures.
// These are used in the send() logic.
STATIC void send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) {
esp_espnow_obj_t *self = _get_singleton();
self->tx_responses++;
if (status != ESP_NOW_SEND_SUCCESS) {
self->tx_failures++;
}
}
// Callback triggered when an ESP-Now packet is received.
// Write the peer MAC address and the message into the recv_buffer as an
// ESPNow packet.
// If the buffer is full, drop the message and increment the dropped count.
// Schedules the user callback if one has been registered (ESPNow.config()).
STATIC void recv_cb(const uint8_t *mac_addr, const uint8_t *msg, int msg_len) {
esp_espnow_obj_t *self = _get_singleton();
ringbuf_t *buf = self->recv_buffer;
// TODO: Test this works with ">".
if (sizeof(espnow_pkt_t) + msg_len >= ringbuf_free(buf)) {
self->dropped_rx_pkts++;
return;
}
espnow_hdr_t header;
header.magic = ESPNOW_MAGIC;
header.msg_len = msg_len;
#if MICROPY_ESPNOW_RSSI
header.rssi = _get_rssi_from_wifi_pkt(msg);
header.time_ms = mp_hal_ticks_ms();
#endif // MICROPY_ESPNOW_RSSI
ringbuf_put_bytes(buf, (uint8_t *)&header, sizeof(header));
ringbuf_put_bytes(buf, mac_addr, ESP_NOW_ETH_ALEN);
ringbuf_put_bytes(buf, msg, msg_len);
self->rx_packets++;
if (self->recv_cb != mp_const_none) {
mp_sched_schedule(self->recv_cb, self->recv_cb_arg);
}
}
// ### Peer Management Functions
//
// Set the ESP-NOW Primary Master Key (pmk) (for encrypted communications).
// Raise OSError if ESP-NOW functions are not initialised.
// Raise ValueError if key is not a bytes-like object exactly 16 bytes long.
STATIC mp_obj_t espnow_set_pmk(mp_obj_t _, mp_obj_t key) {
check_esp_err(esp_now_set_pmk(_get_bytes_len(key, ESP_NOW_KEY_LEN)));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_set_pmk_obj, espnow_set_pmk);
// Common code for add_peer() and mod_peer() to process the args and kw_args:
// Raise ValueError if the LMK is not a bytes-like object of exactly 16 bytes.
// Raise TypeError if invalid keyword args or too many positional args.
// Return true if all args parsed correctly.
STATIC bool _update_peer_info(
esp_now_peer_info_t *peer, size_t n_args,
const mp_obj_t *pos_args, mp_map_t *kw_args) {
enum { ARG_lmk, ARG_channel, ARG_ifidx, ARG_encrypt };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_lmk, MP_ARG_OBJ, {.u_obj = mp_const_none} },
{ MP_QSTR_channel, MP_ARG_OBJ, {.u_obj = mp_const_none} },
{ MP_QSTR_ifidx, MP_ARG_OBJ, {.u_obj = mp_const_none} },
{ MP_QSTR_encrypt, MP_ARG_OBJ, {.u_obj = mp_const_none} },
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
if (args[ARG_lmk].u_obj != mp_const_none) {
mp_obj_t obj = args[ARG_lmk].u_obj;
peer->encrypt = mp_obj_is_true(obj);
if (peer->encrypt) {
// Key must be 16 bytes in length.
memcpy(peer->lmk, _get_bytes_len(obj, ESP_NOW_KEY_LEN), ESP_NOW_KEY_LEN);
}
}
if (args[ARG_channel].u_obj != mp_const_none) {
peer->channel = mp_obj_get_int(args[ARG_channel].u_obj);
}
if (args[ARG_ifidx].u_obj != mp_const_none) {
peer->ifidx = mp_obj_get_int(args[ARG_ifidx].u_obj);
}
if (args[ARG_encrypt].u_obj != mp_const_none) {
peer->encrypt = mp_obj_is_true(args[ARG_encrypt].u_obj);
}
return true;
}
// Update the cached peer count in self->peer_count;
// The peer_count ignores broadcast and multicast addresses and is used for the
// send() logic and is updated from add_peer(), mod_peer() and del_peer().
STATIC void _update_peer_count() {
esp_espnow_obj_t *self = _get_singleton_initialised();
esp_now_peer_info_t peer = {0};
bool from_head = true;
int count = 0;
// esp_now_fetch_peer() skips over any broadcast or multicast addresses
while (esp_now_fetch_peer(from_head, &peer) == ESP_OK) {
from_head = false;
if (++count >= ESP_NOW_MAX_TOTAL_PEER_NUM) {
break; // Should not happen
}
}
self->peer_count = count;
}
// ESPNow.add_peer(peer_mac, [lmk, [channel, [ifidx, [encrypt]]]]) or
// ESPNow.add_peer(peer_mac, [lmk=b'0123456789abcdef'|b''|None|False],
// [channel=1..11|0], [ifidx=0|1], [encrypt=True|False])
// Positional args set to None will be left at defaults.
// Raise OSError if ESPNow.init() has not been called.
// Raise ValueError if mac or LMK are not bytes-like objects or wrong length.
// Raise TypeError if invalid keyword args or too many positional args.
// Return None.
STATIC mp_obj_t espnow_add_peer(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args) {
esp_now_peer_info_t peer = {0};
memcpy(peer.peer_addr, _get_peer(args[1]), ESP_NOW_ETH_ALEN);
_update_peer_info(&peer, n_args - 2, args + 2, kw_args);
check_esp_err(esp_now_add_peer(&peer));
_update_peer_count();
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(espnow_add_peer_obj, 2, espnow_add_peer);
// ESPNow.del_peer(peer_mac): Unregister peer_mac.
// Raise OSError if ESPNow.init() has not been called.
// Raise ValueError if peer is not a bytes-like objects or wrong length.
// Return None.
STATIC mp_obj_t espnow_del_peer(mp_obj_t _, mp_obj_t peer) {
uint8_t peer_addr[ESP_NOW_ETH_ALEN];
memcpy(peer_addr, _get_peer(peer), ESP_NOW_ETH_ALEN);
check_esp_err(esp_now_del_peer(peer_addr));
_update_peer_count();
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_del_peer_obj, espnow_del_peer);
// Convert a peer_info struct to python tuple
// Used by espnow_get_peer() and espnow_get_peers()
static mp_obj_t _peer_info_to_tuple(const esp_now_peer_info_t *peer) {
return NEW_TUPLE(
mp_obj_new_bytes(peer->peer_addr, MP_ARRAY_SIZE(peer->peer_addr)),
mp_obj_new_bytes(peer->lmk, MP_ARRAY_SIZE(peer->lmk)),
mp_obj_new_int(peer->channel),
mp_obj_new_int(peer->ifidx),
(peer->encrypt) ? mp_const_true : mp_const_false);
}
// ESPNow.get_peers(): Fetch peer_info records for all registered ESPNow peers.
// Raise OSError if ESPNow.init() has not been called.
// Return a tuple of tuples:
// ((peer_addr, lmk, channel, ifidx, encrypt),
// (peer_addr, lmk, channel, ifidx, encrypt), ...)
STATIC mp_obj_t espnow_get_peers(mp_obj_t _) {
esp_espnow_obj_t *self = _get_singleton_initialised();
// Build and initialise the peer info tuple.
mp_obj_tuple_t *peerinfo_tuple = mp_obj_new_tuple(self->peer_count, NULL);
esp_now_peer_info_t peer = {0};
for (int i = 0; i < peerinfo_tuple->len; i++) {
int status = esp_now_fetch_peer((i == 0), &peer);
peerinfo_tuple->items[i] =
(status == ESP_OK ? _peer_info_to_tuple(&peer) : mp_const_none);
}
return peerinfo_tuple;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(espnow_get_peers_obj, espnow_get_peers);
#if MICROPY_ESPNOW_EXTRA_PEER_METHODS
// ESPNow.get_peer(peer_mac): Get the peer info for peer_mac as a tuple.
// Raise OSError if ESPNow.init() has not been called.
// Raise ValueError if mac or LMK are not bytes-like objects or wrong length.
// Return a tuple of (peer_addr, lmk, channel, ifidx, encrypt).
STATIC mp_obj_t espnow_get_peer(mp_obj_t _, mp_obj_t arg1) {
esp_now_peer_info_t peer = {0};
memcpy(peer.peer_addr, _get_peer(arg1), ESP_NOW_ETH_ALEN);
check_esp_err(esp_now_get_peer(peer.peer_addr, &peer));
return _peer_info_to_tuple(&peer);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_get_peer_obj, espnow_get_peer);
// ESPNow.mod_peer(peer_mac, [lmk, [channel, [ifidx, [encrypt]]]]) or
// ESPNow.mod_peer(peer_mac, [lmk=b'0123456789abcdef'|b''|None|False],
// [channel=1..11|0], [ifidx=0|1], [encrypt=True|False])
// Positional args set to None will be left at current values.
// Raise OSError if ESPNow.init() has not been called.
// Raise ValueError if mac or LMK are not bytes-like objects or wrong length.
// Raise TypeError if invalid keyword args or too many positional args.
// Return None.
STATIC mp_obj_t espnow_mod_peer(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args) {
esp_now_peer_info_t peer = {0};
memcpy(peer.peer_addr, _get_peer(args[1]), ESP_NOW_ETH_ALEN);
check_esp_err(esp_now_get_peer(peer.peer_addr, &peer));
_update_peer_info(&peer, n_args - 2, args + 2, kw_args);
check_esp_err(esp_now_mod_peer(&peer));
_update_peer_count();
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(espnow_mod_peer_obj, 2, espnow_mod_peer);
// ESPNow.espnow_peer_count(): Get the number of registered peers.
// Raise OSError if ESPNow.init() has not been called.
// Return a tuple of (num_total_peers, num_encrypted_peers).
STATIC mp_obj_t espnow_peer_count(mp_obj_t _) {
esp_now_peer_num_t peer_num = {0};
check_esp_err(esp_now_get_peer_num(&peer_num));
return NEW_TUPLE(
mp_obj_new_int(peer_num.total_num),
mp_obj_new_int(peer_num.encrypt_num));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(espnow_peer_count_obj, espnow_peer_count);
#endif
STATIC const mp_rom_map_elem_t esp_espnow_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_active), MP_ROM_PTR(&espnow_active_obj) },
{ MP_ROM_QSTR(MP_QSTR_config), MP_ROM_PTR(&espnow_config_obj) },
{ MP_ROM_QSTR(MP_QSTR_irq), MP_ROM_PTR(&espnow_irq_obj) },
{ MP_ROM_QSTR(MP_QSTR_stats), MP_ROM_PTR(&espnow_stats_obj) },
// Send and receive messages
{ MP_ROM_QSTR(MP_QSTR_recvinto), MP_ROM_PTR(&espnow_recvinto_obj) },
{ MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&espnow_send_obj) },
{ MP_ROM_QSTR(MP_QSTR_any), MP_ROM_PTR(&espnow_any_obj) },
// Peer management functions
{ MP_ROM_QSTR(MP_QSTR_set_pmk), MP_ROM_PTR(&espnow_set_pmk_obj) },
{ MP_ROM_QSTR(MP_QSTR_add_peer), MP_ROM_PTR(&espnow_add_peer_obj) },
{ MP_ROM_QSTR(MP_QSTR_del_peer), MP_ROM_PTR(&espnow_del_peer_obj) },
{ MP_ROM_QSTR(MP_QSTR_get_peers), MP_ROM_PTR(&espnow_get_peers_obj) },
#if MICROPY_ESPNOW_EXTRA_PEER_METHODS
{ MP_ROM_QSTR(MP_QSTR_mod_peer), MP_ROM_PTR(&espnow_mod_peer_obj) },
{ MP_ROM_QSTR(MP_QSTR_get_peer), MP_ROM_PTR(&espnow_get_peer_obj) },
{ MP_ROM_QSTR(MP_QSTR_peer_count), MP_ROM_PTR(&espnow_peer_count_obj) },
#endif // MICROPY_ESPNOW_EXTRA_PEER_METHODS
};
STATIC MP_DEFINE_CONST_DICT(esp_espnow_locals_dict, esp_espnow_locals_dict_table);
STATIC const mp_rom_map_elem_t espnow_globals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR__espnow) },
{ MP_ROM_QSTR(MP_QSTR_ESPNowBase), MP_ROM_PTR(&esp_espnow_type) },
{ MP_ROM_QSTR(MP_QSTR_MAX_DATA_LEN), MP_ROM_INT(ESP_NOW_MAX_DATA_LEN)},
{ MP_ROM_QSTR(MP_QSTR_ADDR_LEN), MP_ROM_INT(ESP_NOW_ETH_ALEN)},
{ MP_ROM_QSTR(MP_QSTR_KEY_LEN), MP_ROM_INT(ESP_NOW_KEY_LEN)},
{ MP_ROM_QSTR(MP_QSTR_MAX_TOTAL_PEER_NUM), MP_ROM_INT(ESP_NOW_MAX_TOTAL_PEER_NUM)},
{ MP_ROM_QSTR(MP_QSTR_MAX_ENCRYPT_PEER_NUM), MP_ROM_INT(ESP_NOW_MAX_ENCRYPT_PEER_NUM)},
};
STATIC MP_DEFINE_CONST_DICT(espnow_globals_dict, espnow_globals_dict_table);
// ### Dummy Buffer Protocol support
// ...so asyncio can poll.ipoll() on this device
// Support ioctl(MP_STREAM_POLL, ) for asyncio
STATIC mp_uint_t espnow_stream_ioctl(
mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) {
if (request != MP_STREAM_POLL) {
*errcode = MP_EINVAL;
return MP_STREAM_ERROR;
}
esp_espnow_obj_t *self = _get_singleton();
return (self->recv_buffer == NULL) ? 0 : // If not initialised
arg ^ (
// If no data in the buffer, unset the Read ready flag
((ringbuf_avail(self->recv_buffer) == 0) ? MP_STREAM_POLL_RD : 0) |
// If still waiting for responses, unset the Write ready flag
((self->tx_responses < self->tx_packets) ? MP_STREAM_POLL_WR : 0));
}
STATIC const mp_stream_p_t espnow_stream_p = {
.ioctl = espnow_stream_ioctl,
};
#if MICROPY_ESPNOW_RSSI
// Return reference to the dictionary of peers we have seen:
// {peer1: (rssi, time_sec), peer2: (rssi, time_msec), ...}
// where:
// peerX is a byte string containing the 6-byte mac address of the peer,
// rssi is the wifi signal strength from the last msg received
// (in dBm from -127 to 0)
// time_sec is the time in milliseconds since device last booted.
STATIC void espnow_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
esp_espnow_obj_t *self = _get_singleton();
if (dest[0] != MP_OBJ_NULL) { // Only allow "Load" operation
return;
}
if (attr == MP_QSTR_peers_table) {
dest[0] = self->peers_table;
return;
}
dest[1] = MP_OBJ_SENTINEL; // Attribute not found
}
#endif // MICROPY_ESPNOW_RSSI
MP_DEFINE_CONST_OBJ_TYPE(
esp_espnow_type,
MP_QSTR_ESPNowBase,
MP_TYPE_FLAG_NONE,
make_new, espnow_make_new,
#if MICROPY_ESPNOW_RSSI
attr, espnow_attr,
#endif // MICROPY_ESPNOW_RSSI
protocol, &espnow_stream_p,
locals_dict, &esp_espnow_locals_dict
);
const mp_obj_module_t mp_module_espnow = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&espnow_globals_dict,
};
MP_REGISTER_MODULE(MP_QSTR__espnow, mp_module_espnow);
MP_REGISTER_ROOT_POINTER(struct _esp_espnow_obj_t *espnow_singleton);

30
ports/esp32/modespnow.h

@ -0,0 +1,30 @@
/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2021 Glenn Moloney @glenn20
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "py/obj.h"
// Called from main.c:mp_task() to reset the espnow software stack
mp_obj_t espnow_deinit(mp_obj_t _);

1
ports/esp32/modnetwork.h

@ -63,5 +63,6 @@ static inline void esp_exceptions(esp_err_t e) {
void usocket_events_deinit(void);
void network_wlan_event_handler(system_event_t *event);
void esp_initialise_wifi(void);
#endif

30
ports/esp32/modules/espnow.py

@ -0,0 +1,30 @@
# espnow module for MicroPython on ESP32
# MIT license; Copyright (c) 2022 Glenn Moloney @glenn20
from _espnow import *
class ESPNow(ESPNowBase):
# Static buffers for alloc free receipt of messages with ESPNow.irecv().
_data = [None, bytearray(MAX_DATA_LEN)]
_none_tuple = (None, None)
def __init__(self):
super().__init__()
def irecv(self, timeout_ms=None):
n = self.recvinto(self._data, timeout_ms)
return self._data if n else self._none_tuple
def recv(self, timeout_ms=None):
n = self.recvinto(self._data, timeout_ms)
return [bytes(x) for x in self._data] if n else self._none_tuple
def irq(self, callback):
super().irq(callback, self)
def __iter__(self):
return self
def __next__(self):
return self.irecv() # Use alloc free irecv() method

3
ports/esp32/mpconfigport.h

@ -70,6 +70,9 @@
#define MICROPY_PY_THREAD_GIL_VM_DIVISOR (32)
// extended modules
#ifndef MICROPY_ESPNOW
#define MICROPY_ESPNOW (1)
#endif
#ifndef MICROPY_PY_BLUETOOTH
#define MICROPY_PY_BLUETOOTH (1)
#define MICROPY_PY_BLUETOOTH_USE_SYNC_EVENTS (1)

12
ports/esp32/network_wlan.c

@ -159,16 +159,20 @@ STATIC void require_if(mp_obj_t wlan_if, int if_no) {
}
}
STATIC mp_obj_t get_wlan(size_t n_args, const mp_obj_t *args) {
static int initialized = 0;
if (!initialized) {
void esp_initialise_wifi() {
static int wifi_initialized = 0;
if (!wifi_initialized) {
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_LOGD("modnetwork", "Initializing WiFi");
esp_exceptions(esp_wifi_init(&cfg));
esp_exceptions(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_LOGD("modnetwork", "Initialized");
initialized = 1;
wifi_initialized = 1;
}
}
STATIC mp_obj_t get_wlan(size_t n_args, const mp_obj_t *args) {
esp_initialise_wifi();
int idx = (n_args > 0) ? mp_obj_get_int(args[0]) : WIFI_IF_STA;
if (idx == WIFI_IF_STA) {

10
ports/esp8266/Makefile

@ -70,6 +70,11 @@ LD_FILES ?= boards/esp8266_2m.ld
LDFLAGS += -nostdlib -T $(LD_FILES) -Map=$(@:.elf=.map) --cref
LIBS += -L$(ESP_SDK)/lib -lmain -ljson -llwip_open -lpp -lnet80211 -lwpa -lphy -lnet80211
ifeq ($(MICROPY_ESPNOW),1)
CFLAGS += -DMICROPY_ESPNOW=1
LIBS += -lespnow
endif
LIBGCC_FILE_NAME = $(shell $(CC) $(CFLAGS) -print-libgcc-file-name)
LIBS += -L$(dir $(LIBGCC_FILE_NAME)) -lgcc
@ -113,6 +118,11 @@ SRC_C = \
hspi.c \
$(wildcard $(BOARD_DIR)/*.c) \
ifeq ($(MICROPY_ESPNOW),1)
SRC_C += \
modespnow.c
endif
LIB_SRC_C = $(addprefix lib/,\
libm/math.c \
libm/fmodf.c \

1
ports/esp8266/boards/GENERIC/mpconfigboard.mk

@ -1,5 +1,6 @@
LD_FILES = boards/esp8266_2m.ld
MICROPY_ESPNOW ?= 1
MICROPY_PY_BTREE ?= 1
MICROPY_VFS_FAT ?= 1
MICROPY_VFS_LFS2 ?= 1

1
ports/esp8266/boards/GENERIC_1M/mpconfigboard.mk

@ -1,4 +1,5 @@
LD_FILES = boards/esp8266_1m.ld
MICROPY_ESPNOW ?= 1
MICROPY_PY_BTREE ?= 1
MICROPY_VFS_LFS2 ?= 1

1
ports/esp8266/boards/esp8266_common.ld

@ -83,6 +83,7 @@ SECTIONS
*libnet80211.a:(.literal.* .text.*)
*libwpa.a:(.literal.* .text.*)
*libwpa2.a:(.literal.* .text.*)
*libespnow.a:(.literal.* .text.*)
/* we put some specific text in this section */

1
ports/esp8266/boards/manifest.py

@ -1,4 +1,5 @@
freeze("$(PORT_DIR)/modules")
# require("aioespnow")
require("bundle-networking")
require("dht")
require("ds18x20")

8
ports/esp8266/main.c

@ -45,6 +45,10 @@
#include "gccollect.h"
#include "user_interface.h"
#if MICROPY_ESPNOW
#include "modespnow.h"
#endif
STATIC char heap[38 * 1024];
STATIC void mp_reset(void) {
@ -73,6 +77,10 @@ STATIC void mp_reset(void) {
mp_uos_dupterm_obj.fun.var(2, args);
}
#if MICROPY_ESPNOW
espnow_deinit(mp_const_none);
#endif
#if MICROPY_MODULE_FROZEN
pyexec_frozen_module("_boot.py", false);
pyexec_file_if_exists("boot.py");

507
ports/esp8266/modespnow.c

@ -0,0 +1,507 @@
/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2017-2020 Nick Moore
* Copyright (c) 2018 shawwwn <shawwwn1@gmail.com>
* Copyright (c) 2020-2021 Glenn Moloney @glenn20
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "py/runtime.h"
#if MICROPY_ESPNOW
#include "c_types.h"
#include "espnow.h"
#include "py/mphal.h"
#include "py/mperrno.h"
#include "py/qstr.h"
#include "py/objstr.h"
#include "py/objarray.h"
#include "py/stream.h"
#include "py/binary.h"
#include "py/ringbuf.h"
#include "mpconfigport.h"
#include "modespnow.h"
// For the esp8266
#define ESP_NOW_MAX_DATA_LEN (250)
#define ESP_NOW_KEY_LEN (16)
#define ESP_NOW_ETH_ALEN (6)
#define ESP_NOW_SEND_SUCCESS (0)
#define ESP_ERR_ESPNOW_NO_MEM (-77777)
#define ESP_OK (0)
#define ESP_NOW_MAX_TOTAL_PEER_NUM (20)
#define ESP_NOW_MAX_ENCRYPT_PEER_NUM (6)
#define ESP_ERR_ESPNOW_NOT_INIT (0x300 + 100 + 1)
typedef int esp_err_t;
static const uint8_t ESPNOW_MAGIC = 0x99;
// Use this for peeking at the header of the next packet in the buffer.
typedef struct {
uint8_t magic; // = ESPNOW_MAGIC
uint8_t msg_len; // Length of the message
} __attribute__((packed)) espnow_hdr_t;
// ESPNow packet format for the receive buffer.
typedef struct {
espnow_hdr_t hdr; // The header
uint8_t peer[6]; // Peer address
uint8_t msg[0]; // Message is up to 250 bytes
} __attribute__((packed)) espnow_pkt_t;
// The maximum length of an espnow packet (bytes)
static const size_t MAX_PACKET_LEN = (
sizeof(espnow_pkt_t) + ESP_NOW_MAX_DATA_LEN);
// Enough for 2 full-size packets: 2 * (6 + 2 + 250) = 516 bytes
// Will allocate an additional 7 bytes for buffer overhead
#define DEFAULT_RECV_BUFFER_SIZE \
(2 * (sizeof(espnow_pkt_t) + ESP_NOW_MAX_DATA_LEN))
// Default timeout (millisec) to wait for incoming ESPNow messages (5 minutes).
#define DEFAULT_RECV_TIMEOUT_MS (5 * 60 * 1000)
// Number of milliseconds to wait for pending responses to sent packets.
// This is a fallback which should never be reached.
#define PENDING_RESPONSES_TIMEOUT_MS 100
// The data structure for the espnow_singleton.
typedef struct _esp_espnow_obj_t {
mp_obj_base_t base;
ringbuf_t *recv_buffer; // A buffer for received packets
size_t recv_buffer_size; // Size of recv buffer
size_t recv_timeout_ms; // Timeout for irecv()
size_t tx_packets; // Count of sent packets
volatile size_t tx_responses; // # of sent packet responses received
volatile size_t tx_failures; // # of sent packet responses failed
} esp_espnow_obj_t;
// Initialised below.
const mp_obj_type_t esp_espnow_type;
static esp_espnow_obj_t espnow_singleton = {
.base.type = &esp_espnow_type,
.recv_buffer = NULL,
.recv_buffer_size = DEFAULT_RECV_BUFFER_SIZE,
.recv_timeout_ms = DEFAULT_RECV_TIMEOUT_MS,
};
// ### Initialisation and Config functions
//
static void check_esp_err(int e) {
if (e != 0) {
mp_raise_OSError(e);
}
}
// Return a pointer to the ESPNow module singleton
// If state == INITIALISED check the device has been initialised.
// Raises OSError if not initialised and state == INITIALISED.
static esp_espnow_obj_t *_get_singleton() {
return &espnow_singleton;
}
static esp_espnow_obj_t *_get_singleton_initialised() {
esp_espnow_obj_t *self = _get_singleton();
if (self->recv_buffer == NULL) {
// Throw an espnow not initialised error
check_esp_err(ESP_ERR_ESPNOW_NOT_INIT);
}
return self;
}
// Allocate and initialise the ESPNow module as a singleton.
// Returns the initialised espnow_singleton.
STATIC mp_obj_t espnow_make_new(const mp_obj_type_t *type, size_t n_args,
size_t n_kw, const mp_obj_t *all_args) {
return _get_singleton();
}
// Forward declare the send and recv ESPNow callbacks
STATIC void send_cb(uint8_t *mac_addr, uint8_t status);
STATIC void recv_cb(uint8_t *mac_addr, uint8_t *data, uint8_t len);
// ESPNow.deinit(): De-initialise the ESPNOW software stack, disable callbacks
// and deallocate the recv data buffers.
// Note: this function is called from main.c:mp_task() to cleanup before soft
// reset, so cannot be declared STATIC and must guard against self == NULL;.
mp_obj_t espnow_deinit(mp_obj_t _) {
esp_espnow_obj_t *self = _get_singleton();
if (self->recv_buffer != NULL) {
// esp_now_unregister_recv_cb();
esp_now_deinit();
self->recv_buffer->buf = NULL;
self->recv_buffer = NULL;
self->tx_packets = self->tx_responses;
}
MP_STATE_PORT(espnow_buffer) = NULL;
return mp_const_none;
}
// ESPNow.active(): Initialise the data buffers and ESP-NOW functions.
// Initialise the Espressif ESPNOW software stack, register callbacks and
// allocate the recv data buffers.
// Returns True if interface is active, else False.
STATIC mp_obj_t espnow_active(size_t n_args, const mp_obj_t *args) {
esp_espnow_obj_t *self = args[0];
if (n_args > 1) {
if (mp_obj_is_true(args[1])) {
if (self->recv_buffer == NULL) { // Already initialised
self->recv_buffer = m_new_obj(ringbuf_t);
ringbuf_alloc(self->recv_buffer, self->recv_buffer_size);
MP_STATE_PORT(espnow_buffer) = self->recv_buffer;
esp_now_init();
esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
esp_now_register_recv_cb(recv_cb);
esp_now_register_send_cb(send_cb);
}
} else {
espnow_deinit(self);
}
}
return mp_obj_new_bool(self->recv_buffer != NULL);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_active_obj, 1, 2, espnow_active);
// ESPNow.config(): Initialise the data buffers and ESP-NOW functions.
// Initialise the Espressif ESPNOW software stack, register callbacks and
// allocate the recv data buffers.
// Returns True if interface is active, else False.
STATIC mp_obj_t espnow_config(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
esp_espnow_obj_t *self = _get_singleton();
enum { ARG_rxbuf, ARG_timeout_ms };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_rxbuf, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} },
{ MP_QSTR_timeout_ms, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} },
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args,
MP_ARRAY_SIZE(allowed_args), allowed_args, args);
if (args[ARG_rxbuf].u_int >= 0) {
self->recv_buffer_size = args[ARG_rxbuf].u_int;
}
if (args[ARG_timeout_ms].u_int >= 0) {
self->recv_timeout_ms = args[ARG_timeout_ms].u_int;
}
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(espnow_config_obj, 1, espnow_config);
// ### The ESP_Now send and recv callback routines
//
// Callback triggered when a sent packet is acknowledged by the peer (or not).
// Just count the number of responses and number of failures.
// These are used in the send()/write() logic.
STATIC void send_cb(uint8_t *mac_addr, uint8_t status) {
esp_espnow_obj_t *self = _get_singleton();
self->tx_responses++;
if (status != ESP_NOW_SEND_SUCCESS) {
self->tx_failures++;
}
}
// Callback triggered when an ESP-Now packet is received.
// Write the peer MAC address and the message into the recv_buffer as an
// ESPNow packet.
// If the buffer is full, drop the message and increment the dropped count.
// Schedules the user callback if one has been registered (ESPNow.config()).
STATIC void recv_cb(uint8_t *mac_addr, uint8_t *msg, uint8_t msg_len) {
esp_espnow_obj_t *self = _get_singleton();
ringbuf_t *buf = self->recv_buffer;
// TODO: Test this works with ">".
if (buf == NULL || sizeof(espnow_pkt_t) + msg_len >= ringbuf_free(buf)) {
return;
}
espnow_hdr_t header;
header.magic = ESPNOW_MAGIC;
header.msg_len = msg_len;
ringbuf_put_bytes(buf, (uint8_t *)&header, sizeof(header));
ringbuf_put_bytes(buf, mac_addr, ESP_NOW_ETH_ALEN);
ringbuf_put_bytes(buf, msg, msg_len);
}
// Return C pointer to byte memory string/bytes/bytearray in obj.
// Raise ValueError if the length does not match expected len.
static uint8_t *_get_bytes_len_rw(mp_obj_t obj, size_t len, mp_uint_t rw) {
mp_buffer_info_t bufinfo;
mp_get_buffer_raise(obj, &bufinfo, rw);
if (bufinfo.len != len) {
mp_raise_ValueError(MP_ERROR_TEXT("invalid buffer length"));
}
return (uint8_t *)bufinfo.buf;
}
static uint8_t *_get_bytes_len(mp_obj_t obj, size_t len) {
return _get_bytes_len_rw(obj, len, MP_BUFFER_READ);
}
static uint8_t *_get_bytes_len_w(mp_obj_t obj, size_t len) {
return _get_bytes_len_rw(obj, len, MP_BUFFER_WRITE);
}
// ### Handling espnow packets in the recv buffer
//
// Copy data from the ring buffer - wait if buffer is empty up to timeout_ms
// 0: Success
// -1: Not enough data available to complete read (try again later)
// -2: Requested read is larger than buffer - will never succeed
static int ringbuf_get_bytes_wait(ringbuf_t *r, uint8_t *data, size_t len, mp_int_t timeout_ms) {
mp_uint_t start = mp_hal_ticks_ms();
int status = 0;
while (((status = ringbuf_get_bytes(r, data, len)) == -1)
&& (timeout_ms < 0 || (mp_uint_t)(mp_hal_ticks_ms() - start) < (mp_uint_t)timeout_ms)) {
MICROPY_EVENT_POLL_HOOK;
}
return status;
}
// ESPNow.recvinto([timeout_ms, []]):
// Returns a list of byte strings: (peer_addr, message) where peer_addr is
// the MAC address of the sending peer.
// Arguments:
// timeout_ms: timeout in milliseconds (or None).
// buffers: list of bytearrays to store values: [peer, message].
// Default timeout is set with ESPNow.config(timeout=milliseconds).
// Return (None, None) on timeout.
STATIC mp_obj_t espnow_recvinto(size_t n_args, const mp_obj_t *args) {
esp_espnow_obj_t *self = _get_singleton_initialised();
size_t timeout_ms = ((n_args > 2 && args[2] != mp_const_none)
? mp_obj_get_int(args[2]) : self->recv_timeout_ms);
mp_obj_list_t *list = MP_OBJ_TO_PTR(args[1]);
if (!mp_obj_is_type(list, &mp_type_list) || list->len < 2) {
mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recvinto(): Invalid argument"));
}
mp_obj_array_t *msg = MP_OBJ_TO_PTR(list->items[1]);
size_t msg_size = msg->len + msg->free;
if (mp_obj_is_type(msg, &mp_type_bytearray)) {
msg->len = msg_size; // Make all the space in msg array available
msg->free = 0;
}
uint8_t *peer_buf = _get_bytes_len_w(list->items[0], ESP_NOW_ETH_ALEN);
uint8_t *msg_buf = _get_bytes_len_w(msg, ESP_NOW_MAX_DATA_LEN);
// Read the packet header from the incoming buffer
espnow_hdr_t hdr;
if (ringbuf_get_bytes_wait(self->recv_buffer, (uint8_t *)&hdr, sizeof(hdr), timeout_ms) < 0) {
return MP_OBJ_NEW_SMALL_INT(0); // Timeout waiting for packet
}
int msg_len = hdr.msg_len;
// Check the message packet header format and read the message data
if (hdr.magic != ESPNOW_MAGIC
|| msg_len > ESP_NOW_MAX_DATA_LEN
|| ringbuf_get_bytes(self->recv_buffer, peer_buf, ESP_NOW_ETH_ALEN) < 0
|| ringbuf_get_bytes(self->recv_buffer, msg_buf, msg_len) < 0) {
mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recv(): buffer error"));
}
if (mp_obj_is_type(msg, &mp_type_bytearray)) {
// Set the length of the message bytearray.
msg->len = msg_len;
msg->free = msg_size - msg_len;
}
return MP_OBJ_NEW_SMALL_INT(msg_len);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_recvinto_obj, 2, 3, espnow_recvinto);
// Used by espnow_send() for sends() with sync==True.
// Wait till all pending sent packet responses have been received.
// ie. self->tx_responses == self->tx_packets.
// Return the number of responses where status != ESP_NOW_SEND_SUCCESS.
static void _wait_for_pending_responses(esp_espnow_obj_t *self) {
for (int i = 0; i < PENDING_RESPONSES_TIMEOUT_MS; i++) {
if (self->tx_responses >= self->tx_packets) {
return;
}
mp_hal_delay_ms(1); // Allow other tasks to run
}
// Note: the loop timeout is just a fallback - in normal operation
// we should never reach that timeout.
}
// ESPNow.send(peer_addr, message, [sync (=true)])
// ESPNow.send(message)
// Send a message to the peer's mac address. Optionally wait for a response.
// If sync == True, wait for response after sending.
// Returns:
// True if sync==False and message sent successfully.
// True if sync==True and message is received successfully by all recipients
// False if sync==True and message is not received by at least one recipient
// Raises: EAGAIN if the internal espnow buffers are full.
STATIC mp_obj_t espnow_send(size_t n_args, const mp_obj_t *args) {
esp_espnow_obj_t *self = _get_singleton_initialised();
bool sync = n_args <= 3 || args[3] == mp_const_none || mp_obj_is_true(args[3]);
// Get a pointer to the buffer of obj
mp_buffer_info_t message;
mp_get_buffer_raise(args[2], &message, MP_BUFFER_READ);
// Bugfix: esp_now_send() generates a panic if message buffer points
// to an address in ROM (eg. a statically interned QSTR).
// Fix: if message is not in gc pool, copy to a temp buffer.
static char temp[ESP_NOW_MAX_DATA_LEN]; // Static to save code space
byte *p = (byte *)message.buf;
// if (p < MP_STATE_MEM(area.gc_pool_start) || MP_STATE_MEM(area.gc_pool_end) < p) {
if (MP_STATE_MEM(area.gc_pool_end) < p) {
// If buffer is not in GC pool copy from ROM to stack
memcpy(temp, message.buf, message.len);
message.buf = temp;
}
if (sync) {
// If the last call was sync==False there may be outstanding responses.
// We need to wait for all pending responses if this call has sync=True.
_wait_for_pending_responses(self);
}
int saved_failures = self->tx_failures;
check_esp_err(
esp_now_send(_get_bytes_len(args[1], ESP_NOW_ETH_ALEN), message.buf, message.len));
self->tx_packets++;
if (sync) {
// Wait for message to be received by peer
_wait_for_pending_responses(self);
}
// Return False if sync and any peers did not respond.
return mp_obj_new_bool(!(sync && self->tx_failures != saved_failures));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_send_obj, 3, 4, espnow_send);
// ### Peer Management Functions
//
// Set the ESP-NOW Primary Master Key (pmk) (for encrypted communications).
// Raise OSError if not initialised.
// Raise ValueError if key is not a bytes-like object exactly 16 bytes long.
STATIC mp_obj_t espnow_set_pmk(mp_obj_t _, mp_obj_t key) {
check_esp_err(esp_now_set_kok(_get_bytes_len(key, ESP_NOW_KEY_LEN), ESP_NOW_KEY_LEN));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_set_pmk_obj, espnow_set_pmk);
// ESPNow.add_peer(peer_mac, [lmk, [channel, [ifidx, [encrypt]]]])
// Positional args set to None will be left at defaults.
// Raise OSError if not initialised.
// Raise ValueError if mac or LMK are not bytes-like objects or wrong length.
// Raise TypeError if invalid keyword args or too many positional args.
// Return None.
STATIC mp_obj_t espnow_add_peer(size_t n_args, const mp_obj_t *args) {
check_esp_err(
esp_now_add_peer(
_get_bytes_len(args[1], ESP_NOW_ETH_ALEN),
ESP_NOW_ROLE_COMBO,
(n_args > 3) ? mp_obj_get_int(args[3]) : 0,
(n_args > 2) ? _get_bytes_len(args[2], ESP_NOW_KEY_LEN) : NULL,
ESP_NOW_KEY_LEN));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_add_peer_obj, 2, 4, espnow_add_peer);
// ESPNow.del_peer(peer_mac): Unregister peer_mac.
// Raise OSError if not initialised.
// Raise ValueError if peer is not a bytes-like objects or wrong length.
// Return None.
STATIC mp_obj_t espnow_del_peer(mp_obj_t _, mp_obj_t peer) {
esp_now_del_peer(_get_bytes_len(peer, ESP_NOW_ETH_ALEN));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_del_peer_obj, espnow_del_peer);
STATIC const mp_rom_map_elem_t esp_espnow_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_active), MP_ROM_PTR(&espnow_active_obj) },
{ MP_ROM_QSTR(MP_QSTR_config), MP_ROM_PTR(&espnow_config_obj) },
{ MP_ROM_QSTR(MP_QSTR_recvinto), MP_ROM_PTR(&espnow_recvinto_obj) },
{ MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&espnow_send_obj) },
// Peer management functions
{ MP_ROM_QSTR(MP_QSTR_set_pmk), MP_ROM_PTR(&espnow_set_pmk_obj) },
{ MP_ROM_QSTR(MP_QSTR_add_peer), MP_ROM_PTR(&espnow_add_peer_obj) },
{ MP_ROM_QSTR(MP_QSTR_del_peer), MP_ROM_PTR(&espnow_del_peer_obj) },
};
STATIC MP_DEFINE_CONST_DICT(esp_espnow_locals_dict, esp_espnow_locals_dict_table);
STATIC const mp_rom_map_elem_t espnow_globals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR__espnow) },
{ MP_ROM_QSTR(MP_QSTR_ESPNowBase), MP_ROM_PTR(&esp_espnow_type) },
{ MP_ROM_QSTR(MP_QSTR_MAX_DATA_LEN), MP_ROM_INT(ESP_NOW_MAX_DATA_LEN)},
{ MP_ROM_QSTR(MP_QSTR_ADDR_LEN), MP_ROM_INT(ESP_NOW_ETH_ALEN)},
{ MP_ROM_QSTR(MP_QSTR_KEY_LEN), MP_ROM_INT(ESP_NOW_KEY_LEN)},
{ MP_ROM_QSTR(MP_QSTR_MAX_TOTAL_PEER_NUM), MP_ROM_INT(ESP_NOW_MAX_TOTAL_PEER_NUM)},
{ MP_ROM_QSTR(MP_QSTR_MAX_ENCRYPT_PEER_NUM), MP_ROM_INT(ESP_NOW_MAX_ENCRYPT_PEER_NUM)},
};
STATIC MP_DEFINE_CONST_DICT(espnow_globals_dict, espnow_globals_dict_table);
// ### Dummy Buffer Protocol support
// ...so asyncio can poll.ipoll() on this device
// Support ioctl(MP_STREAM_POLL, ) for asyncio
STATIC mp_uint_t espnow_stream_ioctl(mp_obj_t self_in, mp_uint_t request,
uintptr_t arg, int *errcode) {
if (request != MP_STREAM_POLL) {
*errcode = MP_EINVAL;
return MP_STREAM_ERROR;
}
esp_espnow_obj_t *self = _get_singleton();
return (self->recv_buffer == NULL) ? 0 : // If not initialised
arg ^ ((ringbuf_avail(self->recv_buffer) == 0) ? MP_STREAM_POLL_RD : 0);
}
STATIC const mp_stream_p_t espnow_stream_p = {
.ioctl = espnow_stream_ioctl,
};
MP_DEFINE_CONST_OBJ_TYPE(
esp_espnow_type,
MP_QSTR_ESPNowBase,
MP_TYPE_FLAG_NONE,
make_new, espnow_make_new,
protocol, &espnow_stream_p,
locals_dict, &esp_espnow_locals_dict
);
const mp_obj_module_t mp_module_espnow = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&espnow_globals_dict,
};
MP_REGISTER_MODULE(MP_QSTR__espnow, mp_module_espnow);
MP_REGISTER_ROOT_POINTER(void *espnow_buffer);
#endif

28
ports/esp8266/modespnow.h

@ -0,0 +1,28 @@
/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2021 Glenn Moloney @glenn20
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
// Called from main.c:mp_task() to reset the espnow software stack
mp_obj_t espnow_deinit(mp_obj_t _);

37
ports/esp8266/modules/espnow.py

@ -0,0 +1,37 @@
# espnow module for MicroPython on ESP8266
# MIT license; Copyright (c) 2022 Glenn Moloney @glenn20
from _espnow import *
from uselect import poll, POLLIN
class ESPNow(ESPNowBase):
# Static buffers for alloc free receipt of messages with ESPNow.irecv().
_data = [bytearray(ADDR_LEN), bytearray(MAX_DATA_LEN)]
_none_tuple = (None, None)
def __init__(self):
super().__init__()
self._poll = poll() # For any() method below...
self._poll.register(self, POLLIN)
def irecv(self, timeout_ms=None):
n = self.recvinto(self._data, timeout_ms)
return self._data if n else self._none_tuple
def recv(self, timeout_ms=None):
n = self.recvinto(self._data, timeout_ms)
return [bytes(x) for x in self._data] if n else self._none_tuple
def __iter__(self):
return self
def __next__(self):
return self.irecv() # Use alloc free irecv() method
def any(self): # For the ESP8266 which does not have ESPNow.any()
try:
next(self._poll.ipoll(0))
return True
except StopIteration:
return False

57
tests/multi_espnow/10_simple_data.py

@ -0,0 +1,57 @@
# Simple test of a ESPnow server and client transferring data.
# This test works with ESP32 or ESP8266 as server or client.
try:
import network
import espnow
except ImportError:
print("SKIP")
raise SystemExit
# Set read timeout to 5 seconds
timeout_ms = 5000
default_pmk = b"MicroPyth0nRules"
def init(sta_active=True, ap_active=False):
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
e = espnow.ESPNow()
e.active(True)
e.set_pmk(default_pmk)
wlans[0].active(sta_active)
wlans[1].active(ap_active)
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
e.set_pmk(default_pmk)
return e
# Server
def instance0():
e = init(True, False)
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
multitest.next()
peer, msg1 = e.recv(timeout_ms)
if msg1 is None:
print("e.recv({timeout_ms}): Timeout waiting for message.")
e.active(False)
return
print(bytes(msg1))
msg2 = b"server to client"
e.add_peer(peer)
e.send(peer, msg2)
print(bytes(msg2))
e.active(False)
# Client
def instance1():
e = init(True, False)
multitest.next()
peer = PEERS[0]
e.add_peer(peer)
msg1 = b"client to server"
e.send(peer, msg1)
print(bytes(msg1))
peer2, msg2 = e.recv(timeout_ms)
print(bytes(msg2))
e.active(False)

6
tests/multi_espnow/10_simple_data.py.exp

@ -0,0 +1,6 @@
--- instance0 ---
b'client to server'
b'server to client'
--- instance1 ---
b'client to server'
b'server to client'

93
tests/multi_espnow/20_send_echo.py

@ -0,0 +1,93 @@
# Test of a ESPnow echo server and client transferring data.
# This test works with ESP32 or ESP8266 as server or client.
try:
import network
import random
import espnow
except ImportError:
print("SKIP")
raise SystemExit
# Set read timeout to 5 seconds
timeout_ms = 5000
default_pmk = b"MicroPyth0nRules"
sync = True
def echo_server(e):
peers = []
while True:
peer, msg = e.recv(timeout_ms)
if peer is None:
return
if peer not in peers:
peers.append(peer)
e.add_peer(peer)
# Echo the MAC and message back to the sender
if not e.send(peer, msg, sync):
print("ERROR: send() failed to", peer)
return
if msg == b"!done":
return
def echo_test(e, peer, msg, sync):
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
try:
if not e.send(peer, msg, sync):
print("ERROR: Send failed.")
return
except OSError as exc:
# Don't print exc as it is differs for esp32 and esp8266
print("ERROR: OSError:")
return
p2, msg2 = e.recv(timeout_ms)
print("OK" if msg2 == msg else "ERROR: Received != Sent")
def echo_client(e, peer, msglens):
for sync in [True, False]:
for msglen in msglens:
msg = bytearray(msglen)
if msglen > 0:
msg[0] = b"_"[0] # Random message must not start with '!'
for i in range(1, msglen):
msg[i] = random.getrandbits(8)
echo_test(e, peer, msg, sync)
def init(sta_active=True, ap_active=False):
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
e = espnow.ESPNow()
e.active(True)
e.set_pmk(default_pmk)
wlans[0].active(sta_active)
wlans[1].active(ap_active)
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
return e
# Server
def instance0():
e = init(True, False)
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
multitest.next()
print("Server Start")
echo_server(e)
print("Server Done")
e.active(False)
# Client
def instance1():
e = init(True, False)
multitest.next()
peer = PEERS[0]
e.add_peer(peer)
echo_client(e, peer, [1, 2, 8, 100, 249, 250, 251, 0])
echo_test(e, peer, b"!done", True)
e.active(False)

21
tests/multi_espnow/20_send_echo.py.exp

@ -0,0 +1,21 @@
--- instance0 ---
Server Start
Server Done
--- instance1 ---
TEST: send/recv(msglen=1,sync=True): OK
TEST: send/recv(msglen=2,sync=True): OK
TEST: send/recv(msglen=8,sync=True): OK
TEST: send/recv(msglen=100,sync=True): OK
TEST: send/recv(msglen=249,sync=True): OK
TEST: send/recv(msglen=250,sync=True): OK
TEST: send/recv(msglen=251,sync=True): ERROR: OSError:
TEST: send/recv(msglen=0,sync=True): ERROR: OSError:
TEST: send/recv(msglen=1,sync=False): OK
TEST: send/recv(msglen=2,sync=False): OK
TEST: send/recv(msglen=8,sync=False): OK
TEST: send/recv(msglen=100,sync=False): OK
TEST: send/recv(msglen=249,sync=False): OK
TEST: send/recv(msglen=250,sync=False): OK
TEST: send/recv(msglen=251,sync=False): ERROR: OSError:
TEST: send/recv(msglen=0,sync=False): ERROR: OSError:
TEST: send/recv(msglen=5,sync=True): OK

130
tests/multi_espnow/30_lmk_echo.py

@ -0,0 +1,130 @@
# Test of a ESPnow echo server and client transferring encrypted data.
# This test works with ESP32 or ESP8266 as server or client.
# First instance (echo server):
# Set the shared PMK
# Set the PEERS global to our mac addresses
# Run the echo server
# First exchange an unencrypted message from the client (so we
# can get its MAC address) and echo the message back (unenecrypted).
# Then set the peer LMK so all further communications are encrypted.
# Second instance (echo client):
# Set the shared PMK
# Send an unencrypted message to the server and wait for echo response.
# Set the LMK for the peer communications so all further comms are encrypted.
# Send random messages and compare with response from server.
try:
import network
import random
import time
import espnow
except ImportError:
print("SKIP")
raise SystemExit
# Set read timeout to 5 seconds
timeout_ms = 5000
default_pmk = b"MicroPyth0nRules"
default_lmk = b"0123456789abcdef"
sync = True
def echo_server(e):
peers = []
while True:
# Wait for messages from the client
peer, msg = e.recv(timeout_ms)
if peer is None:
return
if peer not in peers:
# If this is first message, add the peer unencrypted
e.add_peer(peer)
# Echo the message back to the sender
if not e.send(peer, msg, sync):
print("ERROR: send() failed to", peer)
return
if peer not in peers:
# If this is first message, add the peer encrypted
peers.append(peer)
e.del_peer(peer)
e.add_peer(peer, default_lmk)
if msg == b"!done":
return
# Send a message from the client and compare with response from server.
def echo_test(e, peer, msg, sync):
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
try:
if not e.send(peer, msg, sync):
print("ERROR: Send failed.")
return
except OSError as exc:
# Don't print exc as it is differs for esp32 and esp8266
print("ERROR: OSError:")
return
p2, msg2 = e.recv(timeout_ms)
if p2 is None:
print("ERROR: No response from server.")
raise SystemExit
print("OK" if msg2 == msg else "ERROR: Received != Sent")
# Send some random messages to server and check the responses
def echo_client(e, peer, msglens):
for sync in [True, False]:
for msglen in msglens:
msg = bytearray(msglen)
if msglen > 0:
msg[0] = b"_"[0] # Random message must not start with '!'
for i in range(1, msglen):
msg[i] = random.getrandbits(8)
echo_test(e, peer, msg, sync)
# Initialise the wifi and espnow hardware and software
def init(sta_active=True, ap_active=False):
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
e = espnow.ESPNow()
e.active(True)
e.set_pmk(default_pmk)
wlans[0].active(sta_active)
wlans[1].active(ap_active)
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
return e
# Server
def instance0():
e = init(True, False)
macs = [network.WLAN(i).config("mac") for i in (0, 1)]
print("Server Start")
multitest.globals(PEERS=macs)
multitest.next()
echo_server(e)
print("Server Done")
e.active(False)
# Client
def instance1():
e = init(True, False)
multitest.next()
peer = PEERS[0]
e.add_peer(peer)
echo_test(e, peer, b"start", True)
# Wait long enough for the server to set the lmk
time.sleep(0.1)
e.del_peer(peer)
e.add_peer(peer, default_lmk)
echo_client(e, peer, [250])
echo_test(e, peer, b"!done", True)
e.active(False)

8
tests/multi_espnow/30_lmk_echo.py.exp

@ -0,0 +1,8 @@
--- instance0 ---
Server Start
Server Done
--- instance1 ---
TEST: send/recv(msglen=5,sync=True): OK
TEST: send/recv(msglen=250,sync=True): OK
TEST: send/recv(msglen=250,sync=False): OK
TEST: send/recv(msglen=5,sync=True): OK

113
tests/multi_espnow/40_recv_test.py

@ -0,0 +1,113 @@
# Test of a ESPnow echo server and client transferring data.
# This test works with ESP32 or ESP8266 as server or client.
# Explicitly tests the irecv(), rev() and recvinto() methods.
try:
import network
import random
import espnow
except ImportError:
print("SKIP")
raise SystemExit
# Set read timeout to 5 seconds
timeout_ms = 5000
default_pmk = b"MicroPyth0nRules"
sync = True
def echo_server(e):
peers = []
while True:
peer, msg = e.irecv(timeout_ms)
if peer is None:
return
if peer not in peers:
peers.append(peer)
e.add_peer(peer)
# Echo the MAC and message back to the sender
if not e.send(peer, msg, sync):
print("ERROR: send() failed to", peer)
return
if msg == b"!done":
return
def client_send(e, peer, msg, sync):
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
try:
if not e.send(peer, msg, sync):
print("ERROR: Send failed.")
return
except OSError as exc:
# Don't print exc as it is differs for esp32 and esp8266
print("ERROR: OSError:")
return
def init(sta_active=True, ap_active=False):
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
e = espnow.ESPNow()
e.active(True)
e.set_pmk(default_pmk)
wlans[0].active(sta_active)
wlans[1].active(ap_active)
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
return e
# Server
def instance0():
e = init(True, False)
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
multitest.next()
print("Server Start")
echo_server(e)
print("Server Done")
e.active(False)
# Client
def instance1():
# Instance 1 (the client)
e = init(True, False)
e.config(timeout_ms=timeout_ms)
multitest.next()
peer = PEERS[0]
e.add_peer(peer)
print("RECVINTO() test...")
msg = bytes([random.getrandbits(8) for _ in range(12)])
client_send(e, peer, msg, True)
data = [bytearray(espnow.ADDR_LEN), bytearray(espnow.MAX_DATA_LEN)]
n = e.recvinto(data)
print("OK" if data[1] == msg else "ERROR: Received != Sent")
print("IRECV() test...")
msg = bytes([random.getrandbits(8) for _ in range(12)])
client_send(e, peer, msg, True)
p2, msg2 = e.irecv()
print("OK" if msg2 == msg else "ERROR: Received != Sent")
print("RECV() test...")
msg = bytes([random.getrandbits(8) for _ in range(12)])
client_send(e, peer, msg, True)
p2, msg2 = e.recv()
print("OK" if msg2 == msg else "ERROR: Received != Sent")
print("ITERATOR() test...")
msg = bytes([random.getrandbits(8) for _ in range(12)])
client_send(e, peer, msg, True)
p2, msg2 = next(e)
print("OK" if msg2 == msg else "ERROR: Received != Sent")
# Tell the server to stop
print("DONE")
msg = b"!done"
client_send(e, peer, msg, True)
p2, msg2 = e.irecv()
print("OK" if msg2 == msg else "ERROR: Received != Sent")
e.active(False)

14
tests/multi_espnow/40_recv_test.py.exp

@ -0,0 +1,14 @@
--- instance0 ---
Server Start
Server Done
--- instance1 ---
RECVINTO() test...
TEST: send/recv(msglen=12,sync=True): OK
IRECV() test...
TEST: send/recv(msglen=12,sync=True): OK
RECV() test...
TEST: send/recv(msglen=12,sync=True): OK
ITERATOR() test...
TEST: send/recv(msglen=12,sync=True): OK
DONE
TEST: send/recv(msglen=5,sync=True): OK

114
tests/multi_espnow/50_esp32_rssi_test.py

@ -0,0 +1,114 @@
# Test the ESP32 RSSI extensions on instance1.
# Will SKIP test if instance1 is not an ESP32.
# Instance0 may be an ESP32 or ESP8266.
try:
import time
import network
import random
import espnow
except ImportError:
print("SKIP")
raise SystemExit
# Set read timeout to 5 seconds
timeout_ms = 5000
default_pmk = b"MicroPyth0nRules"
sync = True
def echo_server(e):
peers = []
while True:
peer, msg = e.irecv(timeout_ms)
if peer is None:
return
if peer not in peers:
peers.append(peer)
e.add_peer(peer)
# Echo the MAC and message back to the sender
if not e.send(peer, msg, sync):
print("ERROR: send() failed to", peer)
return
if msg == b"!done":
return
def client_send(e, peer, msg, sync):
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
try:
if not e.send(peer, msg, sync):
print("ERROR: Send failed.")
return
except OSError as exc:
# Don't print exc as it is differs for esp32 and esp8266
print("ERROR: OSError:")
return
def init(sta_active=True, ap_active=False):
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
e = espnow.ESPNow()
e.active(True)
e.set_pmk(default_pmk)
wlans[0].active(sta_active)
wlans[1].active(ap_active)
return e
# Server
def instance0():
e = init(True, False)
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
multitest.next()
print("Server Start")
echo_server(e)
print("Server Done")
e.active(False)
# Client
def instance1():
# Instance 1 (the client)
e = init(True, False)
if not hasattr(e, "peers_table"):
e.active(False)
print("SKIP")
raise SystemExit
e.config(timeout_ms=timeout_ms)
multitest.next()
peer = PEERS[0]
e.add_peer(peer)
# assert len(e.peers) == 1
print("IRECV() test...")
msg = bytes([random.getrandbits(8) for _ in range(12)])
client_send(e, peer, msg, True)
p2, msg2 = e.irecv()
print("OK" if msg2 == msg else "ERROR: Received != Sent")
print("RSSI test...")
if len(e.peers_table) != 1:
print("ERROR: len(ESPNow.peers_table()) != 1. ESPNow.peers_table()=", peers)
elif list(e.peers_table.keys())[0] != peer:
print("ERROR: ESPNow.peers_table().keys[0] != peer. ESPNow.peers_table()=", peers)
else:
rssi, time_ms = e.peers_table[peer]
if not -127 < rssi < 0:
print("ERROR: Invalid rssi value:", rssi)
elif time.ticks_diff(time.ticks_ms(), time_ms) > 5000:
print("ERROR: Unexpected time_ms value:", time_ms)
else:
print("OK")
# Tell the server to stop
print("DONE")
msg = b"!done"
client_send(e, peer, msg, True)
p2, msg2 = e.irecv()
print("OK" if msg2 == msg else "ERROR: Received != Sent")
e.active(False)

10
tests/multi_espnow/50_esp32_rssi_test.py.exp

@ -0,0 +1,10 @@
--- instance0 ---
Server Start
Server Done
--- instance1 ---
IRECV() test...
TEST: send/recv(msglen=12,sync=True): OK
RSSI test...
OK
DONE
TEST: send/recv(msglen=5,sync=True): OK

117
tests/multi_espnow/60_irq_test.py

@ -0,0 +1,117 @@
# Test of a ESPnow echo server and client transferring data.
# Test the ESP32 extemnsions. Assumes instance1 is an ESP32.
# Instance0 may be an ESP32 or ESP8266
try:
import network
import random
import time
import espnow
except ImportError:
print("SKIP")
raise SystemExit
# Set read timeout to 5 seconds
timeout_ms = 5000
default_pmk = b"MicroPyth0nRules"
sync = True
def echo_server(e):
peers = []
while True:
peer, msg = e.irecv(timeout_ms)
if peer is None:
return
if peer not in peers:
peers.append(peer)
e.add_peer(peer)
# Echo the MAC and message back to the sender
if not e.send(peer, msg, sync):
print("ERROR: send() failed to", peer)
return
if msg == b"!done":
return
def client_send(e, peer, msg, sync):
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
try:
if not e.send(peer, msg, sync):
print("ERROR: Send failed.")
return
except OSError as exc:
# Don't print exc as it is differs for esp32 and esp8266
print("ERROR: OSError:")
return
def init(sta_active=True, ap_active=False):
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
e = espnow.ESPNow()
e.active(True)
e.set_pmk(default_pmk)
wlans[0].active(sta_active)
wlans[1].active(ap_active)
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
return e
# Server
def instance0():
e = init(True, False)
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
multitest.next()
print("Server Start")
echo_server(e)
print("Server Done")
e.active(False)
done = False
# Client
def instance1():
# Instance 1 (the client)
e = init(True, False)
try:
e.irq(None)
except AttributeError:
print("SKIP")
raise SystemExit
e.config(timeout_ms=timeout_ms)
multitest.next()
peer = PEERS[0]
e.add_peer(peer)
def on_recv_cb(e):
global done
p2, msg2 = e.irecv(0)
print("OK" if msg2 == msg else "ERROR: Received != Sent")
done = True
global done
print("IRQ() test...")
e.irq(on_recv_cb)
done = False
msg = bytes([random.getrandbits(8) for _ in range(12)])
client_send(e, peer, msg, True)
start = time.ticks_ms()
while not done:
if time.ticks_diff(time.ticks_ms(), start) > timeout_ms:
print("Timeout waiting for response.")
raise SystemExit
e.irq(None)
# Tell the server to stop
print("DONE")
msg = b"!done"
client_send(e, peer, msg, True)
p2, msg2 = e.irecv()
print("OK" if msg2 == msg else "ERROR: Received != Sent")
e.active(False)

8
tests/multi_espnow/60_irq_test.py.exp

@ -0,0 +1,8 @@
--- instance0 ---
Server Start
Server Done
--- instance1 ---
IRQ() test...
TEST: send/recv(msglen=12,sync=True): OK
DONE
TEST: send/recv(msglen=5,sync=True): OK

110
tests/multi_espnow/80_uasyncio_client.py

@ -0,0 +1,110 @@
# Test of a ESPnow echo server and asyncio client transferring data.
# Test will SKIP if instance1 (asyncio client) does not support asyncio.
# - eg. ESP8266 with 1MB flash.
# Instance0 is not required to support asyncio.
try:
import network
import random
import espnow
except ImportError:
print("SKIP")
raise SystemExit
# Set read timeout to 5 seconds
timeout_ms = 5000
default_pmk = b"MicroPyth0nRules"
sync = True
def echo_server(e):
peers = []
while True:
peer, msg = e.irecv(timeout_ms)
if peer is None:
return
if peer not in peers:
peers.append(peer)
e.add_peer(peer)
# Echo the MAC and message back to the sender
if not e.send(peer, msg, sync):
print("ERROR: send() failed to", peer)
return
if msg == b"!done":
return
def client_send(e, peer, msg, sync):
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
try:
if not e.send(peer, msg, sync):
print("ERROR: Send failed.")
return
except OSError as exc:
# Don't print exc as it is differs for esp32 and esp8266
print("ERROR: OSError:")
return
print("OK")
def init(e, sta_active=True, ap_active=False):
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
e.active(True)
e.set_pmk(default_pmk)
wlans[0].active(sta_active)
wlans[1].active(ap_active)
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
return e
async def client(e):
init(e, True, False)
e.config(timeout_ms=timeout_ms)
peer = PEERS[0]
e.add_peer(peer)
multitest.next()
print("airecv() test...")
msgs = []
for i in range(5):
# Send messages to the peer who will echo it back
msgs.append(bytes([random.getrandbits(8) for _ in range(12)]))
client_send(e, peer, msgs[i], True)
for i in range(5):
mac, reply = await e.airecv()
print("OK" if reply == msgs[i] else "ERROR: Received != Sent")
# Tell the server to stop
print("DONE")
msg = b"!done"
client_send(e, peer, msg, True)
mac, reply = await e.airecv()
print("OK" if reply == msg else "ERROR: Received != Sent")
e.active(False)
# Server
def instance0():
e = espnow.ESPNow()
init(e, True, False)
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
multitest.next()
print("Server Start")
echo_server(e)
print("Server Done")
e.active(False)
# Client
def instance1():
try:
import uasyncio as asyncio
from aioespnow import AIOESPNow
except ImportError:
print("SKIP")
raise SystemExit
asyncio.run(client(AIOESPNow()))

18
tests/multi_espnow/80_uasyncio_client.py.exp

@ -0,0 +1,18 @@
--- instance0 ---
Server Start
Server Done
--- instance1 ---
airecv() test...
TEST: send/recv(msglen=12,sync=True): OK
TEST: send/recv(msglen=12,sync=True): OK
TEST: send/recv(msglen=12,sync=True): OK
TEST: send/recv(msglen=12,sync=True): OK
TEST: send/recv(msglen=12,sync=True): OK
OK
OK
OK
OK
OK
DONE
TEST: send/recv(msglen=5,sync=True): OK
OK

96
tests/multi_espnow/81_uasyncio_server.py

@ -0,0 +1,96 @@
# Test of a ESPnow asyncio echo server and client transferring data.
# Test will SKIP if instance0 (asyncio echo server) does not support asyncio.
# - eg. ESP8266 with 1MB flash.
# Instance1 is not required to support asyncio.
try:
import network
import random
import espnow
except ImportError:
print("SKIP")
raise SystemExit
# Set read timeout to 5 seconds
timeout_ms = 5000
default_pmk = b"MicroPyth0nRules"
sync = True
def client_send(e, peer, msg, sync):
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
try:
if not e.send(peer, msg, sync):
print("ERROR: Send failed.")
return
except OSError as exc:
# Don't print exc as it is differs for esp32 and esp8266
print("ERROR: OSError:")
return
def init(e, sta_active=True, ap_active=False):
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
e.active(True)
e.set_pmk(default_pmk)
wlans[0].active(sta_active)
wlans[1].active(ap_active)
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
return e
async def echo_server(e):
peers = []
async for peer, msg in e:
if peer not in peers:
peers.append(peer)
e.add_peer(peer)
# Echo the message back to the sender
if not await e.asend(peer, msg, sync):
print("ERROR: asend() failed to", peer)
return
if msg == b"!done":
return
# Server
def instance0():
try:
import uasyncio as asyncio
from aioespnow import AIOESPNow
except ImportError:
print("SKIP")
raise SystemExit
e = AIOESPNow()
init(e, True, False)
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
multitest.next()
print("Server Start")
asyncio.run(echo_server(e))
print("Server Done")
e.active(False)
def instance1():
e = espnow.ESPNow()
init(e, True, False)
peer = PEERS[0]
e.add_peer(peer)
multitest.next()
for i in range(5):
msg = bytes([random.getrandbits(8) for _ in range(12)])
client_send(e, peer, msg, True)
p2, msg2 = e.irecv(timeout_ms)
print("OK" if msg2 == msg else "ERROR: Received != Sent")
# Tell the server to stop
print("DONE")
msg = b"!done"
client_send(e, peer, msg, True)
p2, msg2 = e.irecv(timeout_ms)
print("OK" if msg2 == msg else "ERROR: Received != Sent")
e.active(False)

11
tests/multi_espnow/81_uasyncio_server.py.exp

@ -0,0 +1,11 @@
--- instance0 ---
Server Start
Server Done
--- instance1 ---
TEST: send/recv(msglen=12,sync=True): OK
TEST: send/recv(msglen=12,sync=True): OK
TEST: send/recv(msglen=12,sync=True): OK
TEST: send/recv(msglen=12,sync=True): OK
TEST: send/recv(msglen=12,sync=True): OK
DONE
TEST: send/recv(msglen=5,sync=True): OK

108
tests/multi_espnow/90_memory_test.py

@ -0,0 +1,108 @@
# Test of a ESPnow echo server and client transferring data.
# This test works with ESP32 or ESP8266 as server or client.
try:
import network
import random
import espnow
except ImportError:
print("SKIP")
raise SystemExit
# Set read timeout to 5 seconds
timeout_ms = 5000
default_pmk = b"MicroPyth0nRules"
sync = True
def echo_server(e):
peers = []
i = 0
while True:
peer, msg = e.irecv(timeout_ms)
i += 1
if i % 10 == 0:
print("OK:", i)
if peer is None:
return
if peer not in peers:
peers.append(peer)
e.add_peer(peer)
# Echo the MAC and message back to the sender
if not e.send(peer, msg, sync):
print("ERROR: send() failed to", peer)
return
if msg == b"!done":
return
def echo_test(e, peer, msg, sync):
try:
if not e.send(peer, msg, sync):
print("ERROR: Send failed.")
return
except OSError as exc:
# Don't print exc as it is differs for esp32 and esp8266
print("ERROR: OSError:")
return
p2, msg2 = e.irecv(timeout_ms)
if msg2 != msg:
print("ERROR: Received != Sent")
def echo_client(e, peer, msglens):
for sync in [True]:
for msglen in msglens:
msg = bytearray(msglen)
if msglen > 0:
msg[0] = b"_"[0] # Random message must not start with '!'
for i in range(1, msglen):
msg[i] = random.getrandbits(8)
echo_test(e, peer, msg, sync)
def init(sta_active=True, ap_active=False):
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
e = espnow.ESPNow()
e.active(True)
e.set_pmk(default_pmk)
wlans[0].active(sta_active)
wlans[1].active(ap_active)
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
return e
# Server
def instance0():
e = init(True, False)
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
multitest.next()
print("Server Start")
echo_server(e)
print("Server Done")
e.active(False)
# Client
def instance1():
e = init(True, False)
multitest.next()
peer = PEERS[0]
e.add_peer(peer)
echo_test(e, peer, b"ping", True)
gc.collect()
mem_start = gc.mem_alloc()
for i in range(10):
echo_client(e, peer, [250] * 10)
print("OK:", (i + 1) * 10)
echo_test(e, peer, b"!done", True)
gc.collect()
mem_end = gc.mem_alloc()
if mem_end - mem_start < 1024:
print("OK: Less than 1024 bytes consumed")
else:
print("Error: Memory consumed is", mem_end - mem_start)
e.active(False)

25
tests/multi_espnow/90_memory_test.py.exp

@ -0,0 +1,25 @@
--- instance0 ---
Server Start
OK: 10
OK: 20
OK: 30
OK: 40
OK: 50
OK: 60
OK: 70
OK: 80
OK: 90
OK: 100
Server Done
--- instance1 ---
OK: 10
OK: 20
OK: 30
OK: 40
OK: 50
OK: 60
OK: 70
OK: 80
OK: 90
OK: 100
OK: Less than 1024 bytes consumed
Loading…
Cancel
Save