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.
This commit is contained in:
parent
9d735d1be7
commit
7fa322afb8
917
docs/library/espnow.rst
Normal file
917
docs/library/espnow.rst
Normal file
@ -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()
|
||||
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -84,6 +84,7 @@ set(MICROPY_SOURCE_PORT
|
||||
${PROJECT_DIR}/mpthreadport.c
|
||||
${PROJECT_DIR}/machine_rtc.c
|
||||
${PROJECT_DIR}/machine_sdcard.c
|
||||
${PROJECT_DIR}/modespnow.c
|
||||
)
|
||||
|
||||
set(MICROPY_SOURCE_QSTR
|
||||
|
884
ports/esp32/modespnow.c
Normal file
884
ports/esp32/modespnow.c
Normal file
@ -0,0 +1,884 @@
|
||||
/*
|
||||
* This file is part of the MicroPython project, http://micropython.org/
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2017-2020 Nick Moore
|
||||
* Copyright (c) 2018 shawwwn <shawwwn1@gmail.com>
|
||||
* Copyright (c) 2020-2021 Glenn Moloney @glenn20
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_now.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_wifi_types.h"
|
||||
|
||||
#include "py/runtime.h"
|
||||
#include "py/mphal.h"
|
||||
#include "py/mperrno.h"
|
||||
#include "py/obj.h"
|
||||
#include "py/objstr.h"
|
||||
#include "py/objarray.h"
|
||||
#include "py/stream.h"
|
||||
#include "py/binary.h"
|
||||
#include "py/ringbuf.h"
|
||||
|
||||
#include "mpconfigport.h"
|
||||
#include "mphalport.h"
|
||||
#include "modnetwork.h"
|
||||
#include "modespnow.h"
|
||||
|
||||
#ifndef MICROPY_ESPNOW_RSSI
|
||||
// Include code to track rssi of peers
|
||||
#define MICROPY_ESPNOW_RSSI 1
|
||||
#endif
|
||||
#ifndef MICROPY_ESPNOW_EXTRA_PEER_METHODS
|
||||
// Include mod_peer(),get_peer(),peer_count()
|
||||
#define MICROPY_ESPNOW_EXTRA_PEER_METHODS 1
|
||||
#endif
|
||||
|
||||
// Relies on gcc Variadic Macros and Statement Expressions
|
||||
#define NEW_TUPLE(...) \
|
||||
({mp_obj_t _z[] = {__VA_ARGS__}; mp_obj_new_tuple(MP_ARRAY_SIZE(_z), _z); })
|
||||
|
||||
static const uint8_t ESPNOW_MAGIC = 0x99;
|
||||
|
||||
// ESPNow packet format for the receive buffer.
|
||||
// Use this for peeking at the header of the next packet in the buffer.
|
||||
typedef struct {
|
||||
uint8_t magic; // = ESPNOW_MAGIC
|
||||
uint8_t msg_len; // Length of the message
|
||||
#if MICROPY_ESPNOW_RSSI
|
||||
uint32_t time_ms; // Timestamp (ms) when packet is received
|
||||
int8_t rssi; // RSSI value (dBm) (-127 to 0)
|
||||
#endif // MICROPY_ESPNOW_RSSI
|
||||
} __attribute__((packed)) espnow_hdr_t;
|
||||
|
||||
typedef struct {
|
||||
espnow_hdr_t hdr; // The header
|
||||
uint8_t peer[6]; // Peer address
|
||||
uint8_t msg[0]; // Message is up to 250 bytes
|
||||
} __attribute__((packed)) espnow_pkt_t;
|
||||
|
||||
// The maximum length of an espnow packet (bytes)
|
||||
static const size_t MAX_PACKET_LEN = (
|
||||
(sizeof(espnow_pkt_t) + ESP_NOW_MAX_DATA_LEN));
|
||||
|
||||
// Enough for 2 full-size packets: 2 * (6 + 7 + 250) = 526 bytes
|
||||
// Will allocate an additional 7 bytes for buffer overhead
|
||||
static const size_t DEFAULT_RECV_BUFFER_SIZE = (2 * MAX_PACKET_LEN);
|
||||
|
||||
// Default timeout (millisec) to wait for incoming ESPNow messages (5 minutes).
|
||||
static const size_t DEFAULT_RECV_TIMEOUT_MS = (5 * 60 * 1000);
|
||||
|
||||
// Time to wait (millisec) for responses from sent packets: (2 seconds).
|
||||
static const size_t DEFAULT_SEND_TIMEOUT_MS = (2 * 1000);
|
||||
|
||||
// Number of milliseconds to wait for pending responses to sent packets.
|
||||
// This is a fallback which should never be reached.
|
||||
static const mp_uint_t PENDING_RESPONSES_TIMEOUT_MS = 100;
|
||||
static const mp_uint_t PENDING_RESPONSES_BUSY_POLL_MS = 10;
|
||||
|
||||
// The data structure for the espnow_singleton.
|
||||
typedef struct _esp_espnow_obj_t {
|
||||
mp_obj_base_t base;
|
||||
|
||||
ringbuf_t *recv_buffer; // A buffer for received packets
|
||||
size_t recv_buffer_size; // The size of the recv_buffer
|
||||
mp_int_t recv_timeout_ms; // Timeout for recv()
|
||||
volatile size_t rx_packets; // # of received packets
|
||||
size_t dropped_rx_pkts; // # of dropped packets (buffer full)
|
||||
size_t tx_packets; // # of sent packets
|
||||
volatile size_t tx_responses; // # of sent packet responses received
|
||||
volatile size_t tx_failures; // # of sent packet responses failed
|
||||
size_t peer_count; // Cache the # of peers for send(sync=True)
|
||||
mp_obj_t recv_cb; // Callback when a packet is received
|
||||
mp_obj_t recv_cb_arg; // Argument passed to callback
|
||||
#if MICROPY_ESPNOW_RSSI
|
||||
mp_obj_t peers_table; // A dictionary of discovered peers
|
||||
#endif // MICROPY_ESPNOW_RSSI
|
||||
} esp_espnow_obj_t;
|
||||
|
||||
const mp_obj_type_t esp_espnow_type;
|
||||
|
||||
// ### Initialisation and Config functions
|
||||
//
|
||||
|
||||
// Return a pointer to the ESPNow module singleton
|
||||
// If state == INITIALISED check the device has been initialised.
|
||||
// Raises OSError if not initialised and state == INITIALISED.
|
||||
static esp_espnow_obj_t *_get_singleton() {
|
||||
return MP_STATE_PORT(espnow_singleton);
|
||||
}
|
||||
|
||||
static esp_espnow_obj_t *_get_singleton_initialised() {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
// assert(self);
|
||||
if (self->recv_buffer == NULL) {
|
||||
// Throw an espnow not initialised error
|
||||
check_esp_err(ESP_ERR_ESPNOW_NOT_INIT);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// Allocate and initialise the ESPNow module as a singleton.
|
||||
// Returns the initialised espnow_singleton.
|
||||
STATIC mp_obj_t espnow_make_new(const mp_obj_type_t *type, size_t n_args,
|
||||
size_t n_kw, const mp_obj_t *all_args) {
|
||||
|
||||
// The espnow_singleton must be defined in MICROPY_PORT_ROOT_POINTERS
|
||||
// (see mpconfigport.h) to prevent memory allocated here from being
|
||||
// garbage collected.
|
||||
// NOTE: on soft reset the espnow_singleton MUST be set to NULL and the
|
||||
// ESP-NOW functions de-initialised (see main.c).
|
||||
esp_espnow_obj_t *self = MP_STATE_PORT(espnow_singleton);
|
||||
if (self != NULL) {
|
||||
return self;
|
||||
}
|
||||
self = m_new_obj(esp_espnow_obj_t);
|
||||
self->base.type = &esp_espnow_type;
|
||||
self->recv_buffer_size = DEFAULT_RECV_BUFFER_SIZE;
|
||||
self->recv_timeout_ms = DEFAULT_RECV_TIMEOUT_MS;
|
||||
self->recv_buffer = NULL; // Buffer is allocated in espnow_init()
|
||||
self->recv_cb = mp_const_none;
|
||||
#if MICROPY_ESPNOW_RSSI
|
||||
self->peers_table = mp_obj_new_dict(0);
|
||||
// Prevent user code modifying the dict
|
||||
mp_obj_dict_get_map(self->peers_table)->is_fixed = 1;
|
||||
#endif // MICROPY_ESPNOW_RSSI
|
||||
|
||||
// Set the global singleton pointer for the espnow protocol.
|
||||
MP_STATE_PORT(espnow_singleton) = self;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// Forward declare the send and recv ESPNow callbacks
|
||||
STATIC void send_cb(const uint8_t *mac_addr, esp_now_send_status_t status);
|
||||
|
||||
STATIC void recv_cb(const uint8_t *mac_addr, const uint8_t *data, int len);
|
||||
|
||||
// ESPNow.init(): Initialise the data buffers and ESP-NOW functions.
|
||||
// Initialise the Espressif ESPNOW software stack, register callbacks and
|
||||
// allocate the recv data buffers.
|
||||
// Returns None.
|
||||
static mp_obj_t espnow_init(mp_obj_t _) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
if (self->recv_buffer == NULL) { // Already initialised
|
||||
self->recv_buffer = m_new_obj(ringbuf_t);
|
||||
ringbuf_alloc(self->recv_buffer, self->recv_buffer_size);
|
||||
|
||||
esp_initialise_wifi(); // Call the wifi init code in network_wlan.c
|
||||
check_esp_err(esp_now_init());
|
||||
check_esp_err(esp_now_register_recv_cb(recv_cb));
|
||||
check_esp_err(esp_now_register_send_cb(send_cb));
|
||||
}
|
||||
return mp_const_none;
|
||||
}
|
||||
|
||||
// ESPNow.deinit(): De-initialise the ESPNOW software stack, disable callbacks
|
||||
// and deallocate the recv data buffers.
|
||||
// Note: this function is called from main.c:mp_task() to cleanup before soft
|
||||
// reset, so cannot be declared STATIC and must guard against self == NULL;.
|
||||
mp_obj_t espnow_deinit(mp_obj_t _) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
if (self != NULL && self->recv_buffer != NULL) {
|
||||
check_esp_err(esp_now_unregister_recv_cb());
|
||||
check_esp_err(esp_now_unregister_send_cb());
|
||||
check_esp_err(esp_now_deinit());
|
||||
self->recv_buffer->buf = NULL;
|
||||
self->recv_buffer = NULL;
|
||||
self->peer_count = 0; // esp_now_deinit() removes all peers.
|
||||
self->tx_packets = self->tx_responses;
|
||||
}
|
||||
return mp_const_none;
|
||||
}
|
||||
|
||||
STATIC mp_obj_t espnow_active(size_t n_args, const mp_obj_t *args) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
if (n_args > 1) {
|
||||
if (mp_obj_is_true(args[1])) {
|
||||
espnow_init(self);
|
||||
} else {
|
||||
espnow_deinit(self);
|
||||
}
|
||||
}
|
||||
return self->recv_buffer != NULL ? mp_const_true : mp_const_false;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_active_obj, 1, 2, espnow_active);
|
||||
|
||||
// ESPNow.config(['param'|param=value, ..])
|
||||
// Get or set configuration values. Supported config params:
|
||||
// buffer: size of buffer for rx packets (default=514 bytes)
|
||||
// timeout: Default read timeout (default=300,000 milliseconds)
|
||||
STATIC mp_obj_t espnow_config(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
enum { ARG_get, ARG_buffer, ARG_timeout_ms, ARG_rate };
|
||||
static const mp_arg_t allowed_args[] = {
|
||||
{ MP_QSTR_, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
|
||||
{ MP_QSTR_buffer, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} },
|
||||
{ MP_QSTR_timeout_ms, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = INT_MIN} },
|
||||
{ MP_QSTR_rate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} },
|
||||
};
|
||||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
||||
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args,
|
||||
MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
||||
|
||||
if (args[ARG_buffer].u_int >= 0) {
|
||||
self->recv_buffer_size = args[ARG_buffer].u_int;
|
||||
}
|
||||
if (args[ARG_timeout_ms].u_int != INT_MIN) {
|
||||
self->recv_timeout_ms = args[ARG_timeout_ms].u_int;
|
||||
}
|
||||
if (args[ARG_rate].u_int >= 0) {
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 0)
|
||||
esp_initialise_wifi(); // Call the wifi init code in network_wlan.c
|
||||
check_esp_err(esp_wifi_config_espnow_rate(ESP_IF_WIFI_STA, args[ARG_rate].u_int));
|
||||
check_esp_err(esp_wifi_config_espnow_rate(ESP_IF_WIFI_AP, args[ARG_rate].u_int));
|
||||
#else
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("rate option not supported"));
|
||||
#endif
|
||||
}
|
||||
if (args[ARG_get].u_obj == MP_OBJ_NULL) {
|
||||
return mp_const_none;
|
||||
}
|
||||
#define QS(x) (uintptr_t)MP_OBJ_NEW_QSTR(x)
|
||||
// Return the value of the requested parameter
|
||||
uintptr_t name = (uintptr_t)args[ARG_get].u_obj;
|
||||
if (name == QS(MP_QSTR_buffer)) {
|
||||
return mp_obj_new_int(self->recv_buffer_size);
|
||||
} else if (name == QS(MP_QSTR_timeout_ms)) {
|
||||
return mp_obj_new_int(self->recv_timeout_ms);
|
||||
} else {
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("unknown config param"));
|
||||
}
|
||||
#undef QS
|
||||
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(espnow_config_obj, 1, espnow_config);
|
||||
|
||||
// ESPNow.irq(recv_cb)
|
||||
// Set callback function to be invoked when a message is received.
|
||||
STATIC mp_obj_t espnow_irq(size_t n_args, const mp_obj_t *args) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
mp_obj_t recv_cb = args[1];
|
||||
if (recv_cb != mp_const_none && !mp_obj_is_callable(recv_cb)) {
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("invalid handler"));
|
||||
}
|
||||
self->recv_cb = recv_cb;
|
||||
self->recv_cb_arg = (n_args > 2) ? args[2] : mp_const_none;
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_irq_obj, 2, 3, espnow_irq);
|
||||
|
||||
// ESPnow.stats(): Provide some useful stats.
|
||||
// Returns a tuple of:
|
||||
// (tx_pkts, tx_responses, tx_failures, rx_pkts, dropped_rx_pkts)
|
||||
STATIC mp_obj_t espnow_stats(mp_obj_t _) {
|
||||
const esp_espnow_obj_t *self = _get_singleton();
|
||||
return NEW_TUPLE(
|
||||
mp_obj_new_int(self->tx_packets),
|
||||
mp_obj_new_int(self->tx_responses),
|
||||
mp_obj_new_int(self->tx_failures),
|
||||
mp_obj_new_int(self->rx_packets),
|
||||
mp_obj_new_int(self->dropped_rx_pkts));
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_1(espnow_stats_obj, espnow_stats);
|
||||
|
||||
#if MICROPY_ESPNOW_RSSI
|
||||
// ### Maintaining the peer table and reading RSSI values
|
||||
//
|
||||
// We maintain a peers table for several reasons, to:
|
||||
// - support monitoring the RSSI values for all peers; and
|
||||
// - to return unique bytestrings for each peer which supports more efficient
|
||||
// application memory usage and peer handling.
|
||||
|
||||
// Get the RSSI value from the wifi packet header
|
||||
static inline int8_t _get_rssi_from_wifi_pkt(const uint8_t *msg) {
|
||||
// Warning: Secret magic to get the rssi from the wifi packet header
|
||||
// See espnow.c:espnow_recv_cb() at https://github.com/espressif/esp-now/
|
||||
// In the wifi packet the msg comes after a wifi_promiscuous_pkt_t
|
||||
// and a espnow_frame_format_t.
|
||||
// Backtrack to get a pointer to the wifi_promiscuous_pkt_t.
|
||||
static const size_t sizeof_espnow_frame_format = 39;
|
||||
wifi_promiscuous_pkt_t *wifi_pkt =
|
||||
(wifi_promiscuous_pkt_t *)(msg - sizeof_espnow_frame_format -
|
||||
sizeof(wifi_promiscuous_pkt_t));
|
||||
|
||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 2, 0)
|
||||
return wifi_pkt->rx_ctrl.rssi - 100; // Offset rssi for IDF 4.0.2
|
||||
#else
|
||||
return wifi_pkt->rx_ctrl.rssi;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Lookup a peer in the peers table and return a reference to the item in the
|
||||
// peers_table. Add peer to the table if it is not found (may alloc memory).
|
||||
// Will not return NULL.
|
||||
static mp_map_elem_t *_lookup_add_peer(esp_espnow_obj_t *self, const uint8_t *peer) {
|
||||
// We do not want to allocate any new memory in the case that the peer
|
||||
// already exists in the peers_table (which is almost all the time).
|
||||
// So, we use a byte string on the stack and look that up in the dict.
|
||||
mp_map_t *map = mp_obj_dict_get_map(self->peers_table);
|
||||
mp_obj_str_t peer_obj = {{&mp_type_bytes}, 0, ESP_NOW_ETH_ALEN, peer};
|
||||
mp_map_elem_t *item = mp_map_lookup(map, &peer_obj, MP_MAP_LOOKUP);
|
||||
if (item == NULL) {
|
||||
// If not found, add the peer using a new bytestring
|
||||
map->is_fixed = 0; // Allow to modify the dict
|
||||
mp_obj_t new_peer = mp_obj_new_bytes(peer, ESP_NOW_ETH_ALEN);
|
||||
item = mp_map_lookup(map, new_peer, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
|
||||
item->value = mp_obj_new_list(2, NULL);
|
||||
map->is_fixed = 1; // Relock the dict
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
// Update the peers table with the new rssi value from a received pkt and
|
||||
// return a reference to the item in the peers_table.
|
||||
static mp_map_elem_t *_update_rssi(const uint8_t *peer, int8_t rssi, uint32_t time_ms) {
|
||||
esp_espnow_obj_t *self = _get_singleton_initialised();
|
||||
// Lookup the peer in the device table
|
||||
mp_map_elem_t *item = _lookup_add_peer(self, peer);
|
||||
mp_obj_list_t *list = MP_OBJ_TO_PTR(item->value);
|
||||
list->items[0] = MP_OBJ_NEW_SMALL_INT(rssi);
|
||||
list->items[1] = mp_obj_new_int(time_ms);
|
||||
return item;
|
||||
}
|
||||
#endif // MICROPY_ESPNOW_RSSI
|
||||
|
||||
// Return C pointer to byte memory string/bytes/bytearray in obj.
|
||||
// Raise ValueError if the length does not match expected len.
|
||||
static uint8_t *_get_bytes_len_rw(mp_obj_t obj, size_t len, mp_uint_t rw) {
|
||||
mp_buffer_info_t bufinfo;
|
||||
mp_get_buffer_raise(obj, &bufinfo, rw);
|
||||
if (bufinfo.len != len) {
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("invalid buffer length"));
|
||||
}
|
||||
return (uint8_t *)bufinfo.buf;
|
||||
}
|
||||
|
||||
static uint8_t *_get_bytes_len(mp_obj_t obj, size_t len) {
|
||||
return _get_bytes_len_rw(obj, len, MP_BUFFER_READ);
|
||||
}
|
||||
|
||||
static uint8_t *_get_bytes_len_w(mp_obj_t obj, size_t len) {
|
||||
return _get_bytes_len_rw(obj, len, MP_BUFFER_WRITE);
|
||||
}
|
||||
|
||||
// Return C pointer to the MAC address.
|
||||
// Raise ValueError if mac_addr is wrong type or is not 6 bytes long.
|
||||
static const uint8_t *_get_peer(mp_obj_t mac_addr) {
|
||||
return mp_obj_is_true(mac_addr)
|
||||
? _get_bytes_len(mac_addr, ESP_NOW_ETH_ALEN) : NULL;
|
||||
}
|
||||
|
||||
// Copy data from the ring buffer - wait if buffer is empty up to timeout_ms
|
||||
// 0: Success
|
||||
// -1: Not enough data available to complete read (try again later)
|
||||
// -2: Requested read is larger than buffer - will never succeed
|
||||
static int ringbuf_get_bytes_wait(ringbuf_t *r, uint8_t *data, size_t len, mp_int_t timeout_ms) {
|
||||
mp_uint_t start = mp_hal_ticks_ms();
|
||||
int status = 0;
|
||||
while (((status = ringbuf_get_bytes(r, data, len)) == -1)
|
||||
&& (timeout_ms < 0 || (mp_uint_t)(mp_hal_ticks_ms() - start) < (mp_uint_t)timeout_ms)) {
|
||||
MICROPY_EVENT_POLL_HOOK;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
// ESPNow.recvinto(buffers[, timeout_ms]):
|
||||
// Waits for an espnow message and copies the peer_addr and message into
|
||||
// the buffers list.
|
||||
// Arguments:
|
||||
// buffers: (Optional) list of bytearrays to store return values.
|
||||
// timeout_ms: (Optional) timeout in milliseconds (or None).
|
||||
// Buffers should be a list: [bytearray(6), bytearray(250)]
|
||||
// If buffers is 4 elements long, the rssi and timestamp values will be
|
||||
// loaded into the 3rd and 4th elements.
|
||||
// Default timeout is set with ESPNow.config(timeout=milliseconds).
|
||||
// Return (None, None) on timeout.
|
||||
STATIC mp_obj_t espnow_recvinto(size_t n_args, const mp_obj_t *args) {
|
||||
esp_espnow_obj_t *self = _get_singleton_initialised();
|
||||
|
||||
mp_int_t timeout_ms = ((n_args > 2 && args[2] != mp_const_none)
|
||||
? mp_obj_get_int(args[2]) : self->recv_timeout_ms);
|
||||
|
||||
mp_obj_list_t *list = MP_OBJ_TO_PTR(args[1]);
|
||||
if (!mp_obj_is_type(list, &mp_type_list) || list->len < 2) {
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recvinto(): Invalid argument"));
|
||||
}
|
||||
mp_obj_array_t *msg = MP_OBJ_TO_PTR(list->items[1]);
|
||||
if (mp_obj_is_type(msg, &mp_type_bytearray)) {
|
||||
msg->len += msg->free; // Make all the space in msg array available
|
||||
msg->free = 0;
|
||||
}
|
||||
#if MICROPY_ESPNOW_RSSI
|
||||
uint8_t peer_buf[ESP_NOW_ETH_ALEN];
|
||||
#else
|
||||
uint8_t *peer_buf = _get_bytes_len_w(list->items[0], ESP_NOW_ETH_ALEN);
|
||||
#endif // MICROPY_ESPNOW_RSSI
|
||||
uint8_t *msg_buf = _get_bytes_len_w(msg, ESP_NOW_MAX_DATA_LEN);
|
||||
|
||||
// Read the packet header from the incoming buffer
|
||||
espnow_hdr_t hdr;
|
||||
if (ringbuf_get_bytes_wait(self->recv_buffer, (uint8_t *)&hdr, sizeof(hdr), timeout_ms) < 0) {
|
||||
return MP_OBJ_NEW_SMALL_INT(0); // Timeout waiting for packet
|
||||
}
|
||||
int msg_len = hdr.msg_len;
|
||||
|
||||
// Check the message packet header format and read the message data
|
||||
if (hdr.magic != ESPNOW_MAGIC
|
||||
|| msg_len > ESP_NOW_MAX_DATA_LEN
|
||||
|| ringbuf_get_bytes(self->recv_buffer, peer_buf, ESP_NOW_ETH_ALEN) < 0
|
||||
|| ringbuf_get_bytes(self->recv_buffer, msg_buf, msg_len) < 0) {
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recv(): buffer error"));
|
||||
}
|
||||
if (mp_obj_is_type(msg, &mp_type_bytearray)) {
|
||||
// Set the length of the message bytearray.
|
||||
size_t size = msg->len + msg->free;
|
||||
msg->len = msg_len;
|
||||
msg->free = size - msg_len;
|
||||
}
|
||||
|
||||
#if MICROPY_ESPNOW_RSSI
|
||||
// Update rssi value in the peer device table
|
||||
mp_map_elem_t *entry = _update_rssi(peer_buf, hdr.rssi, hdr.time_ms);
|
||||
list->items[0] = entry->key; // Set first element of list to peer
|
||||
if (list->len >= 4) {
|
||||
list->items[2] = MP_OBJ_NEW_SMALL_INT(hdr.rssi);
|
||||
list->items[3] = mp_obj_new_int(hdr.time_ms);
|
||||
}
|
||||
#endif // MICROPY_ESPNOW_RSSI
|
||||
|
||||
return MP_OBJ_NEW_SMALL_INT(msg_len);
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_recvinto_obj, 2, 3, espnow_recvinto);
|
||||
|
||||
// Test if data is available to read from the buffers
|
||||
STATIC mp_obj_t espnow_any(const mp_obj_t _) {
|
||||
esp_espnow_obj_t *self = _get_singleton_initialised();
|
||||
|
||||
return ringbuf_avail(self->recv_buffer) ? mp_const_true : mp_const_false;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_1(espnow_any_obj, espnow_any);
|
||||
|
||||
// Used by espnow_send() for sends() with sync==True.
|
||||
// Wait till all pending sent packet responses have been received.
|
||||
// ie. self->tx_responses == self->tx_packets.
|
||||
static void _wait_for_pending_responses(esp_espnow_obj_t *self) {
|
||||
mp_uint_t start = mp_hal_ticks_ms();
|
||||
mp_uint_t t;
|
||||
while (self->tx_responses < self->tx_packets) {
|
||||
if ((t = mp_hal_ticks_ms() - start) > PENDING_RESPONSES_TIMEOUT_MS) {
|
||||
mp_raise_OSError(MP_ETIMEDOUT);
|
||||
}
|
||||
if (t > PENDING_RESPONSES_BUSY_POLL_MS) {
|
||||
// After 10ms of busy waiting give other tasks a look in.
|
||||
MICROPY_EVENT_POLL_HOOK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ESPNow.send(peer_addr, message, [sync (=true), size])
|
||||
// ESPNow.send(message)
|
||||
// Send a message to the peer's mac address. Optionally wait for a response.
|
||||
// If peer_addr == None or any non-true value, send to all registered peers.
|
||||
// If sync == True, wait for response after sending.
|
||||
// If size is provided it should be the number of bytes in message to send().
|
||||
// Returns:
|
||||
// True if sync==False and message sent successfully.
|
||||
// True if sync==True and message is received successfully by all recipients
|
||||
// False if sync==True and message is not received by at least one recipient
|
||||
// Raises: EAGAIN if the internal espnow buffers are full.
|
||||
STATIC mp_obj_t espnow_send(size_t n_args, const mp_obj_t *args) {
|
||||
esp_espnow_obj_t *self = _get_singleton_initialised();
|
||||
// Check the various combinations of input arguments
|
||||
const uint8_t *peer = (n_args > 2) ? _get_peer(args[1]) : NULL;
|
||||
mp_obj_t msg = (n_args > 2) ? args[2] : (n_args == 2) ? args[1] : MP_OBJ_NULL;
|
||||
bool sync = n_args <= 3 || args[3] == mp_const_none || mp_obj_is_true(args[3]);
|
||||
|
||||
// Get a pointer to the data buffer of the message
|
||||
mp_buffer_info_t message;
|
||||
mp_get_buffer_raise(msg, &message, MP_BUFFER_READ);
|
||||
|
||||
if (sync) {
|
||||
// Flush out any pending responses.
|
||||
// If the last call was sync==False there may be outstanding responses
|
||||
// still to be received (possible many if we just had a burst of
|
||||
// unsync send()s). We need to wait for all pending responses if this
|
||||
// call has sync=True.
|
||||
_wait_for_pending_responses(self);
|
||||
}
|
||||
int saved_failures = self->tx_failures;
|
||||
// Send the packet - try, try again if internal esp-now buffers are full.
|
||||
esp_err_t err;
|
||||
mp_uint_t start = mp_hal_ticks_ms();
|
||||
while ((ESP_ERR_ESPNOW_NO_MEM == (err = esp_now_send(peer, message.buf, message.len)))
|
||||
&& (mp_uint_t)(mp_hal_ticks_ms() - start) < (mp_uint_t)DEFAULT_SEND_TIMEOUT_MS) {
|
||||
MICROPY_EVENT_POLL_HOOK;
|
||||
}
|
||||
check_esp_err(err); // Will raise OSError if e != ESP_OK
|
||||
// Increment the sent packet count. If peer_addr==NULL msg will be
|
||||
// sent to all peers EXCEPT any broadcast or multicast addresses.
|
||||
self->tx_packets += ((peer == NULL) ? self->peer_count : 1);
|
||||
if (sync) {
|
||||
// Wait for and tally all the expected responses from peers
|
||||
_wait_for_pending_responses(self);
|
||||
}
|
||||
// Return False if sync and any peers did not respond.
|
||||
return mp_obj_new_bool(!(sync && self->tx_failures != saved_failures));
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_send_obj, 2, 4, espnow_send);
|
||||
|
||||
// ### The ESP_Now send and recv callback routines
|
||||
//
|
||||
|
||||
// Callback triggered when a sent packet is acknowledged by the peer (or not).
|
||||
// Just count the number of responses and number of failures.
|
||||
// These are used in the send() logic.
|
||||
STATIC void send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
self->tx_responses++;
|
||||
if (status != ESP_NOW_SEND_SUCCESS) {
|
||||
self->tx_failures++;
|
||||
}
|
||||
}
|
||||
|
||||
// Callback triggered when an ESP-Now packet is received.
|
||||
// Write the peer MAC address and the message into the recv_buffer as an
|
||||
// ESPNow packet.
|
||||
// If the buffer is full, drop the message and increment the dropped count.
|
||||
// Schedules the user callback if one has been registered (ESPNow.config()).
|
||||
STATIC void recv_cb(const uint8_t *mac_addr, const uint8_t *msg, int msg_len) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
ringbuf_t *buf = self->recv_buffer;
|
||||
// TODO: Test this works with ">".
|
||||
if (sizeof(espnow_pkt_t) + msg_len >= ringbuf_free(buf)) {
|
||||
self->dropped_rx_pkts++;
|
||||
return;
|
||||
}
|
||||
espnow_hdr_t header;
|
||||
header.magic = ESPNOW_MAGIC;
|
||||
header.msg_len = msg_len;
|
||||
#if MICROPY_ESPNOW_RSSI
|
||||
header.rssi = _get_rssi_from_wifi_pkt(msg);
|
||||
header.time_ms = mp_hal_ticks_ms();
|
||||
#endif // MICROPY_ESPNOW_RSSI
|
||||
|
||||
ringbuf_put_bytes(buf, (uint8_t *)&header, sizeof(header));
|
||||
ringbuf_put_bytes(buf, mac_addr, ESP_NOW_ETH_ALEN);
|
||||
ringbuf_put_bytes(buf, msg, msg_len);
|
||||
self->rx_packets++;
|
||||
if (self->recv_cb != mp_const_none) {
|
||||
mp_sched_schedule(self->recv_cb, self->recv_cb_arg);
|
||||
}
|
||||
}
|
||||
|
||||
// ### Peer Management Functions
|
||||
//
|
||||
|
||||
// Set the ESP-NOW Primary Master Key (pmk) (for encrypted communications).
|
||||
// Raise OSError if ESP-NOW functions are not initialised.
|
||||
// Raise ValueError if key is not a bytes-like object exactly 16 bytes long.
|
||||
STATIC mp_obj_t espnow_set_pmk(mp_obj_t _, mp_obj_t key) {
|
||||
check_esp_err(esp_now_set_pmk(_get_bytes_len(key, ESP_NOW_KEY_LEN)));
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_set_pmk_obj, espnow_set_pmk);
|
||||
|
||||
// Common code for add_peer() and mod_peer() to process the args and kw_args:
|
||||
// Raise ValueError if the LMK is not a bytes-like object of exactly 16 bytes.
|
||||
// Raise TypeError if invalid keyword args or too many positional args.
|
||||
// Return true if all args parsed correctly.
|
||||
STATIC bool _update_peer_info(
|
||||
esp_now_peer_info_t *peer, size_t n_args,
|
||||
const mp_obj_t *pos_args, mp_map_t *kw_args) {
|
||||
|
||||
enum { ARG_lmk, ARG_channel, ARG_ifidx, ARG_encrypt };
|
||||
static const mp_arg_t allowed_args[] = {
|
||||
{ MP_QSTR_lmk, MP_ARG_OBJ, {.u_obj = mp_const_none} },
|
||||
{ MP_QSTR_channel, MP_ARG_OBJ, {.u_obj = mp_const_none} },
|
||||
{ MP_QSTR_ifidx, MP_ARG_OBJ, {.u_obj = mp_const_none} },
|
||||
{ MP_QSTR_encrypt, MP_ARG_OBJ, {.u_obj = mp_const_none} },
|
||||
};
|
||||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
||||
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
||||
if (args[ARG_lmk].u_obj != mp_const_none) {
|
||||
mp_obj_t obj = args[ARG_lmk].u_obj;
|
||||
peer->encrypt = mp_obj_is_true(obj);
|
||||
if (peer->encrypt) {
|
||||
// Key must be 16 bytes in length.
|
||||
memcpy(peer->lmk, _get_bytes_len(obj, ESP_NOW_KEY_LEN), ESP_NOW_KEY_LEN);
|
||||
}
|
||||
}
|
||||
if (args[ARG_channel].u_obj != mp_const_none) {
|
||||
peer->channel = mp_obj_get_int(args[ARG_channel].u_obj);
|
||||
}
|
||||
if (args[ARG_ifidx].u_obj != mp_const_none) {
|
||||
peer->ifidx = mp_obj_get_int(args[ARG_ifidx].u_obj);
|
||||
}
|
||||
if (args[ARG_encrypt].u_obj != mp_const_none) {
|
||||
peer->encrypt = mp_obj_is_true(args[ARG_encrypt].u_obj);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update the cached peer count in self->peer_count;
|
||||
// The peer_count ignores broadcast and multicast addresses and is used for the
|
||||
// send() logic and is updated from add_peer(), mod_peer() and del_peer().
|
||||
STATIC void _update_peer_count() {
|
||||
esp_espnow_obj_t *self = _get_singleton_initialised();
|
||||
|
||||
esp_now_peer_info_t peer = {0};
|
||||
bool from_head = true;
|
||||
int count = 0;
|
||||
// esp_now_fetch_peer() skips over any broadcast or multicast addresses
|
||||
while (esp_now_fetch_peer(from_head, &peer) == ESP_OK) {
|
||||
from_head = false;
|
||||
if (++count >= ESP_NOW_MAX_TOTAL_PEER_NUM) {
|
||||
break; // Should not happen
|
||||
}
|
||||
}
|
||||
self->peer_count = count;
|
||||
}
|
||||
|
||||
// ESPNow.add_peer(peer_mac, [lmk, [channel, [ifidx, [encrypt]]]]) or
|
||||
// ESPNow.add_peer(peer_mac, [lmk=b'0123456789abcdef'|b''|None|False],
|
||||
// [channel=1..11|0], [ifidx=0|1], [encrypt=True|False])
|
||||
// Positional args set to None will be left at defaults.
|
||||
// Raise OSError if ESPNow.init() has not been called.
|
||||
// Raise ValueError if mac or LMK are not bytes-like objects or wrong length.
|
||||
// Raise TypeError if invalid keyword args or too many positional args.
|
||||
// Return None.
|
||||
STATIC mp_obj_t espnow_add_peer(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args) {
|
||||
esp_now_peer_info_t peer = {0};
|
||||
memcpy(peer.peer_addr, _get_peer(args[1]), ESP_NOW_ETH_ALEN);
|
||||
_update_peer_info(&peer, n_args - 2, args + 2, kw_args);
|
||||
|
||||
check_esp_err(esp_now_add_peer(&peer));
|
||||
_update_peer_count();
|
||||
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(espnow_add_peer_obj, 2, espnow_add_peer);
|
||||
|
||||
// ESPNow.del_peer(peer_mac): Unregister peer_mac.
|
||||
// Raise OSError if ESPNow.init() has not been called.
|
||||
// Raise ValueError if peer is not a bytes-like objects or wrong length.
|
||||
// Return None.
|
||||
STATIC mp_obj_t espnow_del_peer(mp_obj_t _, mp_obj_t peer) {
|
||||
uint8_t peer_addr[ESP_NOW_ETH_ALEN];
|
||||
memcpy(peer_addr, _get_peer(peer), ESP_NOW_ETH_ALEN);
|
||||
|
||||
check_esp_err(esp_now_del_peer(peer_addr));
|
||||
_update_peer_count();
|
||||
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_del_peer_obj, espnow_del_peer);
|
||||
|
||||
// Convert a peer_info struct to python tuple
|
||||
// Used by espnow_get_peer() and espnow_get_peers()
|
||||
static mp_obj_t _peer_info_to_tuple(const esp_now_peer_info_t *peer) {
|
||||
return NEW_TUPLE(
|
||||
mp_obj_new_bytes(peer->peer_addr, MP_ARRAY_SIZE(peer->peer_addr)),
|
||||
mp_obj_new_bytes(peer->lmk, MP_ARRAY_SIZE(peer->lmk)),
|
||||
mp_obj_new_int(peer->channel),
|
||||
mp_obj_new_int(peer->ifidx),
|
||||
(peer->encrypt) ? mp_const_true : mp_const_false);
|
||||
}
|
||||
|
||||
// ESPNow.get_peers(): Fetch peer_info records for all registered ESPNow peers.
|
||||
// Raise OSError if ESPNow.init() has not been called.
|
||||
// Return a tuple of tuples:
|
||||
// ((peer_addr, lmk, channel, ifidx, encrypt),
|
||||
// (peer_addr, lmk, channel, ifidx, encrypt), ...)
|
||||
STATIC mp_obj_t espnow_get_peers(mp_obj_t _) {
|
||||
esp_espnow_obj_t *self = _get_singleton_initialised();
|
||||
|
||||
// Build and initialise the peer info tuple.
|
||||
mp_obj_tuple_t *peerinfo_tuple = mp_obj_new_tuple(self->peer_count, NULL);
|
||||
esp_now_peer_info_t peer = {0};
|
||||
for (int i = 0; i < peerinfo_tuple->len; i++) {
|
||||
int status = esp_now_fetch_peer((i == 0), &peer);
|
||||
peerinfo_tuple->items[i] =
|
||||
(status == ESP_OK ? _peer_info_to_tuple(&peer) : mp_const_none);
|
||||
}
|
||||
|
||||
return peerinfo_tuple;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_1(espnow_get_peers_obj, espnow_get_peers);
|
||||
|
||||
#if MICROPY_ESPNOW_EXTRA_PEER_METHODS
|
||||
// ESPNow.get_peer(peer_mac): Get the peer info for peer_mac as a tuple.
|
||||
// Raise OSError if ESPNow.init() has not been called.
|
||||
// Raise ValueError if mac or LMK are not bytes-like objects or wrong length.
|
||||
// Return a tuple of (peer_addr, lmk, channel, ifidx, encrypt).
|
||||
STATIC mp_obj_t espnow_get_peer(mp_obj_t _, mp_obj_t arg1) {
|
||||
esp_now_peer_info_t peer = {0};
|
||||
memcpy(peer.peer_addr, _get_peer(arg1), ESP_NOW_ETH_ALEN);
|
||||
|
||||
check_esp_err(esp_now_get_peer(peer.peer_addr, &peer));
|
||||
|
||||
return _peer_info_to_tuple(&peer);
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_get_peer_obj, espnow_get_peer);
|
||||
|
||||
// ESPNow.mod_peer(peer_mac, [lmk, [channel, [ifidx, [encrypt]]]]) or
|
||||
// ESPNow.mod_peer(peer_mac, [lmk=b'0123456789abcdef'|b''|None|False],
|
||||
// [channel=1..11|0], [ifidx=0|1], [encrypt=True|False])
|
||||
// Positional args set to None will be left at current values.
|
||||
// Raise OSError if ESPNow.init() has not been called.
|
||||
// Raise ValueError if mac or LMK are not bytes-like objects or wrong length.
|
||||
// Raise TypeError if invalid keyword args or too many positional args.
|
||||
// Return None.
|
||||
STATIC mp_obj_t espnow_mod_peer(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args) {
|
||||
esp_now_peer_info_t peer = {0};
|
||||
memcpy(peer.peer_addr, _get_peer(args[1]), ESP_NOW_ETH_ALEN);
|
||||
check_esp_err(esp_now_get_peer(peer.peer_addr, &peer));
|
||||
|
||||
_update_peer_info(&peer, n_args - 2, args + 2, kw_args);
|
||||
|
||||
check_esp_err(esp_now_mod_peer(&peer));
|
||||
_update_peer_count();
|
||||
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(espnow_mod_peer_obj, 2, espnow_mod_peer);
|
||||
|
||||
// ESPNow.espnow_peer_count(): Get the number of registered peers.
|
||||
// Raise OSError if ESPNow.init() has not been called.
|
||||
// Return a tuple of (num_total_peers, num_encrypted_peers).
|
||||
STATIC mp_obj_t espnow_peer_count(mp_obj_t _) {
|
||||
esp_now_peer_num_t peer_num = {0};
|
||||
check_esp_err(esp_now_get_peer_num(&peer_num));
|
||||
|
||||
return NEW_TUPLE(
|
||||
mp_obj_new_int(peer_num.total_num),
|
||||
mp_obj_new_int(peer_num.encrypt_num));
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_1(espnow_peer_count_obj, espnow_peer_count);
|
||||
#endif
|
||||
|
||||
STATIC const mp_rom_map_elem_t esp_espnow_locals_dict_table[] = {
|
||||
{ MP_ROM_QSTR(MP_QSTR_active), MP_ROM_PTR(&espnow_active_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_config), MP_ROM_PTR(&espnow_config_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_irq), MP_ROM_PTR(&espnow_irq_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_stats), MP_ROM_PTR(&espnow_stats_obj) },
|
||||
|
||||
// Send and receive messages
|
||||
{ MP_ROM_QSTR(MP_QSTR_recvinto), MP_ROM_PTR(&espnow_recvinto_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&espnow_send_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_any), MP_ROM_PTR(&espnow_any_obj) },
|
||||
|
||||
// Peer management functions
|
||||
{ MP_ROM_QSTR(MP_QSTR_set_pmk), MP_ROM_PTR(&espnow_set_pmk_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_add_peer), MP_ROM_PTR(&espnow_add_peer_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_del_peer), MP_ROM_PTR(&espnow_del_peer_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_get_peers), MP_ROM_PTR(&espnow_get_peers_obj) },
|
||||
#if MICROPY_ESPNOW_EXTRA_PEER_METHODS
|
||||
{ MP_ROM_QSTR(MP_QSTR_mod_peer), MP_ROM_PTR(&espnow_mod_peer_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_get_peer), MP_ROM_PTR(&espnow_get_peer_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_peer_count), MP_ROM_PTR(&espnow_peer_count_obj) },
|
||||
#endif // MICROPY_ESPNOW_EXTRA_PEER_METHODS
|
||||
};
|
||||
STATIC MP_DEFINE_CONST_DICT(esp_espnow_locals_dict, esp_espnow_locals_dict_table);
|
||||
|
||||
STATIC const mp_rom_map_elem_t espnow_globals_dict_table[] = {
|
||||
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR__espnow) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_ESPNowBase), MP_ROM_PTR(&esp_espnow_type) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_MAX_DATA_LEN), MP_ROM_INT(ESP_NOW_MAX_DATA_LEN)},
|
||||
{ MP_ROM_QSTR(MP_QSTR_ADDR_LEN), MP_ROM_INT(ESP_NOW_ETH_ALEN)},
|
||||
{ MP_ROM_QSTR(MP_QSTR_KEY_LEN), MP_ROM_INT(ESP_NOW_KEY_LEN)},
|
||||
{ MP_ROM_QSTR(MP_QSTR_MAX_TOTAL_PEER_NUM), MP_ROM_INT(ESP_NOW_MAX_TOTAL_PEER_NUM)},
|
||||
{ MP_ROM_QSTR(MP_QSTR_MAX_ENCRYPT_PEER_NUM), MP_ROM_INT(ESP_NOW_MAX_ENCRYPT_PEER_NUM)},
|
||||
};
|
||||
STATIC MP_DEFINE_CONST_DICT(espnow_globals_dict, espnow_globals_dict_table);
|
||||
|
||||
// ### Dummy Buffer Protocol support
|
||||
// ...so asyncio can poll.ipoll() on this device
|
||||
|
||||
// Support ioctl(MP_STREAM_POLL, ) for asyncio
|
||||
STATIC mp_uint_t espnow_stream_ioctl(
|
||||
mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) {
|
||||
if (request != MP_STREAM_POLL) {
|
||||
*errcode = MP_EINVAL;
|
||||
return MP_STREAM_ERROR;
|
||||
}
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
return (self->recv_buffer == NULL) ? 0 : // If not initialised
|
||||
arg ^ (
|
||||
// If no data in the buffer, unset the Read ready flag
|
||||
((ringbuf_avail(self->recv_buffer) == 0) ? MP_STREAM_POLL_RD : 0) |
|
||||
// If still waiting for responses, unset the Write ready flag
|
||||
((self->tx_responses < self->tx_packets) ? MP_STREAM_POLL_WR : 0));
|
||||
}
|
||||
|
||||
STATIC const mp_stream_p_t espnow_stream_p = {
|
||||
.ioctl = espnow_stream_ioctl,
|
||||
};
|
||||
|
||||
#if MICROPY_ESPNOW_RSSI
|
||||
// Return reference to the dictionary of peers we have seen:
|
||||
// {peer1: (rssi, time_sec), peer2: (rssi, time_msec), ...}
|
||||
// where:
|
||||
// peerX is a byte string containing the 6-byte mac address of the peer,
|
||||
// rssi is the wifi signal strength from the last msg received
|
||||
// (in dBm from -127 to 0)
|
||||
// time_sec is the time in milliseconds since device last booted.
|
||||
STATIC void espnow_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
if (dest[0] != MP_OBJ_NULL) { // Only allow "Load" operation
|
||||
return;
|
||||
}
|
||||
if (attr == MP_QSTR_peers_table) {
|
||||
dest[0] = self->peers_table;
|
||||
return;
|
||||
}
|
||||
dest[1] = MP_OBJ_SENTINEL; // Attribute not found
|
||||
}
|
||||
#endif // MICROPY_ESPNOW_RSSI
|
||||
|
||||
MP_DEFINE_CONST_OBJ_TYPE(
|
||||
esp_espnow_type,
|
||||
MP_QSTR_ESPNowBase,
|
||||
MP_TYPE_FLAG_NONE,
|
||||
make_new, espnow_make_new,
|
||||
#if MICROPY_ESPNOW_RSSI
|
||||
attr, espnow_attr,
|
||||
#endif // MICROPY_ESPNOW_RSSI
|
||||
protocol, &espnow_stream_p,
|
||||
locals_dict, &esp_espnow_locals_dict
|
||||
);
|
||||
|
||||
const mp_obj_module_t mp_module_espnow = {
|
||||
.base = { &mp_type_module },
|
||||
.globals = (mp_obj_dict_t *)&espnow_globals_dict,
|
||||
};
|
||||
|
||||
MP_REGISTER_MODULE(MP_QSTR__espnow, mp_module_espnow);
|
||||
MP_REGISTER_ROOT_POINTER(struct _esp_espnow_obj_t *espnow_singleton);
|
30
ports/esp32/modespnow.h
Normal file
30
ports/esp32/modespnow.h
Normal file
@ -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 _);
|
@ -63,5 +63,6 @@ static inline void esp_exceptions(esp_err_t e) {
|
||||
|
||||
void usocket_events_deinit(void);
|
||||
void network_wlan_event_handler(system_event_t *event);
|
||||
void esp_initialise_wifi(void);
|
||||
|
||||
#endif
|
||||
|
30
ports/esp32/modules/espnow.py
Normal file
30
ports/esp32/modules/espnow.py
Normal file
@ -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
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -70,6 +70,11 @@ LD_FILES ?= boards/esp8266_2m.ld
|
||||
LDFLAGS += -nostdlib -T $(LD_FILES) -Map=$(@:.elf=.map) --cref
|
||||
LIBS += -L$(ESP_SDK)/lib -lmain -ljson -llwip_open -lpp -lnet80211 -lwpa -lphy -lnet80211
|
||||
|
||||
ifeq ($(MICROPY_ESPNOW),1)
|
||||
CFLAGS += -DMICROPY_ESPNOW=1
|
||||
LIBS += -lespnow
|
||||
endif
|
||||
|
||||
LIBGCC_FILE_NAME = $(shell $(CC) $(CFLAGS) -print-libgcc-file-name)
|
||||
LIBS += -L$(dir $(LIBGCC_FILE_NAME)) -lgcc
|
||||
|
||||
@ -113,6 +118,11 @@ SRC_C = \
|
||||
hspi.c \
|
||||
$(wildcard $(BOARD_DIR)/*.c) \
|
||||
|
||||
ifeq ($(MICROPY_ESPNOW),1)
|
||||
SRC_C += \
|
||||
modespnow.c
|
||||
endif
|
||||
|
||||
LIB_SRC_C = $(addprefix lib/,\
|
||||
libm/math.c \
|
||||
libm/fmodf.c \
|
||||
|
@ -1,5 +1,6 @@
|
||||
LD_FILES = boards/esp8266_2m.ld
|
||||
|
||||
MICROPY_ESPNOW ?= 1
|
||||
MICROPY_PY_BTREE ?= 1
|
||||
MICROPY_VFS_FAT ?= 1
|
||||
MICROPY_VFS_LFS2 ?= 1
|
||||
|
@ -1,4 +1,5 @@
|
||||
LD_FILES = boards/esp8266_1m.ld
|
||||
|
||||
MICROPY_ESPNOW ?= 1
|
||||
MICROPY_PY_BTREE ?= 1
|
||||
MICROPY_VFS_LFS2 ?= 1
|
||||
|
@ -83,6 +83,7 @@ SECTIONS
|
||||
*libnet80211.a:(.literal.* .text.*)
|
||||
*libwpa.a:(.literal.* .text.*)
|
||||
*libwpa2.a:(.literal.* .text.*)
|
||||
*libespnow.a:(.literal.* .text.*)
|
||||
|
||||
/* we put some specific text in this section */
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
freeze("$(PORT_DIR)/modules")
|
||||
# require("aioespnow")
|
||||
require("bundle-networking")
|
||||
require("dht")
|
||||
require("ds18x20")
|
||||
|
@ -45,6 +45,10 @@
|
||||
#include "gccollect.h"
|
||||
#include "user_interface.h"
|
||||
|
||||
#if MICROPY_ESPNOW
|
||||
#include "modespnow.h"
|
||||
#endif
|
||||
|
||||
STATIC char heap[38 * 1024];
|
||||
|
||||
STATIC void mp_reset(void) {
|
||||
@ -73,6 +77,10 @@ STATIC void mp_reset(void) {
|
||||
mp_uos_dupterm_obj.fun.var(2, args);
|
||||
}
|
||||
|
||||
#if MICROPY_ESPNOW
|
||||
espnow_deinit(mp_const_none);
|
||||
#endif
|
||||
|
||||
#if MICROPY_MODULE_FROZEN
|
||||
pyexec_frozen_module("_boot.py", false);
|
||||
pyexec_file_if_exists("boot.py");
|
||||
|
507
ports/esp8266/modespnow.c
Normal file
507
ports/esp8266/modespnow.c
Normal file
@ -0,0 +1,507 @@
|
||||
/*
|
||||
* This file is part of the MicroPython project, http://micropython.org/
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2017-2020 Nick Moore
|
||||
* Copyright (c) 2018 shawwwn <shawwwn1@gmail.com>
|
||||
* Copyright (c) 2020-2021 Glenn Moloney @glenn20
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "py/runtime.h"
|
||||
|
||||
#if MICROPY_ESPNOW
|
||||
|
||||
#include "c_types.h"
|
||||
#include "espnow.h"
|
||||
|
||||
#include "py/mphal.h"
|
||||
#include "py/mperrno.h"
|
||||
#include "py/qstr.h"
|
||||
#include "py/objstr.h"
|
||||
#include "py/objarray.h"
|
||||
#include "py/stream.h"
|
||||
#include "py/binary.h"
|
||||
#include "py/ringbuf.h"
|
||||
|
||||
#include "mpconfigport.h"
|
||||
|
||||
#include "modespnow.h"
|
||||
|
||||
// For the esp8266
|
||||
#define ESP_NOW_MAX_DATA_LEN (250)
|
||||
#define ESP_NOW_KEY_LEN (16)
|
||||
#define ESP_NOW_ETH_ALEN (6)
|
||||
#define ESP_NOW_SEND_SUCCESS (0)
|
||||
#define ESP_ERR_ESPNOW_NO_MEM (-77777)
|
||||
#define ESP_OK (0)
|
||||
#define ESP_NOW_MAX_TOTAL_PEER_NUM (20)
|
||||
#define ESP_NOW_MAX_ENCRYPT_PEER_NUM (6)
|
||||
#define ESP_ERR_ESPNOW_NOT_INIT (0x300 + 100 + 1)
|
||||
typedef int esp_err_t;
|
||||
|
||||
static const uint8_t ESPNOW_MAGIC = 0x99;
|
||||
|
||||
// Use this for peeking at the header of the next packet in the buffer.
|
||||
typedef struct {
|
||||
uint8_t magic; // = ESPNOW_MAGIC
|
||||
uint8_t msg_len; // Length of the message
|
||||
} __attribute__((packed)) espnow_hdr_t;
|
||||
|
||||
// ESPNow packet format for the receive buffer.
|
||||
typedef struct {
|
||||
espnow_hdr_t hdr; // The header
|
||||
uint8_t peer[6]; // Peer address
|
||||
uint8_t msg[0]; // Message is up to 250 bytes
|
||||
} __attribute__((packed)) espnow_pkt_t;
|
||||
|
||||
// The maximum length of an espnow packet (bytes)
|
||||
static const size_t MAX_PACKET_LEN = (
|
||||
sizeof(espnow_pkt_t) + ESP_NOW_MAX_DATA_LEN);
|
||||
|
||||
// Enough for 2 full-size packets: 2 * (6 + 2 + 250) = 516 bytes
|
||||
// Will allocate an additional 7 bytes for buffer overhead
|
||||
#define DEFAULT_RECV_BUFFER_SIZE \
|
||||
(2 * (sizeof(espnow_pkt_t) + ESP_NOW_MAX_DATA_LEN))
|
||||
|
||||
// Default timeout (millisec) to wait for incoming ESPNow messages (5 minutes).
|
||||
#define DEFAULT_RECV_TIMEOUT_MS (5 * 60 * 1000)
|
||||
|
||||
// Number of milliseconds to wait for pending responses to sent packets.
|
||||
// This is a fallback which should never be reached.
|
||||
#define PENDING_RESPONSES_TIMEOUT_MS 100
|
||||
|
||||
// The data structure for the espnow_singleton.
|
||||
typedef struct _esp_espnow_obj_t {
|
||||
mp_obj_base_t base;
|
||||
ringbuf_t *recv_buffer; // A buffer for received packets
|
||||
size_t recv_buffer_size; // Size of recv buffer
|
||||
size_t recv_timeout_ms; // Timeout for irecv()
|
||||
size_t tx_packets; // Count of sent packets
|
||||
volatile size_t tx_responses; // # of sent packet responses received
|
||||
volatile size_t tx_failures; // # of sent packet responses failed
|
||||
} esp_espnow_obj_t;
|
||||
|
||||
// Initialised below.
|
||||
const mp_obj_type_t esp_espnow_type;
|
||||
|
||||
static esp_espnow_obj_t espnow_singleton = {
|
||||
.base.type = &esp_espnow_type,
|
||||
.recv_buffer = NULL,
|
||||
.recv_buffer_size = DEFAULT_RECV_BUFFER_SIZE,
|
||||
.recv_timeout_ms = DEFAULT_RECV_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
// ### Initialisation and Config functions
|
||||
//
|
||||
|
||||
static void check_esp_err(int e) {
|
||||
if (e != 0) {
|
||||
mp_raise_OSError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Return a pointer to the ESPNow module singleton
|
||||
// If state == INITIALISED check the device has been initialised.
|
||||
// Raises OSError if not initialised and state == INITIALISED.
|
||||
static esp_espnow_obj_t *_get_singleton() {
|
||||
return &espnow_singleton;
|
||||
}
|
||||
|
||||
static esp_espnow_obj_t *_get_singleton_initialised() {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
if (self->recv_buffer == NULL) {
|
||||
// Throw an espnow not initialised error
|
||||
check_esp_err(ESP_ERR_ESPNOW_NOT_INIT);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// Allocate and initialise the ESPNow module as a singleton.
|
||||
// Returns the initialised espnow_singleton.
|
||||
STATIC mp_obj_t espnow_make_new(const mp_obj_type_t *type, size_t n_args,
|
||||
size_t n_kw, const mp_obj_t *all_args) {
|
||||
|
||||
return _get_singleton();
|
||||
}
|
||||
|
||||
// Forward declare the send and recv ESPNow callbacks
|
||||
STATIC void send_cb(uint8_t *mac_addr, uint8_t status);
|
||||
|
||||
STATIC void recv_cb(uint8_t *mac_addr, uint8_t *data, uint8_t len);
|
||||
|
||||
// ESPNow.deinit(): De-initialise the ESPNOW software stack, disable callbacks
|
||||
// and deallocate the recv data buffers.
|
||||
// Note: this function is called from main.c:mp_task() to cleanup before soft
|
||||
// reset, so cannot be declared STATIC and must guard against self == NULL;.
|
||||
mp_obj_t espnow_deinit(mp_obj_t _) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
if (self->recv_buffer != NULL) {
|
||||
// esp_now_unregister_recv_cb();
|
||||
esp_now_deinit();
|
||||
self->recv_buffer->buf = NULL;
|
||||
self->recv_buffer = NULL;
|
||||
self->tx_packets = self->tx_responses;
|
||||
}
|
||||
MP_STATE_PORT(espnow_buffer) = NULL;
|
||||
return mp_const_none;
|
||||
}
|
||||
|
||||
// ESPNow.active(): Initialise the data buffers and ESP-NOW functions.
|
||||
// Initialise the Espressif ESPNOW software stack, register callbacks and
|
||||
// allocate the recv data buffers.
|
||||
// Returns True if interface is active, else False.
|
||||
STATIC mp_obj_t espnow_active(size_t n_args, const mp_obj_t *args) {
|
||||
esp_espnow_obj_t *self = args[0];
|
||||
if (n_args > 1) {
|
||||
if (mp_obj_is_true(args[1])) {
|
||||
if (self->recv_buffer == NULL) { // Already initialised
|
||||
self->recv_buffer = m_new_obj(ringbuf_t);
|
||||
ringbuf_alloc(self->recv_buffer, self->recv_buffer_size);
|
||||
MP_STATE_PORT(espnow_buffer) = self->recv_buffer;
|
||||
esp_now_init();
|
||||
esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
|
||||
esp_now_register_recv_cb(recv_cb);
|
||||
esp_now_register_send_cb(send_cb);
|
||||
}
|
||||
} else {
|
||||
espnow_deinit(self);
|
||||
}
|
||||
}
|
||||
return mp_obj_new_bool(self->recv_buffer != NULL);
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_active_obj, 1, 2, espnow_active);
|
||||
|
||||
// ESPNow.config(): Initialise the data buffers and ESP-NOW functions.
|
||||
// Initialise the Espressif ESPNOW software stack, register callbacks and
|
||||
// allocate the recv data buffers.
|
||||
// Returns True if interface is active, else False.
|
||||
STATIC mp_obj_t espnow_config(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
enum { ARG_rxbuf, ARG_timeout_ms };
|
||||
static const mp_arg_t allowed_args[] = {
|
||||
{ MP_QSTR_rxbuf, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} },
|
||||
{ MP_QSTR_timeout_ms, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} },
|
||||
};
|
||||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
||||
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args,
|
||||
MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
||||
if (args[ARG_rxbuf].u_int >= 0) {
|
||||
self->recv_buffer_size = args[ARG_rxbuf].u_int;
|
||||
}
|
||||
if (args[ARG_timeout_ms].u_int >= 0) {
|
||||
self->recv_timeout_ms = args[ARG_timeout_ms].u_int;
|
||||
}
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(espnow_config_obj, 1, espnow_config);
|
||||
|
||||
// ### The ESP_Now send and recv callback routines
|
||||
//
|
||||
|
||||
// Callback triggered when a sent packet is acknowledged by the peer (or not).
|
||||
// Just count the number of responses and number of failures.
|
||||
// These are used in the send()/write() logic.
|
||||
STATIC void send_cb(uint8_t *mac_addr, uint8_t status) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
self->tx_responses++;
|
||||
if (status != ESP_NOW_SEND_SUCCESS) {
|
||||
self->tx_failures++;
|
||||
}
|
||||
}
|
||||
|
||||
// Callback triggered when an ESP-Now packet is received.
|
||||
// Write the peer MAC address and the message into the recv_buffer as an
|
||||
// ESPNow packet.
|
||||
// If the buffer is full, drop the message and increment the dropped count.
|
||||
// Schedules the user callback if one has been registered (ESPNow.config()).
|
||||
STATIC void recv_cb(uint8_t *mac_addr, uint8_t *msg, uint8_t msg_len) {
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
ringbuf_t *buf = self->recv_buffer;
|
||||
// TODO: Test this works with ">".
|
||||
if (buf == NULL || sizeof(espnow_pkt_t) + msg_len >= ringbuf_free(buf)) {
|
||||
return;
|
||||
}
|
||||
espnow_hdr_t header;
|
||||
header.magic = ESPNOW_MAGIC;
|
||||
header.msg_len = msg_len;
|
||||
|
||||
ringbuf_put_bytes(buf, (uint8_t *)&header, sizeof(header));
|
||||
ringbuf_put_bytes(buf, mac_addr, ESP_NOW_ETH_ALEN);
|
||||
ringbuf_put_bytes(buf, msg, msg_len);
|
||||
}
|
||||
|
||||
// Return C pointer to byte memory string/bytes/bytearray in obj.
|
||||
// Raise ValueError if the length does not match expected len.
|
||||
static uint8_t *_get_bytes_len_rw(mp_obj_t obj, size_t len, mp_uint_t rw) {
|
||||
mp_buffer_info_t bufinfo;
|
||||
mp_get_buffer_raise(obj, &bufinfo, rw);
|
||||
if (bufinfo.len != len) {
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("invalid buffer length"));
|
||||
}
|
||||
return (uint8_t *)bufinfo.buf;
|
||||
}
|
||||
|
||||
static uint8_t *_get_bytes_len(mp_obj_t obj, size_t len) {
|
||||
return _get_bytes_len_rw(obj, len, MP_BUFFER_READ);
|
||||
}
|
||||
|
||||
static uint8_t *_get_bytes_len_w(mp_obj_t obj, size_t len) {
|
||||
return _get_bytes_len_rw(obj, len, MP_BUFFER_WRITE);
|
||||
}
|
||||
|
||||
// ### Handling espnow packets in the recv buffer
|
||||
//
|
||||
|
||||
// Copy data from the ring buffer - wait if buffer is empty up to timeout_ms
|
||||
// 0: Success
|
||||
// -1: Not enough data available to complete read (try again later)
|
||||
// -2: Requested read is larger than buffer - will never succeed
|
||||
static int ringbuf_get_bytes_wait(ringbuf_t *r, uint8_t *data, size_t len, mp_int_t timeout_ms) {
|
||||
mp_uint_t start = mp_hal_ticks_ms();
|
||||
int status = 0;
|
||||
while (((status = ringbuf_get_bytes(r, data, len)) == -1)
|
||||
&& (timeout_ms < 0 || (mp_uint_t)(mp_hal_ticks_ms() - start) < (mp_uint_t)timeout_ms)) {
|
||||
MICROPY_EVENT_POLL_HOOK;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
// ESPNow.recvinto([timeout_ms, []]):
|
||||
// Returns a list of byte strings: (peer_addr, message) where peer_addr is
|
||||
// the MAC address of the sending peer.
|
||||
// Arguments:
|
||||
// timeout_ms: timeout in milliseconds (or None).
|
||||
// buffers: list of bytearrays to store values: [peer, message].
|
||||
// Default timeout is set with ESPNow.config(timeout=milliseconds).
|
||||
// Return (None, None) on timeout.
|
||||
STATIC mp_obj_t espnow_recvinto(size_t n_args, const mp_obj_t *args) {
|
||||
esp_espnow_obj_t *self = _get_singleton_initialised();
|
||||
|
||||
size_t timeout_ms = ((n_args > 2 && args[2] != mp_const_none)
|
||||
? mp_obj_get_int(args[2]) : self->recv_timeout_ms);
|
||||
|
||||
mp_obj_list_t *list = MP_OBJ_TO_PTR(args[1]);
|
||||
if (!mp_obj_is_type(list, &mp_type_list) || list->len < 2) {
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recvinto(): Invalid argument"));
|
||||
}
|
||||
mp_obj_array_t *msg = MP_OBJ_TO_PTR(list->items[1]);
|
||||
size_t msg_size = msg->len + msg->free;
|
||||
if (mp_obj_is_type(msg, &mp_type_bytearray)) {
|
||||
msg->len = msg_size; // Make all the space in msg array available
|
||||
msg->free = 0;
|
||||
}
|
||||
uint8_t *peer_buf = _get_bytes_len_w(list->items[0], ESP_NOW_ETH_ALEN);
|
||||
uint8_t *msg_buf = _get_bytes_len_w(msg, ESP_NOW_MAX_DATA_LEN);
|
||||
|
||||
// Read the packet header from the incoming buffer
|
||||
espnow_hdr_t hdr;
|
||||
if (ringbuf_get_bytes_wait(self->recv_buffer, (uint8_t *)&hdr, sizeof(hdr), timeout_ms) < 0) {
|
||||
return MP_OBJ_NEW_SMALL_INT(0); // Timeout waiting for packet
|
||||
}
|
||||
int msg_len = hdr.msg_len;
|
||||
|
||||
// Check the message packet header format and read the message data
|
||||
if (hdr.magic != ESPNOW_MAGIC
|
||||
|| msg_len > ESP_NOW_MAX_DATA_LEN
|
||||
|| ringbuf_get_bytes(self->recv_buffer, peer_buf, ESP_NOW_ETH_ALEN) < 0
|
||||
|| ringbuf_get_bytes(self->recv_buffer, msg_buf, msg_len) < 0) {
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recv(): buffer error"));
|
||||
}
|
||||
if (mp_obj_is_type(msg, &mp_type_bytearray)) {
|
||||
// Set the length of the message bytearray.
|
||||
msg->len = msg_len;
|
||||
msg->free = msg_size - msg_len;
|
||||
}
|
||||
|
||||
return MP_OBJ_NEW_SMALL_INT(msg_len);
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_recvinto_obj, 2, 3, espnow_recvinto);
|
||||
|
||||
// Used by espnow_send() for sends() with sync==True.
|
||||
// Wait till all pending sent packet responses have been received.
|
||||
// ie. self->tx_responses == self->tx_packets.
|
||||
// Return the number of responses where status != ESP_NOW_SEND_SUCCESS.
|
||||
static void _wait_for_pending_responses(esp_espnow_obj_t *self) {
|
||||
for (int i = 0; i < PENDING_RESPONSES_TIMEOUT_MS; i++) {
|
||||
if (self->tx_responses >= self->tx_packets) {
|
||||
return;
|
||||
}
|
||||
mp_hal_delay_ms(1); // Allow other tasks to run
|
||||
}
|
||||
// Note: the loop timeout is just a fallback - in normal operation
|
||||
// we should never reach that timeout.
|
||||
}
|
||||
|
||||
// ESPNow.send(peer_addr, message, [sync (=true)])
|
||||
// ESPNow.send(message)
|
||||
// Send a message to the peer's mac address. Optionally wait for a response.
|
||||
// If sync == True, wait for response after sending.
|
||||
// Returns:
|
||||
// True if sync==False and message sent successfully.
|
||||
// True if sync==True and message is received successfully by all recipients
|
||||
// False if sync==True and message is not received by at least one recipient
|
||||
// Raises: EAGAIN if the internal espnow buffers are full.
|
||||
STATIC mp_obj_t espnow_send(size_t n_args, const mp_obj_t *args) {
|
||||
esp_espnow_obj_t *self = _get_singleton_initialised();
|
||||
|
||||
bool sync = n_args <= 3 || args[3] == mp_const_none || mp_obj_is_true(args[3]);
|
||||
// Get a pointer to the buffer of obj
|
||||
mp_buffer_info_t message;
|
||||
mp_get_buffer_raise(args[2], &message, MP_BUFFER_READ);
|
||||
|
||||
// Bugfix: esp_now_send() generates a panic if message buffer points
|
||||
// to an address in ROM (eg. a statically interned QSTR).
|
||||
// Fix: if message is not in gc pool, copy to a temp buffer.
|
||||
static char temp[ESP_NOW_MAX_DATA_LEN]; // Static to save code space
|
||||
byte *p = (byte *)message.buf;
|
||||
// if (p < MP_STATE_MEM(area.gc_pool_start) || MP_STATE_MEM(area.gc_pool_end) < p) {
|
||||
if (MP_STATE_MEM(area.gc_pool_end) < p) {
|
||||
// If buffer is not in GC pool copy from ROM to stack
|
||||
memcpy(temp, message.buf, message.len);
|
||||
message.buf = temp;
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
// If the last call was sync==False there may be outstanding responses.
|
||||
// We need to wait for all pending responses if this call has sync=True.
|
||||
_wait_for_pending_responses(self);
|
||||
}
|
||||
int saved_failures = self->tx_failures;
|
||||
|
||||
check_esp_err(
|
||||
esp_now_send(_get_bytes_len(args[1], ESP_NOW_ETH_ALEN), message.buf, message.len));
|
||||
self->tx_packets++;
|
||||
if (sync) {
|
||||
// Wait for message to be received by peer
|
||||
_wait_for_pending_responses(self);
|
||||
}
|
||||
// Return False if sync and any peers did not respond.
|
||||
return mp_obj_new_bool(!(sync && self->tx_failures != saved_failures));
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_send_obj, 3, 4, espnow_send);
|
||||
|
||||
// ### Peer Management Functions
|
||||
//
|
||||
|
||||
// Set the ESP-NOW Primary Master Key (pmk) (for encrypted communications).
|
||||
// Raise OSError if not initialised.
|
||||
// Raise ValueError if key is not a bytes-like object exactly 16 bytes long.
|
||||
STATIC mp_obj_t espnow_set_pmk(mp_obj_t _, mp_obj_t key) {
|
||||
check_esp_err(esp_now_set_kok(_get_bytes_len(key, ESP_NOW_KEY_LEN), ESP_NOW_KEY_LEN));
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_set_pmk_obj, espnow_set_pmk);
|
||||
|
||||
// ESPNow.add_peer(peer_mac, [lmk, [channel, [ifidx, [encrypt]]]])
|
||||
// Positional args set to None will be left at defaults.
|
||||
// Raise OSError if not initialised.
|
||||
// Raise ValueError if mac or LMK are not bytes-like objects or wrong length.
|
||||
// Raise TypeError if invalid keyword args or too many positional args.
|
||||
// Return None.
|
||||
STATIC mp_obj_t espnow_add_peer(size_t n_args, const mp_obj_t *args) {
|
||||
check_esp_err(
|
||||
esp_now_add_peer(
|
||||
_get_bytes_len(args[1], ESP_NOW_ETH_ALEN),
|
||||
ESP_NOW_ROLE_COMBO,
|
||||
(n_args > 3) ? mp_obj_get_int(args[3]) : 0,
|
||||
(n_args > 2) ? _get_bytes_len(args[2], ESP_NOW_KEY_LEN) : NULL,
|
||||
ESP_NOW_KEY_LEN));
|
||||
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_add_peer_obj, 2, 4, espnow_add_peer);
|
||||
|
||||
// ESPNow.del_peer(peer_mac): Unregister peer_mac.
|
||||
// Raise OSError if not initialised.
|
||||
// Raise ValueError if peer is not a bytes-like objects or wrong length.
|
||||
// Return None.
|
||||
STATIC mp_obj_t espnow_del_peer(mp_obj_t _, mp_obj_t peer) {
|
||||
esp_now_del_peer(_get_bytes_len(peer, ESP_NOW_ETH_ALEN));
|
||||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_2(espnow_del_peer_obj, espnow_del_peer);
|
||||
|
||||
STATIC const mp_rom_map_elem_t esp_espnow_locals_dict_table[] = {
|
||||
{ MP_ROM_QSTR(MP_QSTR_active), MP_ROM_PTR(&espnow_active_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_config), MP_ROM_PTR(&espnow_config_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_recvinto), MP_ROM_PTR(&espnow_recvinto_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&espnow_send_obj) },
|
||||
|
||||
// Peer management functions
|
||||
{ MP_ROM_QSTR(MP_QSTR_set_pmk), MP_ROM_PTR(&espnow_set_pmk_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_add_peer), MP_ROM_PTR(&espnow_add_peer_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_del_peer), MP_ROM_PTR(&espnow_del_peer_obj) },
|
||||
};
|
||||
STATIC MP_DEFINE_CONST_DICT(esp_espnow_locals_dict, esp_espnow_locals_dict_table);
|
||||
|
||||
STATIC const mp_rom_map_elem_t espnow_globals_dict_table[] = {
|
||||
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR__espnow) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_ESPNowBase), MP_ROM_PTR(&esp_espnow_type) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_MAX_DATA_LEN), MP_ROM_INT(ESP_NOW_MAX_DATA_LEN)},
|
||||
{ MP_ROM_QSTR(MP_QSTR_ADDR_LEN), MP_ROM_INT(ESP_NOW_ETH_ALEN)},
|
||||
{ MP_ROM_QSTR(MP_QSTR_KEY_LEN), MP_ROM_INT(ESP_NOW_KEY_LEN)},
|
||||
{ MP_ROM_QSTR(MP_QSTR_MAX_TOTAL_PEER_NUM), MP_ROM_INT(ESP_NOW_MAX_TOTAL_PEER_NUM)},
|
||||
{ MP_ROM_QSTR(MP_QSTR_MAX_ENCRYPT_PEER_NUM), MP_ROM_INT(ESP_NOW_MAX_ENCRYPT_PEER_NUM)},
|
||||
};
|
||||
STATIC MP_DEFINE_CONST_DICT(espnow_globals_dict, espnow_globals_dict_table);
|
||||
|
||||
// ### Dummy Buffer Protocol support
|
||||
// ...so asyncio can poll.ipoll() on this device
|
||||
|
||||
// Support ioctl(MP_STREAM_POLL, ) for asyncio
|
||||
STATIC mp_uint_t espnow_stream_ioctl(mp_obj_t self_in, mp_uint_t request,
|
||||
uintptr_t arg, int *errcode) {
|
||||
if (request != MP_STREAM_POLL) {
|
||||
*errcode = MP_EINVAL;
|
||||
return MP_STREAM_ERROR;
|
||||
}
|
||||
esp_espnow_obj_t *self = _get_singleton();
|
||||
return (self->recv_buffer == NULL) ? 0 : // If not initialised
|
||||
arg ^ ((ringbuf_avail(self->recv_buffer) == 0) ? MP_STREAM_POLL_RD : 0);
|
||||
}
|
||||
|
||||
STATIC const mp_stream_p_t espnow_stream_p = {
|
||||
.ioctl = espnow_stream_ioctl,
|
||||
};
|
||||
|
||||
MP_DEFINE_CONST_OBJ_TYPE(
|
||||
esp_espnow_type,
|
||||
MP_QSTR_ESPNowBase,
|
||||
MP_TYPE_FLAG_NONE,
|
||||
make_new, espnow_make_new,
|
||||
protocol, &espnow_stream_p,
|
||||
locals_dict, &esp_espnow_locals_dict
|
||||
);
|
||||
|
||||
const mp_obj_module_t mp_module_espnow = {
|
||||
.base = { &mp_type_module },
|
||||
.globals = (mp_obj_dict_t *)&espnow_globals_dict,
|
||||
};
|
||||
|
||||
MP_REGISTER_MODULE(MP_QSTR__espnow, mp_module_espnow);
|
||||
MP_REGISTER_ROOT_POINTER(void *espnow_buffer);
|
||||
#endif
|
28
ports/esp8266/modespnow.h
Normal file
28
ports/esp8266/modespnow.h
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This file is part of the MicroPython project, http://micropython.org/
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2021 Glenn Moloney @glenn20
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Called from main.c:mp_task() to reset the espnow software stack
|
||||
mp_obj_t espnow_deinit(mp_obj_t _);
|
37
ports/esp8266/modules/espnow.py
Normal file
37
ports/esp8266/modules/espnow.py
Normal file
@ -0,0 +1,37 @@
|
||||
# espnow module for MicroPython on ESP8266
|
||||
# MIT license; Copyright (c) 2022 Glenn Moloney @glenn20
|
||||
|
||||
from _espnow import *
|
||||
from uselect import poll, POLLIN
|
||||
|
||||
|
||||
class ESPNow(ESPNowBase):
|
||||
# Static buffers for alloc free receipt of messages with ESPNow.irecv().
|
||||
_data = [bytearray(ADDR_LEN), bytearray(MAX_DATA_LEN)]
|
||||
_none_tuple = (None, None)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._poll = poll() # For any() method below...
|
||||
self._poll.register(self, POLLIN)
|
||||
|
||||
def irecv(self, timeout_ms=None):
|
||||
n = self.recvinto(self._data, timeout_ms)
|
||||
return self._data if n else self._none_tuple
|
||||
|
||||
def recv(self, timeout_ms=None):
|
||||
n = self.recvinto(self._data, timeout_ms)
|
||||
return [bytes(x) for x in self._data] if n else self._none_tuple
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
return self.irecv() # Use alloc free irecv() method
|
||||
|
||||
def any(self): # For the ESP8266 which does not have ESPNow.any()
|
||||
try:
|
||||
next(self._poll.ipoll(0))
|
||||
return True
|
||||
except StopIteration:
|
||||
return False
|
57
tests/multi_espnow/10_simple_data.py
Normal file
57
tests/multi_espnow/10_simple_data.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Simple test of a ESPnow server and client transferring data.
|
||||
# This test works with ESP32 or ESP8266 as server or client.
|
||||
|
||||
try:
|
||||
import network
|
||||
import espnow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
# Set read timeout to 5 seconds
|
||||
timeout_ms = 5000
|
||||
default_pmk = b"MicroPyth0nRules"
|
||||
|
||||
|
||||
def init(sta_active=True, ap_active=False):
|
||||
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
|
||||
e = espnow.ESPNow()
|
||||
e.active(True)
|
||||
e.set_pmk(default_pmk)
|
||||
wlans[0].active(sta_active)
|
||||
wlans[1].active(ap_active)
|
||||
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
|
||||
e.set_pmk(default_pmk)
|
||||
return e
|
||||
|
||||
|
||||
# Server
|
||||
def instance0():
|
||||
e = init(True, False)
|
||||
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
|
||||
multitest.next()
|
||||
peer, msg1 = e.recv(timeout_ms)
|
||||
if msg1 is None:
|
||||
print("e.recv({timeout_ms}): Timeout waiting for message.")
|
||||
e.active(False)
|
||||
return
|
||||
print(bytes(msg1))
|
||||
msg2 = b"server to client"
|
||||
e.add_peer(peer)
|
||||
e.send(peer, msg2)
|
||||
print(bytes(msg2))
|
||||
e.active(False)
|
||||
|
||||
|
||||
# Client
|
||||
def instance1():
|
||||
e = init(True, False)
|
||||
multitest.next()
|
||||
peer = PEERS[0]
|
||||
e.add_peer(peer)
|
||||
msg1 = b"client to server"
|
||||
e.send(peer, msg1)
|
||||
print(bytes(msg1))
|
||||
peer2, msg2 = e.recv(timeout_ms)
|
||||
print(bytes(msg2))
|
||||
e.active(False)
|
6
tests/multi_espnow/10_simple_data.py.exp
Normal file
6
tests/multi_espnow/10_simple_data.py.exp
Normal file
@ -0,0 +1,6 @@
|
||||
--- instance0 ---
|
||||
b'client to server'
|
||||
b'server to client'
|
||||
--- instance1 ---
|
||||
b'client to server'
|
||||
b'server to client'
|
93
tests/multi_espnow/20_send_echo.py
Normal file
93
tests/multi_espnow/20_send_echo.py
Normal file
@ -0,0 +1,93 @@
|
||||
# Test of a ESPnow echo server and client transferring data.
|
||||
# This test works with ESP32 or ESP8266 as server or client.
|
||||
|
||||
try:
|
||||
import network
|
||||
import random
|
||||
import espnow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
# Set read timeout to 5 seconds
|
||||
timeout_ms = 5000
|
||||
default_pmk = b"MicroPyth0nRules"
|
||||
sync = True
|
||||
|
||||
|
||||
def echo_server(e):
|
||||
peers = []
|
||||
while True:
|
||||
peer, msg = e.recv(timeout_ms)
|
||||
if peer is None:
|
||||
return
|
||||
if peer not in peers:
|
||||
peers.append(peer)
|
||||
e.add_peer(peer)
|
||||
|
||||
# Echo the MAC and message back to the sender
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: send() failed to", peer)
|
||||
return
|
||||
|
||||
if msg == b"!done":
|
||||
return
|
||||
|
||||
|
||||
def echo_test(e, peer, msg, sync):
|
||||
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
|
||||
try:
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: Send failed.")
|
||||
return
|
||||
except OSError as exc:
|
||||
# Don't print exc as it is differs for esp32 and esp8266
|
||||
print("ERROR: OSError:")
|
||||
return
|
||||
|
||||
p2, msg2 = e.recv(timeout_ms)
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
|
||||
def echo_client(e, peer, msglens):
|
||||
for sync in [True, False]:
|
||||
for msglen in msglens:
|
||||
msg = bytearray(msglen)
|
||||
if msglen > 0:
|
||||
msg[0] = b"_"[0] # Random message must not start with '!'
|
||||
for i in range(1, msglen):
|
||||
msg[i] = random.getrandbits(8)
|
||||
echo_test(e, peer, msg, sync)
|
||||
|
||||
|
||||
def init(sta_active=True, ap_active=False):
|
||||
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
|
||||
e = espnow.ESPNow()
|
||||
e.active(True)
|
||||
e.set_pmk(default_pmk)
|
||||
wlans[0].active(sta_active)
|
||||
wlans[1].active(ap_active)
|
||||
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
|
||||
return e
|
||||
|
||||
|
||||
# Server
|
||||
def instance0():
|
||||
e = init(True, False)
|
||||
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
|
||||
multitest.next()
|
||||
print("Server Start")
|
||||
echo_server(e)
|
||||
print("Server Done")
|
||||
e.active(False)
|
||||
|
||||
|
||||
# Client
|
||||
def instance1():
|
||||
e = init(True, False)
|
||||
multitest.next()
|
||||
peer = PEERS[0]
|
||||
e.add_peer(peer)
|
||||
echo_client(e, peer, [1, 2, 8, 100, 249, 250, 251, 0])
|
||||
echo_test(e, peer, b"!done", True)
|
||||
e.active(False)
|
21
tests/multi_espnow/20_send_echo.py.exp
Normal file
21
tests/multi_espnow/20_send_echo.py.exp
Normal file
@ -0,0 +1,21 @@
|
||||
--- instance0 ---
|
||||
Server Start
|
||||
Server Done
|
||||
--- instance1 ---
|
||||
TEST: send/recv(msglen=1,sync=True): OK
|
||||
TEST: send/recv(msglen=2,sync=True): OK
|
||||
TEST: send/recv(msglen=8,sync=True): OK
|
||||
TEST: send/recv(msglen=100,sync=True): OK
|
||||
TEST: send/recv(msglen=249,sync=True): OK
|
||||
TEST: send/recv(msglen=250,sync=True): OK
|
||||
TEST: send/recv(msglen=251,sync=True): ERROR: OSError:
|
||||
TEST: send/recv(msglen=0,sync=True): ERROR: OSError:
|
||||
TEST: send/recv(msglen=1,sync=False): OK
|
||||
TEST: send/recv(msglen=2,sync=False): OK
|
||||
TEST: send/recv(msglen=8,sync=False): OK
|
||||
TEST: send/recv(msglen=100,sync=False): OK
|
||||
TEST: send/recv(msglen=249,sync=False): OK
|
||||
TEST: send/recv(msglen=250,sync=False): OK
|
||||
TEST: send/recv(msglen=251,sync=False): ERROR: OSError:
|
||||
TEST: send/recv(msglen=0,sync=False): ERROR: OSError:
|
||||
TEST: send/recv(msglen=5,sync=True): OK
|
130
tests/multi_espnow/30_lmk_echo.py
Normal file
130
tests/multi_espnow/30_lmk_echo.py
Normal file
@ -0,0 +1,130 @@
|
||||
# Test of a ESPnow echo server and client transferring encrypted data.
|
||||
# This test works with ESP32 or ESP8266 as server or client.
|
||||
|
||||
# First instance (echo server):
|
||||
# Set the shared PMK
|
||||
# Set the PEERS global to our mac addresses
|
||||
# Run the echo server
|
||||
# First exchange an unencrypted message from the client (so we
|
||||
# can get its MAC address) and echo the message back (unenecrypted).
|
||||
# Then set the peer LMK so all further communications are encrypted.
|
||||
|
||||
# Second instance (echo client):
|
||||
# Set the shared PMK
|
||||
# Send an unencrypted message to the server and wait for echo response.
|
||||
# Set the LMK for the peer communications so all further comms are encrypted.
|
||||
# Send random messages and compare with response from server.
|
||||
|
||||
try:
|
||||
import network
|
||||
import random
|
||||
import time
|
||||
import espnow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
|
||||
# Set read timeout to 5 seconds
|
||||
timeout_ms = 5000
|
||||
default_pmk = b"MicroPyth0nRules"
|
||||
default_lmk = b"0123456789abcdef"
|
||||
sync = True
|
||||
|
||||
|
||||
def echo_server(e):
|
||||
peers = []
|
||||
while True:
|
||||
# Wait for messages from the client
|
||||
peer, msg = e.recv(timeout_ms)
|
||||
if peer is None:
|
||||
return
|
||||
if peer not in peers:
|
||||
# If this is first message, add the peer unencrypted
|
||||
e.add_peer(peer)
|
||||
|
||||
# Echo the message back to the sender
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: send() failed to", peer)
|
||||
return
|
||||
|
||||
if peer not in peers:
|
||||
# If this is first message, add the peer encrypted
|
||||
peers.append(peer)
|
||||
e.del_peer(peer)
|
||||
e.add_peer(peer, default_lmk)
|
||||
|
||||
if msg == b"!done":
|
||||
return
|
||||
|
||||
|
||||
# Send a message from the client and compare with response from server.
|
||||
def echo_test(e, peer, msg, sync):
|
||||
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
|
||||
try:
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: Send failed.")
|
||||
return
|
||||
except OSError as exc:
|
||||
# Don't print exc as it is differs for esp32 and esp8266
|
||||
print("ERROR: OSError:")
|
||||
return
|
||||
|
||||
p2, msg2 = e.recv(timeout_ms)
|
||||
if p2 is None:
|
||||
print("ERROR: No response from server.")
|
||||
raise SystemExit
|
||||
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
|
||||
# Send some random messages to server and check the responses
|
||||
def echo_client(e, peer, msglens):
|
||||
for sync in [True, False]:
|
||||
for msglen in msglens:
|
||||
msg = bytearray(msglen)
|
||||
if msglen > 0:
|
||||
msg[0] = b"_"[0] # Random message must not start with '!'
|
||||
for i in range(1, msglen):
|
||||
msg[i] = random.getrandbits(8)
|
||||
echo_test(e, peer, msg, sync)
|
||||
|
||||
|
||||
# Initialise the wifi and espnow hardware and software
|
||||
def init(sta_active=True, ap_active=False):
|
||||
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
|
||||
e = espnow.ESPNow()
|
||||
e.active(True)
|
||||
e.set_pmk(default_pmk)
|
||||
wlans[0].active(sta_active)
|
||||
wlans[1].active(ap_active)
|
||||
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
|
||||
return e
|
||||
|
||||
|
||||
# Server
|
||||
def instance0():
|
||||
e = init(True, False)
|
||||
macs = [network.WLAN(i).config("mac") for i in (0, 1)]
|
||||
print("Server Start")
|
||||
multitest.globals(PEERS=macs)
|
||||
multitest.next()
|
||||
echo_server(e)
|
||||
print("Server Done")
|
||||
e.active(False)
|
||||
|
||||
|
||||
# Client
|
||||
def instance1():
|
||||
e = init(True, False)
|
||||
multitest.next()
|
||||
peer = PEERS[0]
|
||||
e.add_peer(peer)
|
||||
echo_test(e, peer, b"start", True)
|
||||
# Wait long enough for the server to set the lmk
|
||||
time.sleep(0.1)
|
||||
e.del_peer(peer)
|
||||
e.add_peer(peer, default_lmk)
|
||||
echo_client(e, peer, [250])
|
||||
echo_test(e, peer, b"!done", True)
|
||||
e.active(False)
|
8
tests/multi_espnow/30_lmk_echo.py.exp
Normal file
8
tests/multi_espnow/30_lmk_echo.py.exp
Normal file
@ -0,0 +1,8 @@
|
||||
--- instance0 ---
|
||||
Server Start
|
||||
Server Done
|
||||
--- instance1 ---
|
||||
TEST: send/recv(msglen=5,sync=True): OK
|
||||
TEST: send/recv(msglen=250,sync=True): OK
|
||||
TEST: send/recv(msglen=250,sync=False): OK
|
||||
TEST: send/recv(msglen=5,sync=True): OK
|
113
tests/multi_espnow/40_recv_test.py
Normal file
113
tests/multi_espnow/40_recv_test.py
Normal file
@ -0,0 +1,113 @@
|
||||
# Test of a ESPnow echo server and client transferring data.
|
||||
# This test works with ESP32 or ESP8266 as server or client.
|
||||
# Explicitly tests the irecv(), rev() and recvinto() methods.
|
||||
|
||||
try:
|
||||
import network
|
||||
import random
|
||||
import espnow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
# Set read timeout to 5 seconds
|
||||
timeout_ms = 5000
|
||||
default_pmk = b"MicroPyth0nRules"
|
||||
sync = True
|
||||
|
||||
|
||||
def echo_server(e):
|
||||
peers = []
|
||||
while True:
|
||||
peer, msg = e.irecv(timeout_ms)
|
||||
if peer is None:
|
||||
return
|
||||
if peer not in peers:
|
||||
peers.append(peer)
|
||||
e.add_peer(peer)
|
||||
|
||||
# Echo the MAC and message back to the sender
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: send() failed to", peer)
|
||||
return
|
||||
|
||||
if msg == b"!done":
|
||||
return
|
||||
|
||||
|
||||
def client_send(e, peer, msg, sync):
|
||||
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
|
||||
try:
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: Send failed.")
|
||||
return
|
||||
except OSError as exc:
|
||||
# Don't print exc as it is differs for esp32 and esp8266
|
||||
print("ERROR: OSError:")
|
||||
return
|
||||
|
||||
|
||||
def init(sta_active=True, ap_active=False):
|
||||
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
|
||||
e = espnow.ESPNow()
|
||||
e.active(True)
|
||||
e.set_pmk(default_pmk)
|
||||
wlans[0].active(sta_active)
|
||||
wlans[1].active(ap_active)
|
||||
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
|
||||
return e
|
||||
|
||||
|
||||
# Server
|
||||
def instance0():
|
||||
e = init(True, False)
|
||||
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
|
||||
multitest.next()
|
||||
print("Server Start")
|
||||
echo_server(e)
|
||||
print("Server Done")
|
||||
e.active(False)
|
||||
|
||||
|
||||
# Client
|
||||
def instance1():
|
||||
# Instance 1 (the client)
|
||||
e = init(True, False)
|
||||
e.config(timeout_ms=timeout_ms)
|
||||
multitest.next()
|
||||
peer = PEERS[0]
|
||||
e.add_peer(peer)
|
||||
|
||||
print("RECVINTO() test...")
|
||||
msg = bytes([random.getrandbits(8) for _ in range(12)])
|
||||
client_send(e, peer, msg, True)
|
||||
data = [bytearray(espnow.ADDR_LEN), bytearray(espnow.MAX_DATA_LEN)]
|
||||
n = e.recvinto(data)
|
||||
print("OK" if data[1] == msg else "ERROR: Received != Sent")
|
||||
|
||||
print("IRECV() test...")
|
||||
msg = bytes([random.getrandbits(8) for _ in range(12)])
|
||||
client_send(e, peer, msg, True)
|
||||
p2, msg2 = e.irecv()
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
print("RECV() test...")
|
||||
msg = bytes([random.getrandbits(8) for _ in range(12)])
|
||||
client_send(e, peer, msg, True)
|
||||
p2, msg2 = e.recv()
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
print("ITERATOR() test...")
|
||||
msg = bytes([random.getrandbits(8) for _ in range(12)])
|
||||
client_send(e, peer, msg, True)
|
||||
p2, msg2 = next(e)
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
# Tell the server to stop
|
||||
print("DONE")
|
||||
msg = b"!done"
|
||||
client_send(e, peer, msg, True)
|
||||
p2, msg2 = e.irecv()
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
e.active(False)
|
14
tests/multi_espnow/40_recv_test.py.exp
Normal file
14
tests/multi_espnow/40_recv_test.py.exp
Normal file
@ -0,0 +1,14 @@
|
||||
--- instance0 ---
|
||||
Server Start
|
||||
Server Done
|
||||
--- instance1 ---
|
||||
RECVINTO() test...
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
IRECV() test...
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
RECV() test...
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
ITERATOR() test...
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
DONE
|
||||
TEST: send/recv(msglen=5,sync=True): OK
|
114
tests/multi_espnow/50_esp32_rssi_test.py
Normal file
114
tests/multi_espnow/50_esp32_rssi_test.py
Normal file
@ -0,0 +1,114 @@
|
||||
# Test the ESP32 RSSI extensions on instance1.
|
||||
# Will SKIP test if instance1 is not an ESP32.
|
||||
# Instance0 may be an ESP32 or ESP8266.
|
||||
|
||||
try:
|
||||
import time
|
||||
import network
|
||||
import random
|
||||
import espnow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
# Set read timeout to 5 seconds
|
||||
timeout_ms = 5000
|
||||
default_pmk = b"MicroPyth0nRules"
|
||||
sync = True
|
||||
|
||||
|
||||
def echo_server(e):
|
||||
peers = []
|
||||
while True:
|
||||
peer, msg = e.irecv(timeout_ms)
|
||||
if peer is None:
|
||||
return
|
||||
if peer not in peers:
|
||||
peers.append(peer)
|
||||
e.add_peer(peer)
|
||||
|
||||
# Echo the MAC and message back to the sender
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: send() failed to", peer)
|
||||
return
|
||||
|
||||
if msg == b"!done":
|
||||
return
|
||||
|
||||
|
||||
def client_send(e, peer, msg, sync):
|
||||
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
|
||||
try:
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: Send failed.")
|
||||
return
|
||||
except OSError as exc:
|
||||
# Don't print exc as it is differs for esp32 and esp8266
|
||||
print("ERROR: OSError:")
|
||||
return
|
||||
|
||||
|
||||
def init(sta_active=True, ap_active=False):
|
||||
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
|
||||
e = espnow.ESPNow()
|
||||
e.active(True)
|
||||
e.set_pmk(default_pmk)
|
||||
wlans[0].active(sta_active)
|
||||
wlans[1].active(ap_active)
|
||||
return e
|
||||
|
||||
|
||||
# Server
|
||||
def instance0():
|
||||
e = init(True, False)
|
||||
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
|
||||
multitest.next()
|
||||
print("Server Start")
|
||||
echo_server(e)
|
||||
print("Server Done")
|
||||
e.active(False)
|
||||
|
||||
|
||||
# Client
|
||||
def instance1():
|
||||
# Instance 1 (the client)
|
||||
e = init(True, False)
|
||||
if not hasattr(e, "peers_table"):
|
||||
e.active(False)
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
e.config(timeout_ms=timeout_ms)
|
||||
multitest.next()
|
||||
peer = PEERS[0]
|
||||
e.add_peer(peer)
|
||||
|
||||
# assert len(e.peers) == 1
|
||||
print("IRECV() test...")
|
||||
msg = bytes([random.getrandbits(8) for _ in range(12)])
|
||||
client_send(e, peer, msg, True)
|
||||
p2, msg2 = e.irecv()
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
print("RSSI test...")
|
||||
if len(e.peers_table) != 1:
|
||||
print("ERROR: len(ESPNow.peers_table()) != 1. ESPNow.peers_table()=", peers)
|
||||
elif list(e.peers_table.keys())[0] != peer:
|
||||
print("ERROR: ESPNow.peers_table().keys[0] != peer. ESPNow.peers_table()=", peers)
|
||||
else:
|
||||
rssi, time_ms = e.peers_table[peer]
|
||||
if not -127 < rssi < 0:
|
||||
print("ERROR: Invalid rssi value:", rssi)
|
||||
elif time.ticks_diff(time.ticks_ms(), time_ms) > 5000:
|
||||
print("ERROR: Unexpected time_ms value:", time_ms)
|
||||
else:
|
||||
print("OK")
|
||||
|
||||
# Tell the server to stop
|
||||
print("DONE")
|
||||
msg = b"!done"
|
||||
client_send(e, peer, msg, True)
|
||||
p2, msg2 = e.irecv()
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
e.active(False)
|
10
tests/multi_espnow/50_esp32_rssi_test.py.exp
Normal file
10
tests/multi_espnow/50_esp32_rssi_test.py.exp
Normal file
@ -0,0 +1,10 @@
|
||||
--- instance0 ---
|
||||
Server Start
|
||||
Server Done
|
||||
--- instance1 ---
|
||||
IRECV() test...
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
RSSI test...
|
||||
OK
|
||||
DONE
|
||||
TEST: send/recv(msglen=5,sync=True): OK
|
117
tests/multi_espnow/60_irq_test.py
Normal file
117
tests/multi_espnow/60_irq_test.py
Normal file
@ -0,0 +1,117 @@
|
||||
# Test of a ESPnow echo server and client transferring data.
|
||||
# Test the ESP32 extemnsions. Assumes instance1 is an ESP32.
|
||||
# Instance0 may be an ESP32 or ESP8266
|
||||
|
||||
try:
|
||||
import network
|
||||
import random
|
||||
import time
|
||||
import espnow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
# Set read timeout to 5 seconds
|
||||
timeout_ms = 5000
|
||||
default_pmk = b"MicroPyth0nRules"
|
||||
sync = True
|
||||
|
||||
|
||||
def echo_server(e):
|
||||
peers = []
|
||||
while True:
|
||||
peer, msg = e.irecv(timeout_ms)
|
||||
if peer is None:
|
||||
return
|
||||
if peer not in peers:
|
||||
peers.append(peer)
|
||||
e.add_peer(peer)
|
||||
|
||||
# Echo the MAC and message back to the sender
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: send() failed to", peer)
|
||||
return
|
||||
|
||||
if msg == b"!done":
|
||||
return
|
||||
|
||||
|
||||
def client_send(e, peer, msg, sync):
|
||||
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
|
||||
try:
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: Send failed.")
|
||||
return
|
||||
except OSError as exc:
|
||||
# Don't print exc as it is differs for esp32 and esp8266
|
||||
print("ERROR: OSError:")
|
||||
return
|
||||
|
||||
|
||||
def init(sta_active=True, ap_active=False):
|
||||
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
|
||||
e = espnow.ESPNow()
|
||||
e.active(True)
|
||||
e.set_pmk(default_pmk)
|
||||
wlans[0].active(sta_active)
|
||||
wlans[1].active(ap_active)
|
||||
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
|
||||
return e
|
||||
|
||||
|
||||
# Server
|
||||
def instance0():
|
||||
e = init(True, False)
|
||||
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
|
||||
multitest.next()
|
||||
print("Server Start")
|
||||
echo_server(e)
|
||||
print("Server Done")
|
||||
e.active(False)
|
||||
|
||||
|
||||
done = False
|
||||
|
||||
|
||||
# Client
|
||||
def instance1():
|
||||
# Instance 1 (the client)
|
||||
e = init(True, False)
|
||||
try:
|
||||
e.irq(None)
|
||||
except AttributeError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
e.config(timeout_ms=timeout_ms)
|
||||
multitest.next()
|
||||
peer = PEERS[0]
|
||||
e.add_peer(peer)
|
||||
|
||||
def on_recv_cb(e):
|
||||
global done
|
||||
p2, msg2 = e.irecv(0)
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
done = True
|
||||
|
||||
global done
|
||||
print("IRQ() test...")
|
||||
e.irq(on_recv_cb)
|
||||
done = False
|
||||
msg = bytes([random.getrandbits(8) for _ in range(12)])
|
||||
client_send(e, peer, msg, True)
|
||||
start = time.ticks_ms()
|
||||
while not done:
|
||||
if time.ticks_diff(time.ticks_ms(), start) > timeout_ms:
|
||||
print("Timeout waiting for response.")
|
||||
raise SystemExit
|
||||
e.irq(None)
|
||||
|
||||
# Tell the server to stop
|
||||
print("DONE")
|
||||
msg = b"!done"
|
||||
client_send(e, peer, msg, True)
|
||||
p2, msg2 = e.irecv()
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
e.active(False)
|
8
tests/multi_espnow/60_irq_test.py.exp
Normal file
8
tests/multi_espnow/60_irq_test.py.exp
Normal file
@ -0,0 +1,8 @@
|
||||
--- instance0 ---
|
||||
Server Start
|
||||
Server Done
|
||||
--- instance1 ---
|
||||
IRQ() test...
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
DONE
|
||||
TEST: send/recv(msglen=5,sync=True): OK
|
110
tests/multi_espnow/80_uasyncio_client.py
Normal file
110
tests/multi_espnow/80_uasyncio_client.py
Normal file
@ -0,0 +1,110 @@
|
||||
# Test of a ESPnow echo server and asyncio client transferring data.
|
||||
# Test will SKIP if instance1 (asyncio client) does not support asyncio.
|
||||
# - eg. ESP8266 with 1MB flash.
|
||||
# Instance0 is not required to support asyncio.
|
||||
|
||||
try:
|
||||
import network
|
||||
import random
|
||||
import espnow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
# Set read timeout to 5 seconds
|
||||
timeout_ms = 5000
|
||||
default_pmk = b"MicroPyth0nRules"
|
||||
sync = True
|
||||
|
||||
|
||||
def echo_server(e):
|
||||
peers = []
|
||||
while True:
|
||||
peer, msg = e.irecv(timeout_ms)
|
||||
if peer is None:
|
||||
return
|
||||
if peer not in peers:
|
||||
peers.append(peer)
|
||||
e.add_peer(peer)
|
||||
|
||||
# Echo the MAC and message back to the sender
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: send() failed to", peer)
|
||||
return
|
||||
|
||||
if msg == b"!done":
|
||||
return
|
||||
|
||||
|
||||
def client_send(e, peer, msg, sync):
|
||||
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
|
||||
try:
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: Send failed.")
|
||||
return
|
||||
except OSError as exc:
|
||||
# Don't print exc as it is differs for esp32 and esp8266
|
||||
print("ERROR: OSError:")
|
||||
return
|
||||
print("OK")
|
||||
|
||||
|
||||
def init(e, sta_active=True, ap_active=False):
|
||||
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
|
||||
e.active(True)
|
||||
e.set_pmk(default_pmk)
|
||||
wlans[0].active(sta_active)
|
||||
wlans[1].active(ap_active)
|
||||
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
|
||||
return e
|
||||
|
||||
|
||||
async def client(e):
|
||||
init(e, True, False)
|
||||
e.config(timeout_ms=timeout_ms)
|
||||
peer = PEERS[0]
|
||||
e.add_peer(peer)
|
||||
multitest.next()
|
||||
|
||||
print("airecv() test...")
|
||||
msgs = []
|
||||
for i in range(5):
|
||||
# Send messages to the peer who will echo it back
|
||||
msgs.append(bytes([random.getrandbits(8) for _ in range(12)]))
|
||||
client_send(e, peer, msgs[i], True)
|
||||
|
||||
for i in range(5):
|
||||
mac, reply = await e.airecv()
|
||||
print("OK" if reply == msgs[i] else "ERROR: Received != Sent")
|
||||
|
||||
# Tell the server to stop
|
||||
print("DONE")
|
||||
msg = b"!done"
|
||||
client_send(e, peer, msg, True)
|
||||
mac, reply = await e.airecv()
|
||||
print("OK" if reply == msg else "ERROR: Received != Sent")
|
||||
|
||||
e.active(False)
|
||||
|
||||
|
||||
# Server
|
||||
def instance0():
|
||||
e = espnow.ESPNow()
|
||||
init(e, True, False)
|
||||
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
|
||||
multitest.next()
|
||||
print("Server Start")
|
||||
echo_server(e)
|
||||
print("Server Done")
|
||||
e.active(False)
|
||||
|
||||
|
||||
# Client
|
||||
def instance1():
|
||||
try:
|
||||
import uasyncio as asyncio
|
||||
from aioespnow import AIOESPNow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
asyncio.run(client(AIOESPNow()))
|
18
tests/multi_espnow/80_uasyncio_client.py.exp
Normal file
18
tests/multi_espnow/80_uasyncio_client.py.exp
Normal file
@ -0,0 +1,18 @@
|
||||
--- instance0 ---
|
||||
Server Start
|
||||
Server Done
|
||||
--- instance1 ---
|
||||
airecv() test...
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
OK
|
||||
OK
|
||||
OK
|
||||
OK
|
||||
OK
|
||||
DONE
|
||||
TEST: send/recv(msglen=5,sync=True): OK
|
||||
OK
|
96
tests/multi_espnow/81_uasyncio_server.py
Normal file
96
tests/multi_espnow/81_uasyncio_server.py
Normal file
@ -0,0 +1,96 @@
|
||||
# Test of a ESPnow asyncio echo server and client transferring data.
|
||||
# Test will SKIP if instance0 (asyncio echo server) does not support asyncio.
|
||||
# - eg. ESP8266 with 1MB flash.
|
||||
# Instance1 is not required to support asyncio.
|
||||
|
||||
try:
|
||||
import network
|
||||
import random
|
||||
import espnow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
# Set read timeout to 5 seconds
|
||||
timeout_ms = 5000
|
||||
default_pmk = b"MicroPyth0nRules"
|
||||
sync = True
|
||||
|
||||
|
||||
def client_send(e, peer, msg, sync):
|
||||
print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
|
||||
try:
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: Send failed.")
|
||||
return
|
||||
except OSError as exc:
|
||||
# Don't print exc as it is differs for esp32 and esp8266
|
||||
print("ERROR: OSError:")
|
||||
return
|
||||
|
||||
|
||||
def init(e, sta_active=True, ap_active=False):
|
||||
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
|
||||
e.active(True)
|
||||
e.set_pmk(default_pmk)
|
||||
wlans[0].active(sta_active)
|
||||
wlans[1].active(ap_active)
|
||||
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
|
||||
return e
|
||||
|
||||
|
||||
async def echo_server(e):
|
||||
peers = []
|
||||
async for peer, msg in e:
|
||||
if peer not in peers:
|
||||
peers.append(peer)
|
||||
e.add_peer(peer)
|
||||
|
||||
# Echo the message back to the sender
|
||||
if not await e.asend(peer, msg, sync):
|
||||
print("ERROR: asend() failed to", peer)
|
||||
return
|
||||
|
||||
if msg == b"!done":
|
||||
return
|
||||
|
||||
|
||||
# Server
|
||||
def instance0():
|
||||
try:
|
||||
import uasyncio as asyncio
|
||||
from aioespnow import AIOESPNow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
e = AIOESPNow()
|
||||
init(e, True, False)
|
||||
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
|
||||
multitest.next()
|
||||
print("Server Start")
|
||||
asyncio.run(echo_server(e))
|
||||
print("Server Done")
|
||||
e.active(False)
|
||||
|
||||
|
||||
def instance1():
|
||||
e = espnow.ESPNow()
|
||||
init(e, True, False)
|
||||
peer = PEERS[0]
|
||||
e.add_peer(peer)
|
||||
multitest.next()
|
||||
|
||||
for i in range(5):
|
||||
msg = bytes([random.getrandbits(8) for _ in range(12)])
|
||||
client_send(e, peer, msg, True)
|
||||
p2, msg2 = e.irecv(timeout_ms)
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
# Tell the server to stop
|
||||
print("DONE")
|
||||
msg = b"!done"
|
||||
client_send(e, peer, msg, True)
|
||||
p2, msg2 = e.irecv(timeout_ms)
|
||||
print("OK" if msg2 == msg else "ERROR: Received != Sent")
|
||||
|
||||
e.active(False)
|
11
tests/multi_espnow/81_uasyncio_server.py.exp
Normal file
11
tests/multi_espnow/81_uasyncio_server.py.exp
Normal file
@ -0,0 +1,11 @@
|
||||
--- instance0 ---
|
||||
Server Start
|
||||
Server Done
|
||||
--- instance1 ---
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
TEST: send/recv(msglen=12,sync=True): OK
|
||||
DONE
|
||||
TEST: send/recv(msglen=5,sync=True): OK
|
108
tests/multi_espnow/90_memory_test.py
Normal file
108
tests/multi_espnow/90_memory_test.py
Normal file
@ -0,0 +1,108 @@
|
||||
# Test of a ESPnow echo server and client transferring data.
|
||||
# This test works with ESP32 or ESP8266 as server or client.
|
||||
|
||||
try:
|
||||
import network
|
||||
import random
|
||||
import espnow
|
||||
except ImportError:
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
# Set read timeout to 5 seconds
|
||||
timeout_ms = 5000
|
||||
default_pmk = b"MicroPyth0nRules"
|
||||
sync = True
|
||||
|
||||
|
||||
def echo_server(e):
|
||||
peers = []
|
||||
i = 0
|
||||
while True:
|
||||
peer, msg = e.irecv(timeout_ms)
|
||||
i += 1
|
||||
if i % 10 == 0:
|
||||
print("OK:", i)
|
||||
if peer is None:
|
||||
return
|
||||
if peer not in peers:
|
||||
peers.append(peer)
|
||||
e.add_peer(peer)
|
||||
|
||||
# Echo the MAC and message back to the sender
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: send() failed to", peer)
|
||||
return
|
||||
|
||||
if msg == b"!done":
|
||||
return
|
||||
|
||||
|
||||
def echo_test(e, peer, msg, sync):
|
||||
try:
|
||||
if not e.send(peer, msg, sync):
|
||||
print("ERROR: Send failed.")
|
||||
return
|
||||
except OSError as exc:
|
||||
# Don't print exc as it is differs for esp32 and esp8266
|
||||
print("ERROR: OSError:")
|
||||
return
|
||||
|
||||
p2, msg2 = e.irecv(timeout_ms)
|
||||
if msg2 != msg:
|
||||
print("ERROR: Received != Sent")
|
||||
|
||||
|
||||
def echo_client(e, peer, msglens):
|
||||
for sync in [True]:
|
||||
for msglen in msglens:
|
||||
msg = bytearray(msglen)
|
||||
if msglen > 0:
|
||||
msg[0] = b"_"[0] # Random message must not start with '!'
|
||||
for i in range(1, msglen):
|
||||
msg[i] = random.getrandbits(8)
|
||||
echo_test(e, peer, msg, sync)
|
||||
|
||||
|
||||
def init(sta_active=True, ap_active=False):
|
||||
wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
|
||||
e = espnow.ESPNow()
|
||||
e.active(True)
|
||||
e.set_pmk(default_pmk)
|
||||
wlans[0].active(sta_active)
|
||||
wlans[1].active(ap_active)
|
||||
wlans[0].disconnect() # Force esp8266 STA interface to disconnect from AP
|
||||
return e
|
||||
|
||||
|
||||
# Server
|
||||
def instance0():
|
||||
e = init(True, False)
|
||||
multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
|
||||
multitest.next()
|
||||
print("Server Start")
|
||||
echo_server(e)
|
||||
print("Server Done")
|
||||
e.active(False)
|
||||
|
||||
|
||||
# Client
|
||||
def instance1():
|
||||
e = init(True, False)
|
||||
multitest.next()
|
||||
peer = PEERS[0]
|
||||
e.add_peer(peer)
|
||||
echo_test(e, peer, b"ping", True)
|
||||
gc.collect()
|
||||
mem_start = gc.mem_alloc()
|
||||
for i in range(10):
|
||||
echo_client(e, peer, [250] * 10)
|
||||
print("OK:", (i + 1) * 10)
|
||||
echo_test(e, peer, b"!done", True)
|
||||
gc.collect()
|
||||
mem_end = gc.mem_alloc()
|
||||
if mem_end - mem_start < 1024:
|
||||
print("OK: Less than 1024 bytes consumed")
|
||||
else:
|
||||
print("Error: Memory consumed is", mem_end - mem_start)
|
||||
e.active(False)
|
25
tests/multi_espnow/90_memory_test.py.exp
Normal file
25
tests/multi_espnow/90_memory_test.py.exp
Normal file
@ -0,0 +1,25 @@
|
||||
--- instance0 ---
|
||||
Server Start
|
||||
OK: 10
|
||||
OK: 20
|
||||
OK: 30
|
||||
OK: 40
|
||||
OK: 50
|
||||
OK: 60
|
||||
OK: 70
|
||||
OK: 80
|
||||
OK: 90
|
||||
OK: 100
|
||||
Server Done
|
||||
--- instance1 ---
|
||||
OK: 10
|
||||
OK: 20
|
||||
OK: 30
|
||||
OK: 40
|
||||
OK: 50
|
||||
OK: 60
|
||||
OK: 70
|
||||
OK: 80
|
||||
OK: 90
|
||||
OK: 100
|
||||
OK: Less than 1024 bytes consumed
|
Loading…
Reference in New Issue
Block a user