adafruit / adafruit/Adafruit_CircuitPython_ESP32SPI
UDP send() does not clear esp32 write buffer
- Dominant language
- Python
- Stars
- 102
- Forks
- 71
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 1
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](https://github.com/adafruit/nina-fw/blob/master/arduino/libraries/WiFi/src/WiFiUdp.cpp) we can see the following:
- `_sndSize` tracks the write buffer size for the current packet
- `beginPacket()` 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()](https://github.com/adafruit/nina-fw/blob/ec9e20f5085a121bfeb6348a9ab98bba69193369/main/CommandHandler.cpp#L531) command which, despite the outdated name, is also used to initialize UDP sockets.
[sendUDPData()](https://github.com/adafruit/nina-fw/blob/ec9e20f5085a121bfeb6348a9ab98bba69193369/main/CommandHandler.cpp#L763) only calls `endPacket()`. It does not call `beginPacket()`.
`socket_write()` has code roughly equivalent (with simplification and no handling of large buffers) to this:
```python
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
```python
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
```python
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)
```
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by tracing socket_write() in the ESP32SPI socket implementation and compare its UDP command sequence with sendUDPData() and startTcpClient() in nina-fw's CommandHandler.cpp, plus beginPacket() and endPacket() in WiFiUdp.cpp. Reproduce the example and verify that each UDP datagram contains only the current payload, then add a regression test if the relevant test location is identified.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, python
- Domain
- embedded-iot, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100