adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
MIT License
3.96k stars 1.16k forks source link

ESP-NOW error 0x306a if channel is not 1 #7903

Open anecdata opened 1 year ago

anecdata commented 1 year ago

CircuitPython version

Adafruit CircuitPython 8.1.0-beta.0-80-g22636e056 on 2023-03-29; Adafruit Feather ESP32-S2 TFT with ESP32S2

Code/REPL

Code and initial testing commentary here (also some on Discord): https://gist.github.com/anecdata/f46a1d07add5fc60cfbcf42dc7be6528

Behavior

The most limiting issue right now I think is that if peer channel is set to anything except 0 (default; becomes channel 1) or 1, send will result in espidf.IDFError: ESP-NOW error 0x306a. ESP-NOW should operate on any channel. This also prevents running wifi and ESP-NOW simultaneously unless the AP is on channel 1.

A couple of other initial questions or issues after testing ESP-NOW... we can put them into separate issues if warranted:

Allocate and initialize ESPNow instance as a singleton.

...also unexpected:

>>> import espnow
>>> 
>>> with espnow.ESPNow() as e:
...     pass
>>> e
<ESPNow>
>>> 
>>> e.deinit()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Object has been deinitialized and can no longer be used. Create a new object.

Addendum: Two additional nice-to-have features:

Addendum 2: Another issue:

Description

No response

Additional information

No response

aziascreations commented 1 year ago

Hello, I was recently playing around with the espnow module and encountered most of these issues.

However, I found some workarounds and made some examples that are might be worth sharing for anyone who will need to work on these issues or want to use the module thoroughly.

Broadcast messages

While the send method may return a 0x3066 or 0x3069 error when broadcasting, it is only does so under some specific conditions:

Here is an example for these conditions:

Demo sender's code ```python import espidf import espnow import time import wifi MAC_SOURCE = b'\xf4\x12\xfa\xcb\xe8\x94' # Somewhat irrelevant MAC_BROADCAST = b'\xFF\xFF\xFF\xFF\xFF\xFF' MAC_TARGET = b'\xdcTueX\xb8' # You need to have a listener with this MAC (Can be changed) MAC_NOBODY = b'\x01\x02\x03\x04\x05\x06' # Preparing the Wi-Fi radio wifi.radio.enabled = True wifi.radio.mac_address = MAC_SOURCE # Preparing ESP-NOW e = espnow.ESPNow() peer_broadcast = espnow.Peer(mac=MAC_BROADCAST) e.peers.append(peer_broadcast) # Sending messages with only 1 registered peer print("Sending message without specific target...") try: e.send("Hello world :)") print("> Success") except espidf.IDFError: # Should be "ESP-NOW error 0x3069" print("> Failure") print("Sending message with specific target...") try: e.send("Hello world :)", peer_broadcast) print("> Success") except espidf.IDFError: # Should not trigger print("> Failure") # Adding peer whose MAC isn't used by anyone. peer_nobody = espnow.Peer(mac=MAC_NOBODY) e.peers.append(peer_nobody) # Sending messages with 2 registered peers print("Sending message without specific target...") try: e.send("Hello world :)") print("> Success") except espidf.IDFError: # Should be triggerred print("> Failure") # Adding peer whose MAC is used and should receive messages. peer_valid = espnow.Peer(mac=MAC_TARGET) e.peers.append(peer_valid) # Sending messages with 3 registered peers print("Sending message without specific target...") try: e.send("Hello world :)") print("> Success") except espidf.IDFError: # Should not trigger print("> Failure") # Finishing print("Done !") e.deinit() ```

For the receiver, use the one below.

Channel switching

It appears that you can change the interface's channel by quickly creating an AP with the desired channel number and stopping it directly after.

The only issue with this approach is that you have to wait for any messages sent to be acknowledged or marked as a failed delivery in the internal callback. If you don't, the peer's target channel appear to be ignored and all messages will be sent on the initial channel.

See espnow.ESPNow.send_success and espnow.ESPNow.send_failure.

Timing issue examples All these examples have two devices: 1. A listener on channel 1. *(None on channel 9)* 2. A sender switching between channel 1 and 9. **No waiting** 1. Switch to channel 1 2. Add peer, send message, arrives on channel 1, unregister peer 3. Switch to channel 9 4. Add peer and send message 5. **Message arrives on channel 1** 6. **All future message will arrive on channel 1, even if alternating between 1 and 9** **Waiting for 100ms** 1. Switch to channel 1 2. Add peer, send message, arrives on channel 1, unregister peer 3. Switch to channel 9 4. Add peer, send message, arrives on channel 9, unregister peer 5. *Rince and repeat*
Sender's code ```python import espnow import time import wifi MAC_SOURCE = b'\xf4\x12\xfa\xcb\xe8\x94' MAC_TARGET = b'\xdcTueX\xb8' # Preparing the Wi-Fi radio wifi.radio.enabled = True wifi.radio.mac_address = MAC_SOURCE # Preparing ESP-NOW e = espnow.ESPNow() # Iterating over the desired channels message_count = 0 for channel in [1, 9, 1, 9, 1, 9, 1]: print("Sending a message on channel " + str(channel)) # Moving to the correct channel. wifi.radio.start_ap(ssid="Moving channels...", channel=channel) wifi.radio.stop_ap() # Re-creating the peer to share the target MAC. peer = espnow.Peer(mac=MAC_TARGET, channel=channel, interface=0) e.peers.append(peer) message = "Hello channel {}, this is message {} !".format(channel, message_count).encode("utf-8") # Sending data directly (Not working) #e.send(message, peer) # Sending data in a loop (Works just fine) for i in range(5): e.send(message, peer) time.sleep(0.1) # Freeing the peer and ESP-NOW e.peers.remove(peer) message_count = message_count + 1 # Finishing print("Done !") e.deinit() ```
Receiver's code The receiver's code is pretty much lifted as-is from the documentation and only has a bit to change the channel before initializing ESP-NOW and the main loop. ```python import espnow import wifi TARGET_CHANNEL = 9 wifi.radio.enabled = True wifi.radio.start_ap(ssid="NO_SSID", channel=TARGET_CHANNEL) wifi.radio.stop_ap() e = espnow.ESPNow() packets = [] print("Waiting for packets...") while True: if e: packet = e.read() print(packet) if packet.msg == b'end': break ```
casainho commented 12 months ago

I was also getting issues when using ESPNow to communicate between a ESP32-S2 and a S3. In all this, I found that the initialization need to be a bit different between them.

For ESP32-S2, the initialization need to be this (see that I am communicating with 2 different ESP32 boards, one with ESP32-S3 and other with ESP32-S2):

# MAC Address value needed for the wireless communication
my_mac_address = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf3]
mac_address_power_switch_board = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf1]
mac_address_motor_board = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf2]

wifi.radio.enabled = True
wifi.radio.mac_address = bytearray(my_mac_address)
wifi.radio.start_ap(ssid="NO_SSID", channel=1)
wifi.radio.stop_ap()

_espnow = ESPNow.ESPNow()
motor = motor_board_espnow.MotorBoard(_espnow, mac_address_motor_board, system_data) # System data object to hold the EBike data
power_switch = power_switch_espnow.PowerSwitch(_espnow, mac_address_power_switch_board, system_data) # System data object to hold the EBike data
The code to send and receive ```python class MotorBoard(object): def __init__(self, _espnow, mac_address, system_data): self._motor_board_espnow = _espnow peer = ESPNow.Peer(mac=bytes(mac_address), channel=1) self._motor_board_espnow.peers.append(peer) self._packets = [] self._system_data = system_data self.motor_board_espnow_id = 1 def process_data(self): try: data = self._motor_board_espnow.read() if data is not None: data = [n for n in data.msg.split()] self._system_data.battery_voltage_x10 = int(data[0]) self._system_data.battery_current_x100 = int(data[1]) * -1.0 self._system_data.motor_current_x100 = int(data[2]) * -1.0 self._system_data.motor_speed_erpm = int(data[3]) self._system_data.brakes_are_active = True if int(data[4]) == 1 else False except: supervisor.reload() def send_data(self): try: system_power_state = 1 if self._system_data.system_power_state else 0 self._motor_board_espnow.send(f"{int(self.motor_board_espnow_id)} {system_power_state}") except: supervisor.reload() ``` ```python class PowerSwitch(object): def __init__(self, _espnow, mac_address, system_data): self._system_data = system_data self.power_switch_id = 4 # power switch ESPNow messages ID self._espnow = _espnow self._peer = ESPNow.Peer(mac=bytes(mac_address), channel=1) self._espnow.peers.append(self._peer) def update(self): try: self._espnow.send(f"{self.power_switch_id} {int(self._system_data.display_communication_counter)} {int(self._system_data.turn_off_relay)}") except: supervisor.reload() ```

For ESP32-S3, the initialization need to be this (see that I am communicating only with a ESP32-S2):

wifi.radio.enabled = True
my_mac_address = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf2]
wifi.radio.mac_address = bytearray(my_mac_address)

# MAC Address value needed for the wireless communication with the display
display_mac_address = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf3]
display = display_espnow.Display(display_mac_address, system_data)
The code to send and receive ```python class Display(object): """Display""" def __init__(self, display_mac_address, system_data): self._system_data = system_data self.my_espnow_id = 1 self._espnow = ESPNow.ESPNow() peer = ESPNow.Peer(mac=bytes(display_mac_address), channel=1) self._espnow.peers.append(peer) def process_data(self): try: data = self._espnow.read() if data is not None: data = [n for n in data.msg.split()] # only process packages for us if int(data[0]) == self.my_espnow_id: self._system_data.motor_enable_state = True if int(data[1]) != 0 else False except: supervisor.reload() def update(self): try: brakes_are_active = 1 if self._system_data.brakes_are_active else 0 self._espnow.send(f"{int(self._system_data.battery_voltage_x10)} {int(self._system_data.battery_current_x100)} {int(self._system_data.motor_current_x100)} {self._system_data.motor_speed_erpm} {brakes_are_active}") except: supervisor.reload() ```
anecdata commented 12 months ago

I'm not entirely clear on the distinction, other than the shared _espnow for the (ESP32-S2) device with two peers. Does this setup not work if the ESP32-S2 and ESP32-S3 are swapped?

Everything is channel=1, so the AP workaround to set the channel may not be strictly necessary in this specific case.

casainho commented 12 months ago

I'm not entirely clear on the distinction, other than the shared _espnow for the (ESP32-S2) device with two peers. Does this setup not work if the ESP32-S2 and ESP32-S3 are swapped?

The only difference is that this code is needed on S2:

wifi.radio.start_ap(ssid="NO_SSID", channel=1)
wifi.radio.stop_ap()
gitcnd commented 6 days ago

I'm planning to take a look at this and https://github.com/adafruit/circuitpython/issues/9380 . For reference: ( https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/error-codes.html )

ESP_ERR_ESPNOW_INTERNAL (0x306a): Internal error

ESP_ERR_ESPNOW_NOT_FOUND (0x3069): ESPNOW peer is not found

I vaguely recall there's a matrix of things you can and/or cannot do at the same time in relation to wifi, channels, bluetooth, and espnow - I worked it out back in micropython days, so hopefully I can work it out here too.