From 7fa322afb8b3d4b124fa613d357cba5c30ac56d8 Mon Sep 17 00:00:00 2001 From: Glenn Moloney Date: Thu, 24 Sep 2020 15:37:04 +1000 Subject: [PATCH] esp32,esp8266: Add support for the Espressif ESP-NOW protocol. ESP-NOW is a proprietary wireless communication protocol which supports connectionless communication between ESP32 and ESP8266 devices, using vendor specific WiFi frames. This commit adds support for this protocol through a new `espnow` module. This commit builds on original work done by @nickzoic, @shawwwn and with contributions from @zoland. Features include: - Use of (extended) ring buffers in py/ringbuf.[ch] for robust IO. - Signal strength (RSSI) monitoring. - Core support in `_espnow` C module, extended by `espnow.py` module. - Asyncio support via `aioespnow.py` module (separate to this commit). - Docs provided at `docs/library/espnow.rst`. Methods available in espnow.ESPNow class are: - active(True/False) - config(): set rx buffer size, read timeout and tx rate - recv()/irecv()/recvinto() to read incoming messages from peers - send() to send messages to peer devices - any() to test if a message is ready to read - irq() to set callback for received messages - stats() returns transfer stats: (tx_pkts, tx_pkt_responses, tx_failures, rx_pkts, lost_rx_pkts) - add_peer(mac, ...) registers a peer before sending messages - get_peer(mac) returns peer info: (mac, lmk, channel, ifidx, encrypt) - mod_peer(mac, ...) changes peer info parameters - get_peers() returns all peer info tuples - peers_table supports RSSI signal monitoring for received messages: {peer1: [rssi, time_ms], peer2: [rssi, time_ms], ...} ESP8266 is a pared down version of the ESP32 ESPNow support due to code size restrictions and differences in the low-level API. See docs for details. Also included is a test suite in tests/multi_espnow. This tests basic espnow data transfer, multiple transfers, various message sizes, encrypted messages (pmk and lmk), and asyncio support. Initial work is from https://github.com/micropython/micropython/pull/4115. Initial import of code is from: https://github.com/nickzoic/micropython/tree/espnow-4115. --- docs/library/espnow.rst | 917 ++++++++++++++++++ docs/library/index.rst | 5 + ports/esp32/boards/manifest.py | 1 + ports/esp32/main.c | 9 + ports/esp32/main/CMakeLists.txt | 1 + ports/esp32/modespnow.c | 884 +++++++++++++++++ ports/esp32/modespnow.h | 30 + ports/esp32/modnetwork.h | 1 + ports/esp32/modules/espnow.py | 30 + ports/esp32/mpconfigport.h | 3 + ports/esp32/network_wlan.c | 12 +- ports/esp8266/Makefile | 10 + ports/esp8266/boards/GENERIC/mpconfigboard.mk | 1 + .../boards/GENERIC_1M/mpconfigboard.mk | 1 + ports/esp8266/boards/esp8266_common.ld | 1 + ports/esp8266/boards/manifest.py | 1 + ports/esp8266/main.c | 8 + ports/esp8266/modespnow.c | 507 ++++++++++ ports/esp8266/modespnow.h | 28 + ports/esp8266/modules/espnow.py | 37 + tests/multi_espnow/10_simple_data.py | 57 ++ tests/multi_espnow/10_simple_data.py.exp | 6 + tests/multi_espnow/20_send_echo.py | 93 ++ tests/multi_espnow/20_send_echo.py.exp | 21 + tests/multi_espnow/30_lmk_echo.py | 130 +++ tests/multi_espnow/30_lmk_echo.py.exp | 8 + tests/multi_espnow/40_recv_test.py | 113 +++ tests/multi_espnow/40_recv_test.py.exp | 14 + tests/multi_espnow/50_esp32_rssi_test.py | 114 +++ tests/multi_espnow/50_esp32_rssi_test.py.exp | 10 + tests/multi_espnow/60_irq_test.py | 117 +++ tests/multi_espnow/60_irq_test.py.exp | 8 + tests/multi_espnow/80_uasyncio_client.py | 110 +++ tests/multi_espnow/80_uasyncio_client.py.exp | 18 + tests/multi_espnow/81_uasyncio_server.py | 96 ++ tests/multi_espnow/81_uasyncio_server.py.exp | 11 + tests/multi_espnow/90_memory_test.py | 108 +++ tests/multi_espnow/90_memory_test.py.exp | 25 + 38 files changed, 3542 insertions(+), 4 deletions(-) create mode 100644 docs/library/espnow.rst create mode 100644 ports/esp32/modespnow.c create mode 100644 ports/esp32/modespnow.h create mode 100644 ports/esp32/modules/espnow.py create mode 100644 ports/esp8266/modespnow.c create mode 100644 ports/esp8266/modespnow.h create mode 100644 ports/esp8266/modules/espnow.py create mode 100644 tests/multi_espnow/10_simple_data.py create mode 100644 tests/multi_espnow/10_simple_data.py.exp create mode 100644 tests/multi_espnow/20_send_echo.py create mode 100644 tests/multi_espnow/20_send_echo.py.exp create mode 100644 tests/multi_espnow/30_lmk_echo.py create mode 100644 tests/multi_espnow/30_lmk_echo.py.exp create mode 100644 tests/multi_espnow/40_recv_test.py create mode 100644 tests/multi_espnow/40_recv_test.py.exp create mode 100644 tests/multi_espnow/50_esp32_rssi_test.py create mode 100644 tests/multi_espnow/50_esp32_rssi_test.py.exp create mode 100644 tests/multi_espnow/60_irq_test.py create mode 100644 tests/multi_espnow/60_irq_test.py.exp create mode 100644 tests/multi_espnow/80_uasyncio_client.py create mode 100644 tests/multi_espnow/80_uasyncio_client.py.exp create mode 100644 tests/multi_espnow/81_uasyncio_server.py create mode 100644 tests/multi_espnow/81_uasyncio_server.py.exp create mode 100644 tests/multi_espnow/90_memory_test.py create mode 100644 tests/multi_espnow/90_memory_test.py.exp diff --git a/docs/library/espnow.rst b/docs/library/espnow.rst new file mode 100644 index 0000000000..468eb38418 --- /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 `_ protocol provided by Espressif on +ESP32 and ESP8266 devices (`API docs `_). + +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`) 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)`. + + *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 + `_. + + .. 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()` 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)` (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()` after +`active(True)`. + +.. 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()` 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()`. + - ``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()` 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()`) 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()`. + - ``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()` 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()` 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 + `_ 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 `_ 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()` 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()`. + +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 +`_. 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` 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` + 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()` a broadcast message, the broadcast (or +multicast) MAC address must first be registered using +`add_peer()`. `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)` 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()` +`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 `_) 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 + `_ + ). 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])` and +`machine.deepsleep([time_ms])` 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)`) before entering light or deep sleep (see +`Sleep Modes `_). +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)` then both interfaces should be set +`active(False)` 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 59ed1127a7..8d7d8c563b 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 a6df79f0a3..fa851b5eea 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 e7d7626a6e..0d876eb5f7 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 9f777ab439..51e53c202f 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 0000000000..047245995d --- /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 + * 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 +#include +#include + +#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 0000000000..3c6280b1ce --- /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 5f2767ac8d..d7a99f5c94 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 0000000000..6956a3a935 --- /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 845c7e8fd7..807ae23b0c 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 aefc4394c2..84b92577fc 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 3aa9438f6e..e3727dfeda 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 6861317218..8d7babdc84 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 fdbb0d8245..adc31702e0 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 0fbbf55217..7e230fd42a 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 10fa6da279..17f58feac8 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 238490ebef..2aa81aba05 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 0000000000..1f89204676 --- /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 + * 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 +#include +#include + +#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 0000000000..b42a615db8 --- /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 0000000000..2f9c256c6b --- /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 0000000000..1d218fe981 --- /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 0000000000..71a247f04f --- /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 0000000000..4a1d1624d8 --- /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 0000000000..e43900bcf6 --- /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 0000000000..ac89080492 --- /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 0000000000..cd05fe6c7f --- /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 0000000000..46f4f78df4 --- /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 0000000000..61f2206254 --- /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 0000000000..6a47b540d3 --- /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 0000000000..22fc4f0285 --- /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 0000000000..37fc57ce4a --- /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 0000000000..c2be2dccfd --- /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 0000000000..fa2918cc03 --- /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 0000000000..05fdf8acaf --- /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 0000000000..ee098b7f3b --- /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 0000000000..abe34fc421 --- /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 0000000000..5e80eb0fdf --- /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 0000000000..1ea8c29599 --- /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