Browse Source
ESP-NOW is a proprietary wireless communication protocol which supports connectionless communication between ESP32 and ESP8266 devices, using vendor specific WiFi frames. This commit adds support for this protocol through a new `espnow` module. This commit builds on original work done by @nickzoic, @shawwwn and with contributions from @zoland. Features include: - Use of (extended) ring buffers in py/ringbuf.[ch] for robust IO. - Signal strength (RSSI) monitoring. - Core support in `_espnow` C module, extended by `espnow.py` module. - Asyncio support via `aioespnow.py` module (separate to this commit). - Docs provided at `docs/library/espnow.rst`. Methods available in espnow.ESPNow class are: - active(True/False) - config(): set rx buffer size, read timeout and tx rate - recv()/irecv()/recvinto() to read incoming messages from peers - send() to send messages to peer devices - any() to test if a message is ready to read - irq() to set callback for received messages - stats() returns transfer stats: (tx_pkts, tx_pkt_responses, tx_failures, rx_pkts, lost_rx_pkts) - add_peer(mac, ...) registers a peer before sending messages - get_peer(mac) returns peer info: (mac, lmk, channel, ifidx, encrypt) - mod_peer(mac, ...) changes peer info parameters - get_peers() returns all peer info tuples - peers_table supports RSSI signal monitoring for received messages: {peer1: [rssi, time_ms], peer2: [rssi, time_ms], ...} ESP8266 is a pared down version of the ESP32 ESPNow support due to code size restrictions and differences in the low-level API. See docs for details. Also included is a test suite in tests/multi_espnow. This tests basic espnow data transfer, multiple transfers, various message sizes, encrypted messages (pmk and lmk), and asyncio support. Initial work is from https://github.com/micropython/micropython/pull/4115. Initial import of code is from: https://github.com/nickzoic/micropython/tree/espnow-4115.pull/6515/head
Glenn Moloney
4 years ago
committed by
Damien George
38 changed files with 3542 additions and 4 deletions
@ -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() |
|||
|
@ -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); |
@ -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 _); |
@ -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 |
@ -1,4 +1,5 @@ |
|||
LD_FILES = boards/esp8266_1m.ld |
|||
|
|||
MICROPY_ESPNOW ?= 1 |
|||
MICROPY_PY_BTREE ?= 1 |
|||
MICROPY_VFS_LFS2 ?= 1 |
|||
|
@ -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 |
@ -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 _); |
@ -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 |
@ -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) |
@ -0,0 +1,6 @@ |
|||
--- instance0 --- |
|||
b'client to server' |
|||
b'server to client' |
|||
--- instance1 --- |
|||
b'client to server' |
|||
b'server to client' |
@ -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) |
@ -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 |
@ -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) |
@ -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 |
@ -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) |
@ -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 |
@ -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) |
@ -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 |
@ -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) |
@ -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 |
@ -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())) |
@ -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 |
@ -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) |
@ -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 |
@ -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) |
@ -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