Skip to content

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

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

Merged
merged 2 commits into from
May 1, 2023

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!!

Docs
API docs are provided in 3d4058b. See docs/library/espnow.rst. The latest API docs can be browsed at: https://micropython-glenn20.readthedocs.io/en/latest/library/espnow.html.

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

Pre-compiled images
Pre-compiled micropython images with ESPNow support are provided at: https://github.com/glenn20/micropython-espnow-images.

Issues
If you are a user of this PR or the images I provide, please post any Issues at https://github.com/glenn20/micropython/issues or https://github.com/glenn20/micropython-espnow-images/issues.

This conversation thread is intended for discussion about this PR and possible eventual merge into micropython.

Comments, suggestion are welcome.

@nickzoic
Copy link
Contributor

nickzoic commented Oct 5, 2020

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

@tve
Copy link
Contributor

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 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 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 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 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 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 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 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 commented Oct 11, 2020 via email

@beyonlo
Copy link

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 commented Oct 15, 2020 via email

@glenn20
Copy link
Contributor Author

glenn20 commented Oct 15, 2020 via email

@glenn20
Copy link
Contributor Author

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.

@glenn20
Copy link
Contributor Author

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

Kudos for the uasyncio support 👍

@glenn20
Copy link
Contributor Author

glenn20 commented Oct 19, 2020 via email

@glenn20
Copy link
Contributor Author

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

@glenn20
Copy link
Contributor Author

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 espnow-g20 branch 2 times, most recently from 940b47b to bb6b9b8 Compare December 21, 2020 03:20
@glenn20 glenn20 changed the title esp32: more progress on ESP-NOW support esp32: ESP-NOW support for ESP32 and ESP8266 Dec 22, 2020
@mhepp63
Copy link

mhepp63 commented Jan 27, 2021

Hello,

thank you for adding this feature to Micropython. I verified that code is working with actual master branch of MPY and I can send messages esp8266 -> esp32 and esp32 -> esp32. I hope it will be merged soon ;)

@glenn20
Copy link
Contributor Author

glenn20 commented Jan 28, 2021

I have just applied a commit which:

  • mod_peer(): bug fix - alignment of args was incorrect
  • irecv(): on timeout, set length of callee-owned msg buffer to zero.
  • Minor code and docs cleanups.

I have also rebased against micropython/master as of 28 Jan 2021.

@glenn20
Copy link
Contributor Author

glenn20 commented Apr 28, 2023

New commit implementing requested changes (will squash later).

@glenn20 glenn20 force-pushed the espnow-g20 branch 2 times, most recently from bed8c46 to c6f950e Compare April 28, 2023 05:29
@glenn20 glenn20 requested a review from dpgeorge April 28, 2023 05:48
@glenn20 glenn20 force-pushed the espnow-g20 branch 2 times, most recently from 74bb60a to 0cfb346 Compare April 30, 2023 07:00
@dpgeorge
Copy link
Member

dpgeorge commented May 1, 2023

I think once the above comments are addressed this is go to be merged! If you do a rebase, please put the ringbuf commit first (because the espnow commit relies on it).

@glenn20
Copy link
Contributor Author

glenn20 commented May 1, 2023

I think once the above comments are addressed this is go to be merged! If you do a rebase, please put the ringbuf commit first (because the espnow commit relies on it).

OK. All done. I have also rebased against latest master. The multi-tests succeed on my ESP8266, ESP32, ESP32S2 and ESP32S3 test devices.

@dpgeorge
Copy link
Member

dpgeorge commented May 1, 2023

OK, code looks good. Only thing left is some minor tweaks to the commit messages. I would do that during merge, but let me know if you object to that and want to do it yourself.

@glenn20
Copy link
Contributor Author

glenn20 commented May 1, 2023

OK, code looks good. Only thing left is some minor tweaks to the commit messages. I would do that during merge, but let me know if you object to that and want to do it yourself.

Completely happy for you to reword the commit messages as you see fit.

Thanks for helping get this through and the improvements to the code. 👍

glenn20 added 2 commits May 1, 2023 16:47
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 micropython#4115.
Initial import of code is from:
https://github.com/nickzoic/micropython/tree/espnow-4115.
@dpgeorge dpgeorge merged commit 7fa322a into micropython:master May 1, 2023
@dpgeorge
Copy link
Member

dpgeorge commented May 1, 2023

Merged!

Thanks @glenn20 for persisting with this for such a long time, and to everyone else involved.

@glenn20
Copy link
Contributor Author

glenn20 commented May 1, 2023

Merged!

Thanks @glenn20 for persisting with this for such a long time, and to everyone else involved.

Especially to @nickzoic for such a good start. 👍

@TJC
Copy link

TJC commented May 1, 2023

So happy to see this finally merged! Congrats

@nickzoic
Copy link
Contributor

nickzoic commented May 1, 2023 via email

@dpgeorge
Copy link
Member

dpgeorge commented May 1, 2023

Yes, thank you @nickzoic !

@AmirHmZz
Copy link
Contributor

AmirHmZz commented May 1, 2023

@glenn20 @nickzoic @dpgeorge Thank you all for bringing ESP-NOW to MicroPython!

@bulletmark
Copy link
Contributor

The MP master branch documentation says I can use this but the code says I can't?

>>> import espnow
>>> import aioespnow
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: no module named 'aioespnow'
>>> 

@dpgeorge
Copy link
Member

dpgeorge commented May 4, 2023

@bulletmark aioespnow is not yet part of this repo in the micropython-lib submodule and so not frozen into firmware, but will be soon. In the meantime you can install it from micropython-lib, eg via mpremote mip install aioespnow (run locally).

@bulletmark
Copy link
Contributor

Thanks Damien. I set up 3 slaves and a master and it works great. I'm completely new to ESPNow but the MP documentation on it is detailed and clear. This is a seriously cool addition to MP!

@glenn20
Copy link
Contributor Author

glenn20 commented May 4, 2023

@dpgeorge : thanks for merging aioespnow into micropython-lib.

Just a reminder of what I think my TODOs are:

  • Once the micropython-lib version in micropython/lib is updated to include aioespnow, I can post another PR to uncomment the # require("aioespnow") in ports/esp{32,8266}/boards/manifest.py.

    Actually, I've realised that for the ESP8266, that should be in ports/esp8266/boards/GENERIC/manifest.py instead (only GENERIC has uasyncio support).

  • Once ports/esp32-esp8266: Add support for set/get the wifi power saving mode. #8993 is merged, I'll also want to make a small update to the docs/library/espnow.rst (I removed the guidance on using the "pm" option so that espnow can work smoothly alongside wifi, and should put it back once the PR is merged).

If you have any concerns or changes, let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.