From d50d5d146f434bb49ff34d5a47216e982bfae4c0 Mon Sep 17 00:00:00 2001 From: Neradoc Date: Wed, 22 Jan 2025 01:21:27 +0100 Subject: [PATCH 1/5] more compatible sockets: socket_write() and send() return the number of bytes sent add some of the constants used by httpserver (others missing) dummy entries for setsockopt(), listen() and setblocking() implement bind() by using ninafw start_server() implement accept() by getting a clien socket from that server add back socknum parameter to allow wrapping a Socket around a ninafw socket number, which is required to be able to receive a connection from the server --- adafruit_esp32spi/adafruit_esp32spi.py | 7 ++- .../adafruit_esp32spi_socketpool.py | 57 +++++++++++++++++-- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi.py b/adafruit_esp32spi/adafruit_esp32spi.py index f0ff6b6..03a4a66 100644 --- a/adafruit_esp32spi/adafruit_esp32spi.py +++ b/adafruit_esp32spi/adafruit_esp32spi.py @@ -825,7 +825,8 @@ def socket_connected(self, socket_num): return self.socket_status(socket_num) == SOCKET_ESTABLISHED def socket_write(self, socket_num, buffer, conn_mode=TCP_MODE): - """Write the bytearray buffer to a socket""" + """Write the bytearray buffer to a socket. + Returns the number of bytes written""" if self._debug: print("Writing:", buffer) self._socknum_ll[0][0] = socket_num @@ -853,7 +854,7 @@ def socket_write(self, socket_num, buffer, conn_mode=TCP_MODE): resp = self._send_command_get_response(_SEND_UDP_DATA_CMD, self._socknum_ll) if resp[0][0] != 1: raise ConnectionError("Failed to send UDP data") - return + return sent if sent != len(buffer): self.socket_close(socket_num) @@ -863,6 +864,8 @@ def socket_write(self, socket_num, buffer, conn_mode=TCP_MODE): if resp[0][0] != 1: raise ConnectionError("Failed to verify data sent") + return sent + def socket_available(self, socket_num): """Determine how many bytes are waiting to be read on the socket""" self._socknum_ll[0][0] = socket_num diff --git a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py index 91615dc..ff8821f 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py @@ -36,11 +36,15 @@ class SocketPool: """ESP32SPI SocketPool library""" - SOCK_STREAM = const(0) - SOCK_DGRAM = const(1) + # socketpool constants + SOCK_STREAM = const(1) + SOCK_DGRAM = const(2) AF_INET = const(2) - NO_SOCKET_AVAIL = const(255) + SOL_SOCKET = const(0xfff) + SO_REUSEADDR = const(0x0004) + # implementation specific constants + NO_SOCKET_AVAIL = const(255) MAX_PACKET = const(4000) def __new__(cls, iface: ESP_SPIcontrol): @@ -82,6 +86,7 @@ def __init__( type: int = SocketPool.SOCK_STREAM, proto: int = 0, fileno: Optional[int] = None, # noqa: UP007 + socknum: Optional[int] = None, # noqa: UP007 ): if family != SocketPool.AF_INET: raise ValueError("Only AF_INET family supported") @@ -89,7 +94,7 @@ def __init__( self._interface = self._socket_pool._interface self._type = type self._buffer = b"" - self._socknum = self._interface.get_socket() + self._socknum = socknum if socknum is not None else self._interface.get_socket() self.settimeout(0) def __enter__(self): @@ -121,13 +126,14 @@ def send(self, data): conntype = self._interface.UDP_MODE else: conntype = self._interface.TCP_MODE - self._interface.socket_write(self._socknum, data, conn_mode=conntype) + sent = self._interface.socket_write(self._socknum, data, conn_mode=conntype) gc.collect() + return sent def sendto(self, data, address): """Connect and send some data to the socket.""" self.connect(address) - self.send(data) + return self.send(data) def recv(self, bufsize: int) -> bytes: """Reads some bytes from the connected remote address. Will only return @@ -217,3 +223,42 @@ def _connected(self): def close(self): """Close the socket, after reading whatever remains""" self._interface.socket_close(self._socknum) + + #################################################################### + # WORK IN PROGRESS + #################################################################### + + def setsockopt(self, *opts, **kwopts): + """Dummy call for compatibility.""" + # FIXME + pass + + def listen(self, backlog): + """Dummy call for compatibility.""" + # FIXME + # probably nothing to do actually + # maybe check that we have called bind or something ? + pass + + def setblocking(self, blocking): + """Dummy call for compatibility.""" + # FIXME + # is this settimeout(0) ? (if True) or something else ? + pass + + def bind(self, host_port): + host, port = host_port + self._interface.start_server(port, self._socknum) + print(f"Binding to {self._socknum}") + + def accept(self): + client_sock_num = self._interface.socket_available(self._socknum) + if client_sock_num != SocketPool.NO_SOCKET_AVAIL: + sock = Socket(self._socket_pool, socknum=client_sock_num) + # get remote information (addr and port) + remote = self._interface.get_remote_data(client_sock_num) + IP_ADDRESS = "{}.{}.{}.{}".format(*remote['ip_addr']) + PORT = remote['port'] + client_address = (IP_ADDRESS, PORT) + return sock, client_address + raise OSError(errno.ECONNRESET) From d2d7ac3b9baff66d7e83f0feb3efd19a26fcf93f Mon Sep 17 00:00:00 2001 From: Neradoc Date: Wed, 22 Jan 2025 03:02:07 +0100 Subject: [PATCH 2/5] use time.monotonic_ns() to measure timeouts re-implement timeout similar to Circuitpython (None blocks, 0 doesn't) NOTE: this is a breaking change for any code that uses it bind() saves the bound address and port listen() uses the saved address and port or uses defaults some fixes for pylint/black --- .../adafruit_esp32spi_socketpool.py | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py index ff8821f..3bab399 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py @@ -14,7 +14,7 @@ from __future__ import annotations try: - from typing import TYPE_CHECKING, Optional + from typing import TYPE_CHECKING, Optional, Tuple if TYPE_CHECKING: from esp32spi.adafruit_esp32spi import ESP_SPIcontrol # noqa: UP007 @@ -40,7 +40,7 @@ class SocketPool: SOCK_STREAM = const(1) SOCK_DGRAM = const(2) AF_INET = const(2) - SOL_SOCKET = const(0xfff) + SOL_SOCKET = const(0xFFF) SO_REUSEADDR = const(0x0004) # implementation specific constants @@ -95,6 +95,7 @@ def __init__( self._type = type self._buffer = b"" self._socknum = socknum if socknum is not None else self._interface.get_socket() + self._bound = () self.settimeout(0) def __enter__(self): @@ -156,12 +157,12 @@ def recv_into(self, buffer, nbytes: int = 0): if not 0 <= nbytes <= len(buffer): raise ValueError("nbytes must be 0 to len(buffer)") - last_read_time = time.monotonic() + last_read_time = time.monotonic_ns() num_to_read = len(buffer) if nbytes == 0 else nbytes num_read = 0 while num_to_read > 0: # we might have read socket data into the self._buffer with: - # esp32spi_wsgiserver: socket_readline + # adafruit_wsgi.esp32spi_wsgiserver: socket_readline if len(self._buffer) > 0: bytes_to_read = min(num_to_read, len(self._buffer)) buffer[num_read : num_read + bytes_to_read] = self._buffer[:bytes_to_read] @@ -173,8 +174,10 @@ def recv_into(self, buffer, nbytes: int = 0): num_avail = self._available() if num_avail > 0: - last_read_time = time.monotonic() - bytes_read = self._interface.socket_read(self._socknum, min(num_to_read, num_avail)) + last_read_time = time.monotonic_ns() + bytes_read = self._interface.socket_read( + self._socknum, min(num_to_read, num_avail) + ) buffer[num_read : num_read + len(bytes_read)] = bytes_read num_read += len(bytes_read) num_to_read -= len(bytes_read) @@ -182,15 +185,27 @@ def recv_into(self, buffer, nbytes: int = 0): # We got a message, but there are no more bytes to read, so we can stop. break # No bytes yet, or more bytes requested. - if self._timeout > 0 and time.monotonic() - last_read_time > self._timeout: + + if self._timeout == 0: # if in non-blocking mode, stop now. + break + + # Time out if there's a positive timeout set. + delta = (time.monotonic_ns() - last_read_time) // 1_000_000 + if self._timeout > 0 and delta > self._timeout: raise OSError(errno.ETIMEDOUT) return num_read def settimeout(self, value): - """Set the read timeout for sockets. - If value is 0 socket reads will block until a message is available. + """Set the read timeout for sockets in seconds. + ``0`` means non-blocking. ``None`` means block indefinitely. """ - self._timeout = value + if value is None: + self._timeout = -1 + else: + if value < 0: + raise ValueError("Timeout cannot be a negative number") + # internally in milliseconds as an int + self._timeout = int(value * 1000) def _available(self): """Returns how many bytes of data are available to be read (up to the MAX_PACKET length)""" @@ -230,35 +245,41 @@ def close(self): def setsockopt(self, *opts, **kwopts): """Dummy call for compatibility.""" - # FIXME - pass - def listen(self, backlog): - """Dummy call for compatibility.""" - # FIXME - # probably nothing to do actually - # maybe check that we have called bind or something ? - pass + def setblocking(self, flag: bool): + """Set the blocking behaviour of this socket. + :param bool flag: False means non-blocking, True means block indefinitely. + """ + if flag: + self.settimeout(None) + else: + self.settimeout(0) - def setblocking(self, blocking): - """Dummy call for compatibility.""" - # FIXME - # is this settimeout(0) ? (if True) or something else ? - pass + def bind(self, address: Tuple[str, int]): + """Bind a socket to an address""" + self._bound = address - def bind(self, host_port): - host, port = host_port + def listen(self, backlog: int): # pylint: disable=unused-argument + """Set socket to listen for incoming connections. + :param int backlog: length of backlog queue for waiting connections (ignored) + """ + if not self._bound: + self._bound = (self._interface.ip_address, 80) + port = self._bound[1] self._interface.start_server(port, self._socknum) - print(f"Binding to {self._socknum}") def accept(self): + """Accept a connection on a listening socket of type SOCK_STREAM, + creating a new socket of type SOCK_STREAM. Returns a tuple of + (new_socket, remote_address) + """ client_sock_num = self._interface.socket_available(self._socknum) if client_sock_num != SocketPool.NO_SOCKET_AVAIL: sock = Socket(self._socket_pool, socknum=client_sock_num) # get remote information (addr and port) remote = self._interface.get_remote_data(client_sock_num) - IP_ADDRESS = "{}.{}.{}.{}".format(*remote['ip_addr']) - PORT = remote['port'] - client_address = (IP_ADDRESS, PORT) + ip_address = "{}.{}.{}.{}".format(*remote["ip_addr"]) + port = remote["port"] + client_address = (ip_address, port) return sock, client_address raise OSError(errno.ECONNRESET) From 07d74d492b02dac56c5731cfc250225e106b7763 Mon Sep 17 00:00:00 2001 From: Neradoc Date: Wed, 22 Jan 2025 03:15:04 +0100 Subject: [PATCH 3/5] docstring --- adafruit_esp32spi/adafruit_esp32spi_socketpool.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py index 3bab399..7e72513 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py @@ -77,7 +77,13 @@ def socket( class Socket: """A simplified implementation of the Python 'socket' class, for connecting - through an interface to a remote device""" + through an interface to a remote device. Has properties specific to the + implementation. + + :param SocketPool socket_pool: The underlying socket pool. + :param Optional[int] socknum: Allows wrapping a Socket instance around a socket + number returned by the nina firmware. Used internally. + """ def __init__( self, From 2c0221bd954ce41f99850b4222b0a3ecf9e5fb73 Mon Sep 17 00:00:00 2001 From: Neradoc Date: Wed, 26 Feb 2025 01:26:37 +0100 Subject: [PATCH 4/5] arrange socket methods in alphabetical order --- .../adafruit_esp32spi_socketpool.py | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py index 7e72513..1d31eb3 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py @@ -249,17 +249,21 @@ def close(self): # WORK IN PROGRESS #################################################################### - def setsockopt(self, *opts, **kwopts): - """Dummy call for compatibility.""" - - def setblocking(self, flag: bool): - """Set the blocking behaviour of this socket. - :param bool flag: False means non-blocking, True means block indefinitely. + def accept(self): + """Accept a connection on a listening socket of type SOCK_STREAM, + creating a new socket of type SOCK_STREAM. Returns a tuple of + (new_socket, remote_address) """ - if flag: - self.settimeout(None) - else: - self.settimeout(0) + client_sock_num = self._interface.socket_available(self._socknum) + if client_sock_num != SocketPool.NO_SOCKET_AVAIL: + sock = Socket(self._socket_pool, socknum=client_sock_num) + # get remote information (addr and port) + remote = self._interface.get_remote_data(client_sock_num) + ip_address = "{}.{}.{}.{}".format(*remote["ip_addr"]) + port = remote["port"] + client_address = (ip_address, port) + return sock, client_address + raise OSError(errno.ECONNRESET) def bind(self, address: Tuple[str, int]): """Bind a socket to an address""" @@ -274,18 +278,14 @@ def listen(self, backlog: int): # pylint: disable=unused-argument port = self._bound[1] self._interface.start_server(port, self._socknum) - def accept(self): - """Accept a connection on a listening socket of type SOCK_STREAM, - creating a new socket of type SOCK_STREAM. Returns a tuple of - (new_socket, remote_address) + def setblocking(self, flag: bool): + """Set the blocking behaviour of this socket. + :param bool flag: False means non-blocking, True means block indefinitely. """ - client_sock_num = self._interface.socket_available(self._socknum) - if client_sock_num != SocketPool.NO_SOCKET_AVAIL: - sock = Socket(self._socket_pool, socknum=client_sock_num) - # get remote information (addr and port) - remote = self._interface.get_remote_data(client_sock_num) - ip_address = "{}.{}.{}.{}".format(*remote["ip_addr"]) - port = remote["port"] - client_address = (ip_address, port) - return sock, client_address - raise OSError(errno.ECONNRESET) + if flag: + self.settimeout(None) + else: + self.settimeout(0) + + def setsockopt(self, *opts, **kwopts): + """Dummy call for compatibility.""" From 91b5d0e744ee6584801de2a0c2b59aeefdb79f13 Mon Sep 17 00:00:00 2001 From: Neradoc Date: Tue, 24 Jun 2025 18:57:26 +0200 Subject: [PATCH 5/5] ruffing ruff --- adafruit_esp32spi/adafruit_esp32spi_socketpool.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py index 1d31eb3..442fd8b 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py @@ -181,9 +181,7 @@ def recv_into(self, buffer, nbytes: int = 0): num_avail = self._available() if num_avail > 0: last_read_time = time.monotonic_ns() - bytes_read = self._interface.socket_read( - self._socknum, min(num_to_read, num_avail) - ) + bytes_read = self._interface.socket_read(self._socknum, min(num_to_read, num_avail)) buffer[num_read : num_read + len(bytes_read)] = bytes_read num_read += len(bytes_read) num_to_read -= len(bytes_read) @@ -265,7 +263,7 @@ def accept(self): return sock, client_address raise OSError(errno.ECONNRESET) - def bind(self, address: Tuple[str, int]): + def bind(self, address: tuple[str, int]): """Bind a socket to an address""" self._bound = address