Description
My Platform:
- metro m4 airlift lite
- adafruit nina-fw 1.7.3
- circuitpython 6.2.0
- April 20, 2021 library bundle
The problem:
udp.send() appends to the write buffer instead of replacing it. This results in the following behavior:
sock.send("line 1+\n")
sock.send("line 2++\n")
sock.send("line 3+++\n")
will actually send
line 1+ <--- datagram 1
line 1+ <--- datagram 2
line 2++ |
line 1+ <--- datagram 3
line 2++ |
line 3+++ |
and this continues until the esp32's internal write buffer is full. at which point calls to send() will only send the buffer and not append anything new.
The expected behavior is that a call to write()
will send the provided data as a datagram packet and nothing else.
Why this happens
In the nina-fw WiFiUdp.c implementation we can see the following:
_sndSize
tracks the write buffer size for the current packetbeginPacket()
sets_sndSize
to 0. This is the only place where_sndSize
is set to 0.endPacket()
does not set_sndSize
to 0.
The only nina-fw command that calls beginPacket()
is the startTcpClient() command which, despite the outdated name, is also used to initialize UDP sockets.
sendUDPData() only calls endPacket()
. It does not call beginPacket()
.
socket_write()
has code roughly equivalent (with simplification and no handling of large buffers) to this:
if self.UDP_MODE:
self._send_command_get_response(_INSERT_DATABUF_TCP_CMD, self._socknum_ll[0], memoryview(buffer))
self._send_command_get_response(_SEND_UDP_DATA_CMD, self._socknum_ll)
_INSERT_DATABUF_TCP_CMD
writes data to the UDP socket buffer, and then _SEND_UDP_DATA_CMD
calls endPacket()
which sends the current buffer as a datagram packet. beginPacket()
is never called during a socket_write()
.
Workaround
Currently you can work around this problem by closing and re-opening the socket before every write. So every write()
now becomes
sock.close()
sock.connect(socketaddr, conntype=esp.UDP_MODE)
sock.write()
The close()
is necessary because connect()
also opens a "server" port for receiving responses, and so connect()
will fail if that server port is already open.
Potential fixes
It's not entirely clear to me whether this is a bug in esp32spi or in nina-fw. Changing esp32spi's socket_write
to send a _START_SERVER_TCP_CMD
message after sending _SEND_UDP_DATA_CMD
would fix this issue. Changing nina-fw's sendUDPData()
command to call beginPacket()
again after endPacket()
would also solve this issue. So would changing both to add a new command specifically to call beginPacket()
into nina-fw and using it in esp32wifi.
So I'm happy to write a patch to fix this, but I don't know which project i should be writing the patch for.
Example code to cause this problem
import board
import time
from digitalio import DigitalInOut
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
from adafruit_esp32spi import adafruit_esp32spi
from secrets import secrets
esp32_cs = DigitalInOut(board.ESP_CS)
esp32_ready = DigitalInOut(board.ESP_BUSY)
esp32_reset = DigitalInOut(board.ESP_RESET)
spi = board.SPI()
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
HOST = "192.168.1.100"
PORT = 3000
def connect():
esp.reset()
if esp.status == adafruit_esp32spi.WL_IDLE_STATUS:
print("ESP32 found and in idle mode")
print("Firmware vers.", esp.firmware_version)
print("MAC addr:", [hex(i) for i in esp.MAC_address])
print("Connecting to AP...")
while not esp.is_connected:
try:
esp.connect_AP(secrets["ssid"], secrets["password"])
except RuntimeError as e:
print("could not connect to AP, retrying: ", e)
continue
print("Connected to", str(esp.ssid, "utf-8"), "\tRSSI:", esp.rssi)
print("My IP address is", esp.pretty_ip(esp.ip_address))
socket.set_interface(esp)
socketaddr = socket.getaddrinfo(HOST, PORT)[0][4]
global sock
sock = socket.socket(type=socket.SOCK_DGRAM)
sock.connect(socketaddr, conntype=esp.UDP_MODE)
connect()
i = 0
while True:
if esp.is_connected:
sock.send("{}\n".format(i))
i = i + 1
else:
print("wifi is down :<")
sock.close()
connect()
time.sleep(1.0)