summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/library/espnow.rst917
-rw-r--r--docs/library/index.rst5
-rw-r--r--ports/esp32/boards/manifest.py1
-rw-r--r--ports/esp32/main.c9
-rw-r--r--ports/esp32/main/CMakeLists.txt1
-rw-r--r--ports/esp32/modespnow.c884
-rw-r--r--ports/esp32/modespnow.h30
-rw-r--r--ports/esp32/modnetwork.h1
-rw-r--r--ports/esp32/modules/espnow.py30
-rw-r--r--ports/esp32/mpconfigport.h3
-rw-r--r--ports/esp32/network_wlan.c12
-rw-r--r--ports/esp8266/Makefile10
-rw-r--r--ports/esp8266/boards/GENERIC/mpconfigboard.mk1
-rw-r--r--ports/esp8266/boards/GENERIC_1M/mpconfigboard.mk1
-rw-r--r--ports/esp8266/boards/esp8266_common.ld1
-rw-r--r--ports/esp8266/boards/manifest.py1
-rw-r--r--ports/esp8266/main.c8
-rw-r--r--ports/esp8266/modespnow.c507
-rw-r--r--ports/esp8266/modespnow.h28
-rw-r--r--ports/esp8266/modules/espnow.py37
-rw-r--r--tests/multi_espnow/10_simple_data.py57
-rw-r--r--tests/multi_espnow/10_simple_data.py.exp6
-rw-r--r--tests/multi_espnow/20_send_echo.py93
-rw-r--r--tests/multi_espnow/20_send_echo.py.exp21
-rw-r--r--tests/multi_espnow/30_lmk_echo.py130
-rw-r--r--tests/multi_espnow/30_lmk_echo.py.exp8
-rw-r--r--tests/multi_espnow/40_recv_test.py113
-rw-r--r--tests/multi_espnow/40_recv_test.py.exp14
-rw-r--r--tests/multi_espnow/50_esp32_rssi_test.py114
-rw-r--r--tests/multi_espnow/50_esp32_rssi_test.py.exp10
-rw-r--r--tests/multi_espnow/60_irq_test.py117
-rw-r--r--tests/multi_espnow/60_irq_test.py.exp8
-rw-r--r--tests/multi_espnow/80_uasyncio_client.py110
-rw-r--r--tests/multi_espnow/80_uasyncio_client.py.exp18
-rw-r--r--tests/multi_espnow/81_uasyncio_server.py96
-rw-r--r--tests/multi_espnow/81_uasyncio_server.py.exp11
-rw-r--r--tests/multi_espnow/90_memory_test.py108
-rw-r--r--tests/multi_espnow/90_memory_test.py.exp25
38 files changed, 3542 insertions, 4 deletions
diff --git a/docs/library/espnow.rst b/docs/library/espnow.rst
new file mode 100644
index 000000000..468eb3841
--- /dev/null
+++ b/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()
+
diff --git a/docs/library/index.rst b/docs/library/index.rst
index 59ed1127a..8d7d8c563 100644
--- a/docs/library/index.rst
+++ b/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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/ports/esp32/boards/manifest.py b/ports/esp32/boards/manifest.py
index a6df79f0a..fa851b5ee 100644
--- a/ports/esp32/boards/manifest.py
+++ b/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")
diff --git a/ports/esp32/main.c b/ports/esp32/main.c
index e7d7626a6..0d876eb5f 100644
--- a/ports/esp32/main.c
+++ b/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
diff --git a/ports/esp32/main/CMakeLists.txt b/ports/esp32/main/CMakeLists.txt
index 9f777ab43..51e53c202 100644
--- a/ports/esp32/main/CMakeLists.txt
+++ b/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
diff --git a/ports/esp32/modespnow.c b/ports/esp32/modespnow.c
new file mode 100644
index 000000000..047245995
--- /dev/null
+++ b/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);
diff --git a/ports/esp32/modespnow.h b/ports/esp32/modespnow.h
new file mode 100644
index 000000000..3c6280b1c
--- /dev/null
+++ b/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 _);
diff --git a/ports/esp32/modnetwork.h b/ports/esp32/modnetwork.h
index 5f2767ac8..d7a99f5c9 100644
--- a/ports/esp32/modnetwork.h
+++ b/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
diff --git a/ports/esp32/modules/espnow.py b/ports/esp32/modules/espnow.py
new file mode 100644
index 000000000..6956a3a93
--- /dev/null
+++ b/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
diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h
index 845c7e8fd..807ae23b0 100644
--- a/ports/esp32/mpconfigport.h
+++ b/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)
diff --git a/ports/esp32/network_wlan.c b/ports/esp32/network_wlan.c
index aefc4394c..84b92577f 100644
--- a/ports/esp32/network_wlan.c
+++ b/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) {
diff --git a/ports/esp8266/Makefile b/ports/esp8266/Makefile
index 3aa9438f6..e3727dfed 100644
--- a/ports/esp8266/Makefile
+++ b/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 \
diff --git a/ports/esp8266/boards/GENERIC/mpconfigboard.mk b/ports/esp8266/boards/GENERIC/mpconfigboard.mk
index 686131721..8d7babdc8 100644
--- a/ports/esp8266/boards/GENERIC/mpconfigboard.mk
+++ b/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
diff --git a/ports/esp8266/boards/GENERIC_1M/mpconfigboard.mk b/ports/esp8266/boards/GENERIC_1M/mpconfigboard.mk
index fdbb0d824..adc31702e 100644
--- a/ports/esp8266/boards/GENERIC_1M/mpconfigboard.mk
+++ b/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
diff --git a/ports/esp8266/boards/esp8266_common.ld b/ports/esp8266/boards/esp8266_common.ld
index 0fbbf5521..7e230fd42 100644
--- a/ports/esp8266/boards/esp8266_common.ld
+++ b/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 */
diff --git a/ports/esp8266/boards/manifest.py b/ports/esp8266/boards/manifest.py
index 10fa6da27..17f58feac 100644
--- a/ports/esp8266/boards/manifest.py
+++ b/ports/esp8266/boards/manifest.py
@@ -1,4 +1,5 @@
freeze("$(PORT_DIR)/modules")
+# require("aioespnow")
require("bundle-networking")
require("dht")
require("ds18x20")
diff --git a/ports/esp8266/main.c b/ports/esp8266/main.c
index 238490ebe..2aa81aba0 100644
--- a/ports/esp8266/main.c
+++ b/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");
diff --git a/ports/esp8266/modespnow.c b/ports/esp8266/modespnow.c
new file mode 100644
index 000000000..1f8920467
--- /dev/null
+++ b/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
diff --git a/ports/esp8266/modespnow.h b/ports/esp8266/modespnow.h
new file mode 100644
index 000000000..b42a615db
--- /dev/null
+++ b/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 _);
diff --git a/ports/esp8266/modules/espnow.py b/ports/esp8266/modules/espnow.py
new file mode 100644
index 000000000..2f9c256c6
--- /dev/null
+++ b/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
diff --git a/tests/multi_espnow/10_simple_data.py b/tests/multi_espnow/10_simple_data.py
new file mode 100644
index 000000000..1d218fe98
--- /dev/null
+++ b/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)
diff --git a/tests/multi_espnow/10_simple_data.py.exp b/tests/multi_espnow/10_simple_data.py.exp
new file mode 100644
index 000000000..71a247f04
--- /dev/null
+++ b/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'
diff --git a/tests/multi_espnow/20_send_echo.py b/tests/multi_espnow/20_send_echo.py
new file mode 100644
index 000000000..4a1d1624d
--- /dev/null
+++ b/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)
diff --git a/tests/multi_espnow/20_send_echo.py.exp b/tests/multi_espnow/20_send_echo.py.exp
new file mode 100644
index 000000000..e43900bcf
--- /dev/null
+++ b/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
diff --git a/tests/multi_espnow/30_lmk_echo.py b/tests/multi_espnow/30_lmk_echo.py
new file mode 100644
index 000000000..ac8908049
--- /dev/null
+++ b/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)
diff --git a/tests/multi_espnow/30_lmk_echo.py.exp b/tests/multi_espnow/30_lmk_echo.py.exp
new file mode 100644
index 000000000..cd05fe6c7
--- /dev/null
+++ b/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
diff --git a/tests/multi_espnow/40_recv_test.py b/tests/multi_espnow/40_recv_test.py
new file mode 100644
index 000000000..46f4f78df
--- /dev/null
+++ b/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)
diff --git a/tests/multi_espnow/40_recv_test.py.exp b/tests/multi_espnow/40_recv_test.py.exp
new file mode 100644
index 000000000..61f220625
--- /dev/null
+++ b/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
diff --git a/tests/multi_espnow/50_esp32_rssi_test.py b/tests/multi_espnow/50_esp32_rssi_test.py
new file mode 100644
index 000000000..6a47b540d
--- /dev/null
+++ b/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)
diff --git a/tests/multi_espnow/50_esp32_rssi_test.py.exp b/tests/multi_espnow/50_esp32_rssi_test.py.exp
new file mode 100644
index 000000000..22fc4f028
--- /dev/null
+++ b/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
diff --git a/tests/multi_espnow/60_irq_test.py b/tests/multi_espnow/60_irq_test.py
new file mode 100644
index 000000000..37fc57ce4
--- /dev/null
+++ b/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)
diff --git a/tests/multi_espnow/60_irq_test.py.exp b/tests/multi_espnow/60_irq_test.py.exp
new file mode 100644
index 000000000..c2be2dccf
--- /dev/null
+++ b/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
diff --git a/tests/multi_espnow/80_uasyncio_client.py b/tests/multi_espnow/80_uasyncio_client.py
new file mode 100644
index 000000000..fa2918cc0
--- /dev/null
+++ b/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()))
diff --git a/tests/multi_espnow/80_uasyncio_client.py.exp b/tests/multi_espnow/80_uasyncio_client.py.exp
new file mode 100644
index 000000000..05fdf8aca
--- /dev/null
+++ b/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
diff --git a/tests/multi_espnow/81_uasyncio_server.py b/tests/multi_espnow/81_uasyncio_server.py
new file mode 100644
index 000000000..ee098b7f3
--- /dev/null
+++ b/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)
diff --git a/tests/multi_espnow/81_uasyncio_server.py.exp b/tests/multi_espnow/81_uasyncio_server.py.exp
new file mode 100644
index 000000000..abe34fc42
--- /dev/null
+++ b/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
diff --git a/tests/multi_espnow/90_memory_test.py b/tests/multi_espnow/90_memory_test.py
new file mode 100644
index 000000000..5e80eb0fd
--- /dev/null
+++ b/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)
diff --git a/tests/multi_espnow/90_memory_test.py.exp b/tests/multi_espnow/90_memory_test.py.exp
new file mode 100644
index 000000000..1ea8c2959
--- /dev/null
+++ b/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