summaryrefslogtreecommitdiff
path: root/docs/library/espnow.rst
blob: 84e9e9465de965d369a5cf06b0d7b9f0fa2abf79 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
: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.WLAN.IF_STA)  # Or network.WLAN.IF_AP
    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.WLAN.IF_STA)
    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 ESP-NOW 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 ESP-NOW
          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 ESP-NOW 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) 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/v5.2.3/esp32/
        api-reference/network/esp_wifi.html#_CPPv415wifi_phy_rate_t>`_. This
        parameter is actually *write-only* due to ESP-IDF not providing any
        means for querying the radio interface's rate parameter.

    .. 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.WLAN.IF_STA`` or ``network.WLAN.IF_AP``) 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.WLAN.IF_STA)
    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 reuse 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
---------------

On ESP32 devices, the Espressif ESP-NOW software requires that other devices
(peers) must be *registered* using `add_peer()` before we can
`send()<ESPNow.send()>` them messages (this is *not* enforced on ESP8266
devices). It is **not** necessary to register a peer to receive an
un-encrypted message from that peer.

**Encrypted messages**: To receive an *encrypted* message, the receiving device
must first register the sender and use the same encryption keys as the sender
(PMK and LMK) (see `set_pmk()` and `add_peer()`.

.. method:: ESPNow.set_pmk(pmk)

    Set the Primary Master Key (PMK) which is used to encrypt the Local Master
    Keys (LMK) for encrypting messages. 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 (any parameter set to
    ``None`` will be set to it's default value):

    .. 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, if channel is set to another
          value then this must match the channel currently configured on the
          interface (see :func:`WLAN.config`). (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.WLAN.IF_STA`` (=0) or ``network.WLAN.IF_AP`` (=1).
          (default=0/``network.WLAN.IF_STA``). 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 =
          ``True`` if *lmk* is set to a valid key, else ``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.
        - ``OSError(num, "ESP_ERR_ESPNOW_CHAN")`` if a channel value was
          set that doesn't match the channel currently configured for this
          interface.
        - ``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()`). Any parameter that is not set (or set to
    ``None``) will retain the existing value for that parameter.

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. For more reliable operation,
  it is recommended to read out as many messages as are available when the
  callback is invoked and to set the read timeout to zero, eg: ::

        def recv_cb(e):
            while True:  # Read out all messages waiting in the buffer
                mac, msg = e.irecv(0)  # Don't wait if no messages left
                if mac is None:
                    return
                print(mac, msg)
        e.irq(recv_cb)

  The `irq()<ESPNow.irq()>` callback method is an alternative method for
  processing incoming 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 ESP-NOW 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.WLAN.IF_STA).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<asyncio>` support.

**Note:** Asyncio support is available on all ESP32 targets as well as those
ESP8266 boards which include the asyncio module (ie. ESP8266 devices with at
least 2MB flash memory).

A small async server example::

    import network
    import aioespnow
    import asyncio

    # A WLAN interface must be active to send()/recv()
    network.WLAN(network.WLAN.IF_STA).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:`asyncio` 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 ESPNow 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 ESPNow 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.WLAN.IF_STA`` or ``network.WLAN.IF_AP``),
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 (`network.WLAN.PM_PERFORMANCE`)
   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. Disabling the power-saving mode on the STA_IF interface;

   - Use ``sta.config(pm=sta.PM_NONE)``

2. 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).

3. 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 ESPNow
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)
  sta.config(pm=sta.PM_NONE)  # ..then disable power saving

  # 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.WLAN.IF_STA); sta.active(False)
      ap = network.WLAN(network.WLAN.IF_AP); 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()