kopia lustrzana https://github.com/peterhinch/micropython-samples
156 wiersze
5.7 KiB
Python
156 wiersze
5.7 KiB
Python
# server.py Minimal server.
|
|
|
|
# Released under the MIT licence.
|
|
# Copyright (C) Peter Hinch 2018
|
|
|
|
# Maintains bidirectional full-duplex links between server applications and
|
|
# multiple WiFi connected clients. Each application instance connects to its
|
|
# designated client. Connections areresilient and recover from outages of WiFi
|
|
# and of the connected endpoint.
|
|
# This server and the server applications are assumed to reside on a device
|
|
# with a wired interface.
|
|
|
|
# Run under MicroPython Unix build.
|
|
|
|
import usocket as socket
|
|
import uasyncio as asyncio
|
|
import utime
|
|
import primitives as asyn
|
|
from client_id import PORT
|
|
|
|
# Global list of open sockets. Enables application to close any open sockets in
|
|
# the event of error.
|
|
socks = []
|
|
|
|
# Read a line from a nonblocking socket. Nonblocking reads and writes can
|
|
# return partial data.
|
|
# Timeout: client is deemed dead if this period elapses without receiving data.
|
|
# This seems to be the only way to detect a WiFi failure, where the client does
|
|
# not get the chance explicitly to close the sockets.
|
|
# Note: on WiFi connected devices sleep_ms(0) produced unreliable results.
|
|
async def readline(s, timeout):
|
|
line = b''
|
|
start = utime.ticks_ms()
|
|
while True:
|
|
if line.endswith(b'\n'):
|
|
if len(line) > 1:
|
|
return line
|
|
line = b''
|
|
start = utime.ticks_ms() # A blank line is just a keepalive
|
|
await asyncio.sleep_ms(100) # See note above
|
|
d = s.readline()
|
|
if d == b'':
|
|
raise OSError
|
|
if d is not None:
|
|
line = b''.join((line, d))
|
|
if utime.ticks_diff(utime.ticks_ms(), start) > timeout:
|
|
raise OSError
|
|
|
|
async def send(s, d, timeout):
|
|
start = utime.ticks_ms()
|
|
while len(d):
|
|
ns = s.send(d) # OSError if client fails
|
|
d = d[ns:]
|
|
await asyncio.sleep_ms(100) # See note above
|
|
if utime.ticks_diff(utime.ticks_ms(), start) > timeout:
|
|
raise OSError
|
|
|
|
# Return the connection for a client if it is connected (else None)
|
|
def client_conn(client_id):
|
|
try:
|
|
c = Connection.conns[client_id]
|
|
except KeyError:
|
|
return
|
|
if c.ok():
|
|
return c
|
|
|
|
# API: application calls server.run()
|
|
# Not using uasyncio.start_server because of https://github.com/micropython/micropython/issues/4290
|
|
async def run(timeout, nconns=10, verbose=False):
|
|
addr = socket.getaddrinfo('0.0.0.0', PORT, 0, socket.SOCK_STREAM)[0][-1]
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
socks.append(s)
|
|
s.bind(addr)
|
|
s.listen(nconns)
|
|
verbose and print('Awaiting connection.')
|
|
while True:
|
|
yield asyncio.IORead(s) # Register socket for polling
|
|
conn, addr = s.accept()
|
|
conn.setblocking(False)
|
|
try:
|
|
idstr = await readline(conn, timeout)
|
|
verbose and print('Got connection from client', idstr)
|
|
socks.append(conn)
|
|
Connection.go(int(idstr), timeout, verbose, conn)
|
|
except OSError:
|
|
if conn is not None:
|
|
conn.close()
|
|
|
|
# A Connection persists even if client dies (minimise object creation).
|
|
# If client dies Connection is closed: .close() flags this state by closing its
|
|
# socket and setting .conn to None (.ok() == False).
|
|
class Connection():
|
|
conns = {} # index: client_id. value: Connection instance
|
|
@classmethod
|
|
def go(cls, client_id, timeout, verbose, conn):
|
|
if client_id not in cls.conns: # New client: instantiate Connection
|
|
Connection(client_id, timeout, verbose)
|
|
cls.conns[client_id].conn = conn
|
|
|
|
def __init__(self, client_id, timeout, verbose):
|
|
self.client_id = client_id
|
|
self.timeout = timeout
|
|
self.verbose = verbose
|
|
Connection.conns[client_id] = self
|
|
# Startup timeout: cancel startup if both sockets not created in time
|
|
self.lock = asyn.Lock(100)
|
|
self.conn = None # Socket
|
|
loop = asyncio.get_event_loop()
|
|
loop.create_task(self._keepalive())
|
|
|
|
def ok(self):
|
|
return self.conn is not None
|
|
|
|
async def _keepalive(self):
|
|
to = self.timeout * 2 // 3
|
|
while True:
|
|
await self.write('\n')
|
|
await asyncio.sleep_ms(to)
|
|
|
|
async def readline(self):
|
|
while True:
|
|
if self.verbose and not self.ok():
|
|
print('Reader Client:', self.client_id, 'awaiting OK status')
|
|
while not self.ok():
|
|
await asyncio.sleep_ms(100)
|
|
self.verbose and print('Reader Client:', self.client_id, 'OK')
|
|
try:
|
|
line = await readline(self.conn, self.timeout)
|
|
return line
|
|
except (OSError, AttributeError): # AttributeError if ok status lost while waiting for lock
|
|
self.verbose and print('Read client disconnected: closing connection.')
|
|
self.close()
|
|
|
|
async def write(self, buf):
|
|
while True:
|
|
if self.verbose and not self.ok():
|
|
print('Writer Client:', self.client_id, 'awaiting OK status')
|
|
while not self.ok():
|
|
await asyncio.sleep_ms(100)
|
|
self.verbose and print('Writer Client:', self.client_id, 'OK')
|
|
try:
|
|
async with self.lock: # >1 writing task?
|
|
await send(self.conn, buf, self.timeout) # OSError on fail
|
|
return
|
|
except (OSError, AttributeError):
|
|
self.verbose and print('Write client disconnected: closing connection.')
|
|
self.close()
|
|
|
|
def close(self):
|
|
if self.conn is not None:
|
|
if self.conn in socks:
|
|
socks.remove(self.conn)
|
|
self.conn.close()
|
|
self.conn = None
|