W5500-EVB-PICO: setblocking, SOL_SOCKET, SO_REUSEADDR NOT working; Cannot use Asyncio start_server()
I'm currently working with the Wiznet W5500-EVB-PICO. For those who aren't familiar, it's essentially a Raspberry Pi Pico and a Wiznet Ethernet Hat combined onto a single board and, as the name implies, gives you a wired ethernet connection.
I'm currently using the following firmware: v1.0.5
Version information from the REPL:
MicroPython v1.18-10-g5db278f1d-dirty on 2022-05-10; Raspberry Pi Pico with RP2040
I'm trying to use Asyncio to start a server.
When I run this code I receive the following error(s):
Task exception wasn't retrieved
future: <Task> coro= <generator object 'start_server' at 2000d700>
Traceback (most recent call last):
File "uasyncio/core.py", line 1, in run_until_complete
File "uasyncio/stream.py", line 1, in start_server
OSError: [Errno 107] ENOTCONN
For reference, similar versions of the above code have worked on both an ESP8266 and Raspberry Pi Pico W.
The Raspberry Pi Pico W version that I've used can be found here.
Upon further inspection it seems that using the following seem to be causing issues:
- socket.setblocking(False)
- socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
To this hypothesis, I used some known-good example code for a small non-asynchronus HTTP server and added the following lines of code (one at a time) to see if I could invoke the error.
s.setblocking(False)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
With "s.setblocking(False)" uncommented on line 51, the following error occurs:
Traceback (most recent call last):
File "<stdin>", line 80, in <module>
File "<stdin>", line 51, in main
OSError: [Errno 107] ENOTCONN
With "s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)" uncommented at line 52, the following error occurs:
Traceback (most recent call last):
File "<stdin>", line 80, in <module>
File "<stdin>", line 52, in main
AttributeError: type object 'socket' has no attribute 'SOL_SOCKET'
It seems that start_server() uses both of these lines of code and, they don't seem to be compatible with the W5500 wrapper/driver that is used in this firmware.
I respectfully request that someone look into this issue. I'll provide any additional information that is required. I'm a hardware guy who recently started doing software so I'll try my best.
docs: asyncio start_server does not behave as described
Micropython version: 1.22.1
Board: W5500-EVB-Pico
Wiznet has a board that adds ethernet capability to a Raspberry Pi Pico - the W5500-EVB-Pico. I'd like to use this to implement network connected instruments in my home lab.
There are plenty of examples on the documentation site, but the TCP examples all seem to use low-level socket programming. The latest Micropython implementations all have asyncio available so why not use the facilities available there?
For example Bytepawn's simple message queue server. Here is a snippet that shows how start_server is used:
async def run_server(host, port):
server = await asyncio.start_server(handle_client, host, port)
print(f'Listening on {host}:{port}...')
async with server:
await server.serve_forever()
asyncio.run(run_server(host='localhost', port=int(sys.argv[1])))
Nope. Doesn't work. The problem seems to be that even though the Micropython documentation says that the asyncio.start_server function works in the same way as the standard library - it doesn't. Specifically, the documentation states that start_server returns an instance of the Server class. It doesn't - it returns a <generator>.
After some digging on the net, I came up with the code below (mainly through the infinite monkey procedure).
This code uses start_server but then runs the resulting generator using an explicitly constructed asyncio event_loop. There is an advantage to this, though, in that this also allows us to start multiple tasks, or multiple servers at the same time.
The create_handler function builds a parametrised handler that is passed to start_server. It can be parametrised with a function that transforms from a string to a string. The default is just the identity function.
Run the program and use telnet to connect to to the board on either port 7777 - which acts as an echo server, or port 7778 which uppercases the reply.
import sys, asyncio
#--------------------------------------------------------------------------
# W5500-EVB-Pico board and network configuration
#--------------------------------------------------------------------------
# on micropython systems, this can be placed in boot.py
from machine import Pin,SPI
import network
import time
#W5x00 interface configuration
def config():
spi=SPI(0,2_000_000, mosi=Pin(19),miso=Pin(16),sck=Pin(18))
nic = network.WIZNET5K(spi,Pin(17),Pin(20)) #spi,cs,reset pin
nic.active(True)
#Use if static IP address is required
#nic.ifconfig(('192.168.1.20','255.255.255.0','192.168.1.1','8.8.8.8'))
#DHCP
nic.ifconfig('dhcp')
print('IP address :', nic.ifconfig())
while not nic.isconnected():
time.sleep(1)
print(nic.regs())
#--------------------------------------------------------------------------
# END network configuration
#--------------------------------------------------------------------------
async def handle_client(reader, writer):
print('New client connected...')
line = str()
while line.strip() != 'quit':
line = (await reader.readline()).decode('utf8')
if line.strip() == '': continue
print(f'Received: {line.strip()}')
writer.write(line.encode('utf8'))
writer.close()
print('Client disconnected...')
def create_handler(transformer = lambda x : x):
async def handle_client(reader, writer):
print('New client connected...')
line = str()
while line.strip() != 'quit':
line = (await reader.readline()).decode('utf8')
if line.strip() == '': continue
print(f'Received: {line.strip()}')
writer.write(transformer(line).encode('utf8'))
writer.close()
print('Client disconnected...')
return handle_client
async def Main():
config()
loop = asyncio.get_event_loop()
loop.create_task(asyncio.start_server(create_handler(), '0.0.0.0',7777))
loop.create_task(asyncio.start_server(create_handler(str.upper), '0.0.0.0',7778))
loop.run_forever()
try:
asyncio.run(Main())
finally:
asyncio.new_event_loop()