Skip to content

Instantly share code, notes, and snippets.

@anecdata
Last active November 4, 2024 20:49
Show Gist options
  • Save anecdata/f46a1d07add5fc60cfbcf42dc7be6528 to your computer and use it in GitHub Desktop.
Save anecdata/f46a1d07add5fc60cfbcf42dc7be6528 to your computer and use it in GitHub Desktop.
CircuitPython example for Espressif ESP-NOW protocol
# SPDX-FileCopyrightText: 2023 anecdata
#
# SPDX-License-Identifier: MIT
import time
import traceback
import supervisor
import os
import rtc
import espnow
import espidf
import wifi
import socketpool
import adafruit_ntp
from sekrets import *
#### ESPNOW Receiver
TZ_DEFAULT = -5
SNDR_CH = 0 # channel 1 (unless connected to an AP or acting as an AP)
def struct_time_to_iso_time():
st = time.localtime()
tz = TZ_DEFAULT
return f"{st[0]:04d}-{st[1]:02d}-{st[2]:02d}T{st[3]:02d}:{st[4]:02d}:{st[5]:02d}{tz:+03}:00"
def connect():
try:
wifi.radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD"), bssid=AP_BSSID) # use AP on channel <> 1
time.sleep(1) # wait for ap_info
print(f"{struct_time_to_iso_time()} ipv4={wifi.radio.ipv4_address} channel={wifi.radio.ap_info.channel} rssi={wifi.radio.ap_info.rssi}")
except ConnectionError as ex:
traceback.print_exception(ex, ex, ex.__traceback__)
def ntp_to_rtc():
wifi.radio.enabled = True
connect()
try:
ntp = adafruit_ntp.NTP(pool, tz_offset=TZ_DEFAULT)
rtc.RTC().datetime = ntp.datetime
print(f"{struct_time_to_iso_time()} RTC time set with NTP time")
except Exception as e:
traceback.print_exception(e, e, e.__traceback__)
wifi.radio.enabled = False # lose the wifi channel
time.sleep(3) # wait for serial
print(f"{'='*25}")
pool = socketpool.SocketPool(wifi.radio)
ntp_to_rtc()
peers = [espnow.Peer(mac=SNDR_MAC, lmk=SNDR_LMK, encrypted=True, channel=SNDR_CH),]
while True:
with espnow.ESPNow() as e:
e.set_pmk(RCVR_PMK)
peers_report = ""
for peer in peers:
e.peers.append(peer)
peers_report += f"mac={peer.mac} lmk={peer.lmk} ch={peer.channel} if={peer.interface} enc={peer.encrypted}\n"
print(f"{'-'*25}\n{struct_time_to_iso_time()} Receiving...", end=" ")
while True:
if e:
try:
packet = e.read()
print(f"{packet}")
break
except ValueError as ex: # Invalid buffer
traceback.print_exception(ex, ex, ex.__traceback__)
supervisor.reload()
break
print(f"send=[{e.send_success} {e.send_failure}] read=[{e.read_success} {e.read_failure}] buf={e.buffer_size} phy={e.phy_rate} peers={peers_report}", end="")
@anecdata
Copy link
Author

anecdata commented Apr 2, 2023

It can typically take up to several milliseconds for e.send_success and e.send_failure to become valid. One way to handle that:

    try:
        e.send(f"{struct_time_to_iso_time().encode()}")

        delivery_time = 0
        start = time.monotonic_ns()
        while not e.send_success and not e.send_failure:
            if (delivery_time := time.monotonic_ns() - start) > SEND_TIMEOUT:
                break

@anecdata
Copy link
Author

anecdata commented Apr 5, 2023

Needs some parameter validation?

Adafruit CircuitPython 8.1.0-beta.0-80-g22636e056 on 2023-03-29; Adafruit Feather ESP32-S2 TFT with ESP32S2
>>> import time
>>> import espnow
>>> from sekrets import *
>>> 
>>> RCVR_CH = 0 
>>> 
>>> e = espnow.ESPNow()
>>> e.set_pmk(SYS_PMK)
>>> peer = espnow.Peer(mac=RCVR_MAC, lmk=LMK_181, encrypted=True, channel=RCVR_CH)
>>> e.peers.append(peer)
>>> 
>>> e.send(f"{'#'*250}")
>>> time.sleep(0.1)  # delivery confirmation takes non-zero time
>>> print(f"send=[{e.send_success} {e.send_failure}] read=[{e.read_success} {e.read_failure}]")
send=[1 0] read=[1 0]
>>> 
>>> e.send(f"{'#'*251}")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
espidf.IDFError: ESP-NOW error 0x306a
>>> time.sleep(0.1)  # delivery confirmation takes non-zero time
>>> print(f"send=[{e.send_success} {e.send_failure}] read=[{e.read_success} {e.read_failure}]")
send=[1 0] read=[1 0]
>>> 
>>> e.send(f"{'#'*250}")
>>> time.sleep(0.1)  # delivery confirmation takes non-zero time
>>> print(f"send=[{e.send_success} {e.send_failure}] read=[{e.read_success} {e.read_failure}]")
send=[2 0] read=[1 0]
>>> 

@anecdata
Copy link
Author

anecdata commented Apr 24, 2023

For ease of navigation...
Original Issue: adafruit/circuitpython#3999
Original PR: adafruit/circuitpython#7470
Issue filed for above comments: adafruit/circuitpython#7903

@tyeth
Copy link

tyeth commented Feb 11, 2024

Thanks for compiling/sharing this @anecdata 🥇 I hadn't seen the micropython notes on esp-now.

Updated link from Archive.org for micropython-espnow power related operation notes:
https://web.archive.org/web/20230520021624/https://micropython-glenn20.readthedocs.io/en/latest/library/espnow.html#espnow-and-wifi-operation

@anecdata
Copy link
Author

Thanks for the updated link @tyeth. CircuitPython does power management slightly differently adafruit/circuitpython#6976 which I think reduces complexity for ESP-Now. Channel I think is still a challenge to manage for now.

@stanelie
Copy link

stanelie commented Oct 31, 2024

The sender example does not run on circuitpython 9.1.4 on a Waveshare ESP32-S3-Zero. I get several error messages (example : "ImportError: no module named 'sekrets'"). Is this expected behavior? Should I try this on circuitpython 8 instead?

@anecdata
Copy link
Author

anecdata commented Oct 31, 2024

@stanelie That was a file I used but didn't publish for the encryption keys, etc. (see the all-caps constants in the code, like RCVR_MAC and RCVR_LMK). You can put them anywhere you like and import them how you like.

@stanelie
Copy link

Do you have examples of how this data should be formatted? For example, the mac address should be AB:CD:EF:GH:IJ or something like ['0x24', '0xec', '0x4a', '0x26', '0x8d', '0xb0'] ?

@anecdata
Copy link
Author

anecdata commented Oct 31, 2024

@stanelie
Copy link

Thanks!

@stanelie
Copy link

stanelie commented Nov 4, 2024

How about setting the data rate for long range?
The doc says "espnow.ESPNow(buffer_size: = 526, phy_rate: int = 0), but I don't see what 0 means (1mbps? 56mbps?)
The values in the reference https://docs.espressif.com/projects/esp-idf/en/release-v4.4/esp32/api-reference/network/esp_wifi.html#_CPPv415wifi_phy_rate_t are not integer values, as expected by the Circuitpython library.

@anecdata
Copy link
Author

anecdata commented Nov 4, 2024

@stanelie it's an enumeration, so increasing integer values
https://github.com/espressif/esp-idf/blob/a9d0f22193acdf47a5a4db36832ae7068818962b/components/esp_wifi/include/esp_wifi_types.h#L594
the default is 0 (1Mbps), but you can change it

@stanelie
Copy link

stanelie commented Nov 4, 2024

Thanks. Is there a better place for my questions than this thread?

@stanelie
Copy link

stanelie commented Nov 4, 2024

I've tried all the phy_rate values between 0 and 32, and 16 and up do not work. So, I am unable to select the 31 or 32nd values (lora 250 and lora 500) that I want to enable long range. Where should I file a bug report about this issue?

@anecdata
Copy link
Author

anecdata commented Nov 4, 2024

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