The Wayback Machine - https://web.archive.org/web/20210124045011/https://github.com/micropython/micropython/pull/6515
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

esp32: ESP-NOW support for ESP32 and ESP8266 #6515

Open
wants to merge 4 commits into
base: master
from

Conversation

@glenn20
Copy link
Contributor

@glenn20 glenn20 commented Oct 4, 2020

ESP-NOW is a proprietary wireless communication protocol which supports communication between ESP32 and ESP8266 devices. See: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_now.html

ESP-NOW would be of particular interest for low-power wireless communication requirements, such as battery-powered operations.

This PR is based on the previous work by @nickzoic, @shawwwn and contributions from @zoland, including:

This PR builds on the espnow-4115 branch by adding:

  • Fixups to rebase against the main branch (also patches cleanly against v1.13).
  • Add send and recv ring buffers to prevent buffer overwrites and minimise packet loss 1b372b2
  • Dynamic allocation of ESPNow singleton so that gc will not reclaim buffer memory. adbdc32
  • Eliminate heap allocation on send and recv callbacks (re-use static buffers) adbdc32
  • Extend API to provide full control over peer_info parameters in 58bd9de
    • Including some proposed API breaking changes!!

Draft API docs are provided in 3d4058b. See docs/library/espnow.rst.

This is working reliably for my purposes as a low-level ESPNow interface. Testers and proposed improvements are welcome.

Caveat
I have only implemented this for the esp32. I don't have any ESP8266 devices.

Further development
It would be useful to extend this interface with a more robust I/O interface (rather than relying on the send and recv callbacks which run in the sched context and easily overflow the sched stack). Some form of uasyncio would be interesting.

Comments, suggestion are welcome.

@nickzoic
Copy link
Contributor

@nickzoic nickzoic commented Oct 5, 2020

Thanks Glenn! I'll do some testing when I get a chance!

@tve
Copy link
Contributor

@tve tve commented Oct 5, 2020

Has this been addressed?

I see a threading issue with the callbacks, from the esp32 docs:

The sending callback function runs from a high-priority WiFi task. So, do not do lengthy operations in the callback function. Instead, post necessary data to a queue and handle it from a lower priority task.
The receiving callback function also runs from WiFi task.

To me "high priority Wifi task" has the smell of Pro CPU and mp_sched_schedule is not protected against that. Of course it would be nice if the Espressif docs were a bit more explicit, perhaps the esp-idf source sheds some light...

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 5, 2020

Has this been addressed?

Thanks for bringing this up again - it fell off my radar.

I see a threading issue with the callbacks, from the esp32 docs:

The sending callback function runs from a high-priority WiFi task. So, do not do lengthy operations in the callback function. Instead, post necessary data to a queue and handle it from a lower priority task.
The receiving callback function also runs from WiFi task.

To me "high priority Wifi task" has the smell of Pro CPU and mp_sched_schedule is not protected against that. Of course it would be nice if the Espressif docs were a bit more explicit, perhaps the esp-idf source sheds some light...

I confess I've had no more success than any others in divining the mysteries behind the espressif docs. I think I am doing just what the docs recommend - handing data off to a queue (ring buffer) from the callback functions (I think of them as ISRs).

The ring buffer is thread safe so long as only one producer is pushing new data onto the head of the buffer (recv_cb() and send_cb() - which execute in the "high-priority Wifi Task") and only one consumer (callback_wrapper()) is pulling data off the tail of the ring buffer.

So, I don't believe it matters which CPU they are being executed on - so long as recv_cb() is never pre-empted by another recv_cb() (on the same or another CPU) and callback_wrapper() is never pre-empted by another callback_wrapper().

I am interested to understand better the concerns around the mp_sched_schedule vulnerability to Pro CPU invocations. If there is some further protection required I'd welcome any guidance. I'm keen to make this as robust as possible.

EDIT: I've done some small sustained transfer tests and haven't seen any buffer corruption yet - but that's no guarantee.

@Feiko
Copy link

@Feiko Feiko commented Oct 5, 2020

Tried to use espnow, using the documentation docs/library/espnow.rst. I can't init espnow, what argument is it expecting? see code below.

from esp import espnow
espnow.ESPNow.init()

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: function takes 1 positional arguments but 0 were given
@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 5, 2020

Hi Feiko,

Thanks for testing. ESPNow is a Class, you need to invoke a class instance first before calling the init() method. The errors is less than useful - I admit.

Typical usage (I'll put a short code snippet at the start of the docs in the next commit).

from esp import ESPNow
e = espnow.ESPNow()
e.init()
....

Furthermore, you need to initialise a WLAN interface before calling init() or your ESP32 may reset (on the todo list): eg.

import network
w0=network.WLAN(network.STA_IF)   # Can be STA_IF or AP_IF

Additionally, you need to invoke w0.active(True) before you can send or receive any data.

@Feiko
Copy link

@Feiko Feiko commented Oct 5, 2020

Thanks Glenn20,

Of course! That did the trick. My Python is a bit rusty. A short code snippet would be very welcome.

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 7, 2020

@tve: I've done a little empirical testing, but I'm not sure if they are valid in micropython.

Using xPortGetCoreID() I tested the coreid for

  • the espnow callbacks (recv_cb() and send_cb()),
  • the callback_wrapper() scheduled by mp_sched_schedule() and
  • the main thread running espnow_send().
    In all cases the code reported back coreid=0. I ran the tests for about two hours.

I was a little surprised because I thought the micropython thread ran on coreid=1 (but I'm not sure why I had acquired that assumption). Is it possible that xPortGetCoreID() does not report correctly (eg. not initialised under micropython)?

Let me know if there are any specific effects I should be protecting against.

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 7, 2020

Ok - I've just checked and discovered that arising from #5489 micropython recently switched to pin everything to core 0, so the results of my test were not as unexpected as I thought.

@tve
Copy link
Contributor

@tve tve commented Oct 11, 2020

Let me know if there are any specific effects I should be protecting against.

Code looks good to me!

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 11, 2020

@beyonlo
Copy link

@beyonlo beyonlo commented Oct 15, 2020

@glenn20

Is possible to do a like wlan.scan() to get all APs MAC and their signal?
Actually, I'm working (ESP32) with wlan.scan() to get location-based on fixed APs. How is possible to do that with ESPNOW?

Thank you.

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 15, 2020

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 15, 2020

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 18, 2020

As flagged, a substantial rewrite with breaking changes. Apologies for the delay - shoulder injury slowed me down for a while and I spent some time faffing about with various approaches to supporting more reliable IO, allocation-free reads and asyncio support. Learnt a lot more about micropython internals.

I know that a substantial change of direction in a pull request is less than ideal, but I do believe the new approach warrants the change. I'm happy to take on board feedback. I will say that I get more reliable and faster data transfers with the new approach.

Breaking Changes

  • Remove on_send() and on_recv(): The mp_sched_schedule() stack is easily overflow-ed and can cause unfortunate reliability interactions when used along with other devices, such as bluetooth or other machine IO.
  • Add recv([timeout_ms]): Wait for and return a tuple: (peer_mac, message). Default timeout can be set with config(timeout=ms). New tuple and bytestring storage is alloced on each call.
  • Add irecv([timeout]): As for recv() but supports alloc-free reads. Returns a callee-owned tuple. ie. the tuple and bytestring storage is alloc-ed once on first call and re-used across subsequent calls.
  • Change send(peer_mac, message[, sync=True]): Add sync flag (default=True) to support syncronised writes to the peer. If sync=True: send() the message and wait for response (or not) of peer(s). If sync=False: send message to peer and return immediately. Responses from peers will be discarded. If sync=True: return False if any peer fails to respond. Note that a 'broadcast' will send to all registered peers. Synchronised mode is much more reliable, while sync=False is much faster.
  • Add clear(True): Clear any pending data in the recv buffer. Use this in case you get a "Buffer error" (should not happen). Will discard all data in the buffer.
  • Add support for stream IO interface: read(size), write(), readinto(), readinto1(), read1(). I'll provide further docs and python code support for the data format used by these functions. Also supports poll.poll() which enables support for asyncio through the StreamReader() class.
  • Add support for iterating over the ESPNow() interface (via irecv()), eg: for i in espnow.ESPNow(): print i

Also a substantial internal rewrite to better consolidate the buffer and data packet logic.

Update for:
 - v1.13+ compatible
 - Ring buffers in esp32/ringbuffer.[ch] for robust IO.
 - Allocate the ESPNow singleton dynamically (protect bufs from gc).

Remove:
 - on_recv() and on_send(): use of "sched" callbacks is unnecessary
   in this use case and just adds fragility.

New methods:
 - recv() to read incoming messages (instead of callbacks).
 - irecv() for allocation-free read of incoming messages.
 - Stream IO support: read(), write() and poll through ioctl(). This
   can be used to support asyncio use of espnow.
 - Also added read1() and readinto1().
 - Add support for iteration on ESPNow object for alloc-free read.
 - config(): set tx and rx buf sizes and read timeout
 - stats(): Returns transfer stats:
     (tx_packets, tx_packet_responses, rx_packets, lost_rx_packets).
 - get_peer(mac): Return peer info: (mac,lmk,channel,ifidx,encrypt)
 - mod_peer(mac, ...) to change peer info parameters.
 - get_peers(): to return all peer info tuples.
Deleted:
 - espnow_lmk() which is subsumed by mod_peer().

Implementation changes:
 - send(): Remove automatic changes to ifidx parameter for peers. This
   is better managed in the app layer since we now provide access to
   the peer parameters.
 - Use the new consolidated check_esp_err(0 for exception

Add new constants as attributes of the ESPNow object:
 - MAX_DATA_LEN         (=ESP_NOW_MAX_DATA_LEN) (250)
 - KEY_LEN              (=ESP_NOW_KEY_LEN) (16)
 - MAX_TOTAL_PEER_NUM   (=ESP_NOW_MAX_TOTAL_PEER_NUM) (20)
 - MAX_ENCRYPT_PEER_NUM (=ESP_NOW_MAX_ENCRYPT_PEER_NUM) (6)

docs/library/espnow.rst: Add docs for ESPNow module.
esp8266/espnow: Revert broken espnow changes from 4115 to esp8266 port
@glenn20 glenn20 force-pushed the glenn20:espnow-g20 branch from 23fda14 to 40d183c Oct 19, 2020
@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 19, 2020

Ok - I've squashed the commits to just two: original import from PR#4115 and my current HEAD.

I believe this is ready for review. It is working well for me but happy to hear from others.

@peterhinch
Copy link
Contributor

@peterhinch peterhinch commented Oct 19, 2020

Kudos for the uasyncio support 👍

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 19, 2020

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Oct 21, 2020

For those interested, I now have a heavily pared down version of the esp32 espnow support working on esp8266 available at: https://github.com/glenn20/micropython/tree/espnow-g20-8266. I am planning to make a separate PR for the esp8266 support after a little more testing, but I welcome any testers or users in the meantime.

Does not support:

  • stream IO and asyncio
  • get_peer(), mod_peer() and get_peers()
  • config()
  • ESPNow broadcast in send()

However, it does support:

  • init([recv_bufsize]): Override the default recv_buffer size in init() (no config() method)
  • deinit()
  • irecv([timeout]): No recv() method.
  • send(peer, message, [sync]): Sync, and non-sync send()s are supported.
  • add_peer(peer[, lmk[, channel]])
  • del_peer(peer)

With this build my compile builds to exactly the max .text size (32768 bytes) for the esp8266.
Note: A number of defensive arg and consistency checks are also removed from the esp8266 espnow code.

@peq123
Copy link

@peq123 peq123 commented Dec 1, 2020

Is there any indication when this will be merged and available for download as part of the main micropython binaries?

@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Dec 14, 2020

Sorry for the slow response (been away on holiday for a few weeks). I don't really know how to get an eye on the process for getting PRs reviewed and merged. I'm just hoping it will be soon-ish. Any suggestions for prodding the decision makers are welcome :-).

 - No more ring buffer for sent packet responses. Just count
   transmission failures.
 - Alloc and init callee-owned tuple and bytearrays (for irecv()) in
   the ESPnow() constructor instead of on first call to irecv().
 - Better comment coverage and fixups for new code
 - espnow_send(): Wait on send_timeout if ESPNOW internal buffer is full
 - Streamline some snippets of code
 - Update docs for latest changes.
 - Revert mod_peer(key=...) to mod_peer(lmk=...) for consistency with
   Espressif esp32 docs.
 - lmk *must* be 16 bytes long now. The aim is to be more explicit about
   security keys. _get_bytes_max_len() is no longer needed.
@glenn20
Copy link
Contributor Author

@glenn20 glenn20 commented Dec 19, 2020

I have added some updates:

  • Merged in the esp8266 support (is only compiled in for GENERIC_1M and GENERIC build targets).
  • Some code simplifications and optimisations for the ESP32 (inspired by code reductions required for esp8266).
  • Updates to the docs.

ESP32 and ESP8266 users can get their code from this one branch now.

@glenn20 glenn20 force-pushed the glenn20:espnow-g20 branch from 9673e12 to 940b47b Dec 19, 2020
Pared down version of the esp32 espnow support. Supports sync and
non-sync send()s and alloc-free buffered irecv() with optional timeout.
 - init([recv_bufsize])
 - deinit()
 - irecv([timeout])
 - send(peer, message, [sync])
 - add_peer(peer[, lmk[, channel]])
 - del_peer(peer)

There is NO support for the stream IO functions or asyncio.
This is pared down to fit in the .text segment for esp8266
(total textsize is exactly 32768 bytes in GENERIC_1M build).

Conditional compile flag so espnow is not compiled for GENERIC_512K
target (it is too big to include in standard build). (Also includes
minor formatting fixes for esp32).
@glenn20 glenn20 force-pushed the glenn20:espnow-g20 branch from 940b47b to bb6b9b8 Dec 21, 2020
@glenn20 glenn20 changed the title esp32: more progress on ESP-NOW support esp32: ESP-NOW support for ESP32 and ESP8266 Dec 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

7 participants