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