diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index bf6a122..b4c3408 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -45,11 +45,13 @@ from random import randint import time +import gc from micropython import const from adafruit_bus_device.spi_device import SPIDevice import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as dhcp import adafruit_wiznet5k.adafruit_wiznet5k_dns as dns +from adafruit_wiznet5k.adafruit_wiznet5k_debug import debug_msg # Wiznet5k Registers _REG_MR = const(0x0000) # Mode @@ -144,6 +146,8 @@ class WIZNET5K: # pylint: disable=too-many-public-methods, too-many-instance-at _UDP_MODE = const(0x02) _TLS_MODE = const(0x03) # This is NOT currently implemented + _sockets_reserved = [] + # pylint: disable=too-many-arguments def __init__( self, @@ -189,6 +193,13 @@ def __init__( self._ch_base_msb = 0 if self._w5xxx_init() != 1: raise RuntimeError("Failed to initialize WIZnet module.") + if self._chip_type == "w5100s": + WIZNET5K._sockets_reserved = [False] * (_W5100_MAX_SOCK_NUM - 1) + elif self._chip_type == "w5500": + WIZNET5K._sockets_reserved = [False] * (_W5200_W5500_MAX_SOCK_NUM - 1) + else: + raise RuntimeError("Unrecognized chip type.") + # Set MAC address self.mac_address = mac self.src_port = 0 @@ -722,26 +733,61 @@ def _send_socket_cmd(self, socket: int, cmd: int) -> None: if self._debug: print("waiting for sncr to clear...") - def get_socket(self) -> int: - """Request, allocate and return a socket from the W5k chip. + def get_socket(self, *, reserve_socket=False) -> int: + """ + Request, allocate and return a socket from the W5k chip. + + Cycle through the sockets to find the first available one. If the called with + reserve_socket=True, update the list of reserved sockets (intended to be used with + socket.socket()). Note that reserved sockets must be released by calling + cancel_reservation() once they are no longer needed. + + If all sockets are reserved, no sockets are available for DNS calls, etc. Therefore, + one socket cannot be reserved. Since socket 0 is the only socket that is capable of + operating in MacRAW mode, it is the non-reservable socket. + + :param bool reserve_socket: Whether to reserve the socket. - Cycle through the sockets to find the first available one, if any. + :returns int: The first available socket. - :return int: The first available socket. Returns 0xFF if no sockets are free. + :raises RuntimeError: If no socket is available. """ - if self._debug: - print("*** Get socket") + debug_msg("*** Get socket.", self._debug) + # Prefer socket zero for none reserved calls as it cannot be reserved. + if not reserve_socket and self.socket_status(0)[0] == SNSR_SOCK_CLOSED: + debug_msg("Allocated socket # 0", self._debug) + return 0 + # Then check the other sockets. - sock = _SOCKET_INVALID - for _sock in range(self.max_sockets): - status = self.socket_status(_sock)[0] - if status == SNSR_SOCK_CLOSED: - sock = _sock - break + # Call garbage collection to encourage socket.__del__() be called to on any + # destroyed instances. Not at all guaranteed to work! + gc.collect() + debug_msg( + "Reserved sockets: {}".format(WIZNET5K._sockets_reserved), self._debug + ) - if self._debug: - print("Allocated socket #{}".format(sock)) - return sock + for socket_number, reserved in enumerate(WIZNET5K._sockets_reserved, start=1): + if ( + not reserved + and self.socket_status(socket_number)[0] == SNSR_SOCK_CLOSED + ): + if reserve_socket: + WIZNET5K._sockets_reserved[socket_number - 1] = True + debug_msg( + "Allocated socket # {}.".format(socket_number), + self._debug, + ) + return socket_number + raise RuntimeError("Out of sockets.") + + @staticmethod + def release_socket(socket_number): + """ + Update the socket reservation list when a socket is no longer reserved. + + :param int socket_number: The socket to release. + """ + WIZNET5K._sockets_reserved[socket_number - 1] = False def socket_listen( self, socket_num: int, port: int, conn_mode: int = _SNMR_TCP diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_debug.py b/adafruit_wiznet5k/adafruit_wiznet5k_debug.py new file mode 100644 index 0000000..9fb02c6 --- /dev/null +++ b/adafruit_wiznet5k/adafruit_wiznet5k_debug.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2023 Martin Stephens +# +# SPDX-License-Identifier: MIT + +"""Makes a debug message function available to all modules.""" +try: + from typing import TYPE_CHECKING, Union + + if TYPE_CHECKING: + from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K +except ImportError: + pass + +import gc + + +def debug_msg( + message: Union[Exception, str, bytes, bytearray], debugging: bool +) -> None: + """ + Helper function to print debugging messages. If the message is a bytes type + object, create a hexdump. + + :param Union[Exception, str, bytes, bytearray] message: The message to print. + :param bool debugging: Only print if debugging is True. + """ + if debugging: + if isinstance(message, (bytes, bytearray)): + message = _hexdump(message) + print(message) + del message + gc.collect() + + +def _hexdump(src: bytes): + """ + Create a 16 column hexdump of a bytes object. + + :param bytes src: The bytes object to hexdump. + + :returns str: The hexdump. + """ + result = [] + for i in range(0, len(src), 16): + chunk = src[i : i + 16] + hexa = " ".join(("{:02x}".format(x) for x in chunk)) + text = "".join((chr(x) if 0x20 <= x < 0x7F else "." for x in chunk)) + result.append("{:04x} {:<48} {}".format(i, hexa, text)) + return "\n".join(result) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_socket.py b/adafruit_wiznet5k/adafruit_wiznet5k_socket.py index 2059fa9..96598b3 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_socket.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_socket.py @@ -232,14 +232,18 @@ def __init__( self._timeout = _default_socket_timeout self._listen_port = None - self._socknum = _the_interface.get_socket() + self._socknum = _the_interface.get_socket(reserve_socket=True) if self._socknum == _SOCKET_INVALID: raise RuntimeError("Failed to allocate socket.") + def __del__(self): + _the_interface.release_socket(self._socknum) + def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: + _the_interface.release_socket(self._socknum) if self._sock_type == SOCK_STREAM: self._disconnect() stamp = time.monotonic() @@ -625,8 +629,9 @@ def close(self) -> None: Mark the socket closed. Once that happens, all future operations on the socket object will fail. The remote end will receive no more data. """ - self._socket_closed = True + _the_interface.release_socket(self._socknum) _the_interface.socket_close(self._socknum) + self._socket_closed = True def _available(self) -> int: """