Skip to content

Fix out of sockets problem #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 61 additions & 15 deletions adafruit_wiznet5k/adafruit_wiznet5k.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions adafruit_wiznet5k/adafruit_wiznet5k_debug.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 7 additions & 2 deletions adafruit_wiznet5k/adafruit_wiznet5k_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
"""
Expand Down