From 25f7cd29825398718dc231571ce60c77ee758dd0 Mon Sep 17 00:00:00 2001 From: Patrick Van Oosterwijck Date: Thu, 29 Apr 2021 15:25:03 -0600 Subject: [PATCH 1/5] Implement DHCP lease maintenance with renew / rebind. - Now can use `socket.bind` to set source port to be consistent with CPython socket behavior. - DHCP object is now persistent in `WIZNET5K` because we want to keep it around to automatically renew and rebind. - Added `maintain_dhcp_lease` to `WIZNET5K` to properly maintain the DHCP lease with renew / rebind behavior. - Implement correct DHCP transaction ID behavior so we don't accidentally grab an IP that was supposed to be assigned to another device. - Now keeps track of automatically assigned source ports to prevent duplication. - Renamed `WIZNET5K._src_port` to `WIZNET5K.src_port` because it's externally accessed from `socket.connect`. - Removed setting source port for DNS lookup, moved to `bind` before socket connect. - `WIZNET5K.get_socket` now only returns fully closed sockets. Previous behavior caused problems with stray old data from previous connections confusing new connections when the socket wasn't fully closed yet. - Improved WSGI server in how it deals with socket removal and allocation. - Fixed some bugs and comments. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 83 ++---- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 282 ++++++++++++------ adafruit_wiznet5k/adafruit_wiznet5k_dns.py | 1 + adafruit_wiznet5k/adafruit_wiznet5k_socket.py | 13 +- .../adafruit_wiznet5k_wsgiserver.py | 13 +- 5 files changed, 237 insertions(+), 155 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index cc3063f..55db789 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -121,6 +121,9 @@ # UDP socket struct. UDP_SOCK = {"bytes_remaining": 0, "remote_ip": 0, "remote_port": 0} +# Source ports in use +SRC_PORTS = [0] * W5200_W5500_MAX_SOCK_NUM + class WIZNET5K: # pylint: disable=too-many-public-methods """Interface for WIZNET5K module. @@ -173,69 +176,48 @@ def __init__( assert self._w5100_init() == 1, "Failed to initialize WIZnet module." # Set MAC address self.mac_address = mac - self._src_port = 0 + self.src_port = 0 self._dns = 0 # Set DHCP + self._dhcp_client = None if is_dhcp: ret = self.set_dhcp(hostname, dhcp_timeout) + if ret != 0: + self._dhcp_client = None assert ret == 0, "Failed to configure DHCP Server!" - def set_dhcp(self, hostname=None, response_timeout=3): + def set_dhcp(self, hostname=None, response_timeout=30): """Initializes the DHCP client and attempts to retrieve and set network configuration from the DHCP server. - Returns True if DHCP configured, False otherwise. + Returns 0 if DHCP configured, -1 otherwise. :param str hostname: The desired hostname, with optional {} to fill in MAC. :param int response_timeout: Time to wait for server to return packet, in seconds. """ if self._debug: print("* Initializing DHCP") - self._src_port = 68 # Return IP assigned by DHCP - _dhcp_client = dhcp.DHCP( + self._dhcp_client = dhcp.DHCP( self, self.mac_address, hostname, response_timeout, debug=self._debug ) - ret = _dhcp_client.request_dhcp_lease() + ret = self._dhcp_client.request_dhcp_lease() if ret == 1: - _ip = ( - _dhcp_client.local_ip[0], - _dhcp_client.local_ip[1], - _dhcp_client.local_ip[2], - _dhcp_client.local_ip[3], - ) - - _subnet_mask = ( - _dhcp_client.subnet_mask[0], - _dhcp_client.subnet_mask[1], - _dhcp_client.subnet_mask[2], - _dhcp_client.subnet_mask[3], - ) - - _gw_addr = ( - _dhcp_client.gateway_ip[0], - _dhcp_client.gateway_ip[1], - _dhcp_client.gateway_ip[2], - _dhcp_client.gateway_ip[3], - ) - - self._dns = ( - _dhcp_client.dns_server_ip[0], - _dhcp_client.dns_server_ip[1], - _dhcp_client.dns_server_ip[2], - _dhcp_client.dns_server_ip[3], - ) - self.ifconfig = (_ip, _subnet_mask, _gw_addr, self._dns) if self._debug: + _ifconfig = self.ifconfig print("* Found DHCP Server:") print( "IP: {}\nSubnet Mask: {}\nGW Addr: {}\nDNS Server: {}".format( - _ip, _subnet_mask, _gw_addr, self._dns + *_ifconfig ) ) - self._src_port = 0 return 0 return -1 + def maintain_dhcp_lease(self): + """Maintain DHCP lease""" + if self._dhcp_client is not None: + self._dhcp_client.maintain_dhcp_lease() + def get_host_by_name(self, hostname): """Convert a hostname to a packed 4-byte IP Address. Returns a 4 bytearray. @@ -244,14 +226,12 @@ def get_host_by_name(self, hostname): print("* Get host by name") if isinstance(hostname, str): hostname = bytes(hostname, "utf-8") - self._src_port = int(time.monotonic()) & 0xFFFF # Return IP assigned by DHCP _dns_client = dns.DNS(self, self._dns, debug=self._debug) ret = _dns_client.gethostbyname(hostname) if self._debug: print("* Resolved IP: ", ret) assert ret != -1, "Failed to resolve hostname!" - self._src_port = 0 return ret @property @@ -469,7 +449,11 @@ def socket_available(self, socket_num, sock_type=SNMR_TCP): :param int sock_type: Socket type, defaults to TCP. """ if self._debug: - print("* socket_available called with protocol", sock_type) + print( + "* socket_available called on socket {}, protocol {}".format( + socket_num, sock_type + ) + ) assert socket_num <= self.max_sockets, "Provided socket exceeds max_sockets." res = self._get_rx_rcv_size(socket_num) @@ -549,13 +533,7 @@ def get_socket(self): sock = SOCKET_INVALID for _sock in range(self.max_sockets): status = self.socket_status(_sock)[0] - if status in ( - SNSR_SOCK_CLOSED, - SNSR_SOCK_TIME_WAIT, - SNSR_SOCK_FIN_WAIT, - SNSR_SOCK_CLOSE_WAIT, - SNSR_SOCK_CLOSING, - ): + if status == SNSR_SOCK_CLOSED: sock = _sock break @@ -576,8 +554,9 @@ def socket_listen(self, socket_num, port): ) ) # Initialize a socket and set the mode - self._src_port = port + self.src_port = port res = self.socket_open(socket_num, conn_mode=SNMR_TCP) + self.src_port = 0 if res == 1: raise RuntimeError("Failed to initalize the socket.") # Send listen command @@ -627,11 +606,15 @@ def socket_open(self, socket_num, conn_mode=SNMR_TCP): self._write_snmr(socket_num, conn_mode) self._write_snir(socket_num, 0xFF) - if self._src_port > 0: + if self.src_port > 0: # write to socket source port - self._write_sock_port(socket_num, self._src_port) + self._write_sock_port(socket_num, self.src_port) else: - self._write_sock_port(socket_num, randint(49152, 65535)) + s_port = randint(49152, 65535) + while s_port in SRC_PORTS: + s_port = randint(49152, 65535) + self._write_sock_port(socket_num, s_port) + SRC_PORTS[socket_num] = s_port # open socket self._write_sncr(socket_num, CMD_SOCK_OPEN) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 7faef3d..b5768fb 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2009 Jordan Terell (blog.jordanterrell.com) # SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries +# SPDX-FileCopyrightText: 2021 Patrick Van Oosterwijck @ Silicognition LLC # # SPDX-License-Identifier: MIT @@ -14,7 +15,7 @@ """ import gc import time -from random import randrange +from random import randint from micropython import const import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket from adafruit_wiznet5k.adafruit_wiznet5k_socket import htonl, htons @@ -27,6 +28,11 @@ STATE_DHCP_LEASED = const(0x03) STATE_DHCP_REREQUEST = const(0x04) STATE_DHCP_RELEASE = const(0x05) +STATE_DHCP_WAIT = const(0x06) +STATE_DHCP_DISCONN = const(0x07) + +# DHCP wait time between attempts +DHCP_WAIT_TIME = const(60) # DHCP Message Types DHCP_DISCOVER = const(1) @@ -55,7 +61,7 @@ DHCP_SERVER_PORT = const(67) # DHCP Lease Time, in seconds DEFAULT_LEASE_TIME = const(900) -BROADCAST_SERVER_ADDR = "255.255.255.255" +BROADCAST_SERVER_ADDR = (255, 255, 255, 255) # DHCP Response Options MSG_TYPE = 53 @@ -68,8 +74,8 @@ LEASE_TIME = 51 OPT_END = 255 - -_BUFF = bytearray(317) +# Packet buffer +_BUFF = bytearray(318) class DHCP: @@ -90,18 +96,19 @@ def __init__( self._response_timeout = response_timeout self._mac_address = mac_address - # Initalize a new UDP socket for DHCP + # Set socket interface socket.set_interface(eth) - self._sock = socket.socket(type=socket.SOCK_DGRAM) - self._sock.settimeout(response_timeout) + self._eth = eth + self._sock = None # DHCP state machine self._dhcp_state = STATE_DHCP_START self._initial_xid = 0 self._transaction_id = 0 + self._start_time = 0 # DHCP server configuration - self.dhcp_server_ip = 0 + self.dhcp_server_ip = BROADCAST_SERVER_ADDR self.local_ip = 0 self.gateway_ip = 0 self.subnet_mask = 0 @@ -109,24 +116,29 @@ def __init__( # Lease configuration self._lease_time = 0 - self._last_check_lease_ms = 0 + self._last_lease_time = 0 self._renew_in_sec = 0 self._rebind_in_sec = 0 self._t1 = 0 self._t2 = 0 + # Select an initial transaction id + self._transaction_id = randint(1, 0x7FFFFFFF) + # Host name mac_string = "".join("{:02X}".format(o) for o in mac_address) self._hostname = bytes( (hostname or "WIZnet{}").split(".")[0].format(mac_string)[:42], "utf-8" ) - def send_dhcp_message(self, state, time_elapsed): + # pylint: disable=too-many-statements + def send_dhcp_message(self, state, time_elapsed, renew=False): """Assemble and send a DHCP message packet to a socket. :param int state: DHCP Message state. - :param float time_elapsed: Number of seconds elapsed since renewal. - + :param float time_elapsed: Number of seconds elapsed since DHCP process started + :param bool renew: Set True for renew and rebind """ + _BUFF[:] = b"\x00" * len(_BUFF) # OP _BUFF[0] = DHCP_BOOT_REQUEST # HTYPE @@ -151,8 +163,11 @@ def send_dhcp_message(self, state, time_elapsed): _BUFF[10] = flags[1] _BUFF[11] = flags[0] - # NOTE: Skipping cidaddr/yiaddr/siaddr/giaddr + # NOTE: Skipping ciaddr/yiaddr/siaddr/giaddr # as they're already set to 0.0.0.0 + # Except when renewing, then fill in ciaddr + if renew: + _BUFF[12:15] = bytes(self.local_ip) # chaddr _BUFF[28:34] = self._mac_address @@ -187,16 +202,15 @@ def send_dhcp_message(self, state, time_elapsed): _BUFF[253] = hostname_len _BUFF[254:after_hostname] = self._hostname - if state == DHCP_REQUEST: + if state == DHCP_REQUEST and not renew: # Set the parsed local IP addr _BUFF[after_hostname] = 50 _BUFF[after_hostname + 1] = 0x04 - - _BUFF[after_hostname + 2 : after_hostname + 6] = self.local_ip + _BUFF[after_hostname + 2 : after_hostname + 6] = bytes(self.local_ip) # Set the parsed dhcp server ip addr _BUFF[after_hostname + 6] = 54 _BUFF[after_hostname + 7] = 0x04 - _BUFF[after_hostname + 8 : after_hostname + 12] = self.dhcp_server_ip + _BUFF[after_hostname + 8 : after_hostname + 12] = bytes(self.dhcp_server_ip) _BUFF[after_hostname + 12] = 55 _BUFF[after_hostname + 13] = 0x06 @@ -217,21 +231,11 @@ def send_dhcp_message(self, state, time_elapsed): # Send DHCP packet self._sock.send(_BUFF) - def parse_dhcp_response( - self, response_timeout - ): # pylint: disable=too-many-branches, too-many-statements + # pylint: disable=too-many-branches, too-many-statements + def parse_dhcp_response(self): """Parse DHCP response from DHCP server. Returns DHCP packet type. - - :param int response_timeout: Time to wait for server to return packet, in seconds. """ - start_time = time.monotonic() - packet_sz = self._sock.available() - while packet_sz <= 0: - packet_sz = self._sock.available() - if (time.monotonic() - start_time) > response_timeout: - return (255, 0) - time.sleep(0.05) # store packet in buffer _BUFF = self._sock.recv() if self._debug: @@ -249,7 +253,7 @@ def parse_dhcp_response( print("f") return 0, 0 - self.local_ip = _BUFF[16:20] + self.local_ip = tuple(_BUFF[16:20]) if _BUFF[28:34] == 0: return 0, 0 @@ -269,13 +273,13 @@ def parse_dhcp_response( ptr += 1 opt_len = _BUFF[ptr] ptr += 1 - self.subnet_mask = _BUFF[ptr : ptr + opt_len] + self.subnet_mask = tuple(_BUFF[ptr : ptr + opt_len]) ptr += opt_len elif _BUFF[ptr] == DHCP_SERVER_ID: ptr += 1 opt_len = _BUFF[ptr] ptr += 1 - self.dhcp_server_ip = _BUFF[ptr : ptr + opt_len] + self.dhcp_server_ip = tuple(_BUFF[ptr : ptr + opt_len]) ptr += opt_len elif _BUFF[ptr] == LEASE_TIME: ptr += 1 @@ -287,13 +291,13 @@ def parse_dhcp_response( ptr += 1 opt_len = _BUFF[ptr] ptr += 1 - self.gateway_ip = _BUFF[ptr : ptr + opt_len] + self.gateway_ip = tuple(_BUFF[ptr : ptr + opt_len]) ptr += opt_len elif _BUFF[ptr] == DNS_SERVERS: ptr += 1 opt_len = _BUFF[ptr] ptr += 1 - self.dns_server_ip = _BUFF[ptr : ptr + 4] + self.dns_server_ip = tuple(_BUFF[ptr : ptr + 4]) ptr += opt_len # still increment even though we only read 1 addr. elif _BUFF[ptr] == T1_VAL: ptr += 1 @@ -319,13 +323,14 @@ def parse_dhcp_response( if self._debug: print( - "Msg Type: {}\nSubnet Mask: {}\nDHCP Server ID:{}\nDNS Server IP:{}\ - \nGateway IP:{}\nT1:{}\nT2:{}\nLease Time:{}".format( + "Msg Type: {}\nSubnet Mask: {}\nDHCP Server IP: {}\nDNS Server IP: {}\ + \nGateway IP: {}\nLocal IP: {}\nT1: {}\nT2: {}\nLease Time: {}".format( msg_type, self.subnet_mask, self.dhcp_server_ip, self.dns_server_ip, self.gateway_ip, + self.local_ip, self._t1, self._t2, self._lease_time, @@ -335,77 +340,158 @@ def parse_dhcp_response( gc.collect() return msg_type, xid - def request_dhcp_lease( - self, - ): # pylint: disable=too-many-branches, too-many-statements - """Request to renew or acquire a DHCP lease.""" - # select an initial transaction id - self._transaction_id = randrange(1, 2000) - - result = 0 - msg_type = 0 - start_time = time.monotonic() - - while self._dhcp_state != STATE_DHCP_LEASED: - if self._dhcp_state == STATE_DHCP_START: - self._transaction_id += 1 - self._sock.connect(((BROADCAST_SERVER_ADDR), DHCP_SERVER_PORT)) - if self._debug: - print("* DHCP: Discover") - self.send_dhcp_message( - STATE_DHCP_DISCOVER, ((time.monotonic() - start_time) / 1000) - ) - self._dhcp_state = STATE_DHCP_DISCOVER - elif self._dhcp_state == STATE_DHCP_DISCOVER: + # pylint: disable=too-many-branches, too-many-statements + def _dhcp_state_machine(self): + """DHCP state machine without wait loops to enable cooperative multi tasking + This state machine is used both by the initial blocking lease request and + the non-blocking DHCP maintenance function""" + if self._eth.link_status: + if self._dhcp_state == STATE_DHCP_DISCONN: + self._dhcp_state = STATE_DHCP_START + else: + if self._dhcp_state != STATE_DHCP_DISCONN: + self._dhcp_state = STATE_DHCP_DISCONN + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + self._last_lease_time = 0 + reset_ip = (0, 0, 0, 0) + self._eth.ifconfig = (reset_ip, reset_ip, reset_ip, reset_ip) + if self._sock is not None: + self._sock.close() + self._sock = None + + if self._dhcp_state == STATE_DHCP_START: + self._start_time = time.monotonic() + self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF + try: + self._sock = socket.socket(type=socket.SOCK_DGRAM) + except RuntimeError: if self._debug: - print("* DHCP: Parsing OFFER") - msg_type, xid = self.parse_dhcp_response(self._response_timeout) - if msg_type == DHCP_OFFER: - # use the _transaction_id the offer returned, - # rather than the current one - self._transaction_id = self._transaction_id.from_bytes(xid, "l") + print("* DHCP: Failed to allocate socket") + self._dhcp_state = STATE_DHCP_WAIT + else: + self._sock.settimeout(self._response_timeout) + self._sock.bind((None, 68)) + self._sock.connect((self.dhcp_server_ip, DHCP_SERVER_PORT)) + if self._last_lease_time == 0 or time.monotonic() > ( + self._last_lease_time + self._lease_time + ): if self._debug: - print("* DHCP: Request") + print("* DHCP: Send discover to {}".format(self.dhcp_server_ip)) self.send_dhcp_message( - DHCP_REQUEST, ((time.monotonic() - start_time) / 1000) + STATE_DHCP_DISCOVER, (time.monotonic() - self._start_time) + ) + self._dhcp_state = STATE_DHCP_DISCOVER + else: + if self._debug: + print("* DHCP: Send request to {}".format(self.dhcp_server_ip)) + self.send_dhcp_message( + DHCP_REQUEST, (time.monotonic() - self._start_time), True ) self._dhcp_state = STATE_DHCP_REQUEST + + elif self._dhcp_state == STATE_DHCP_DISCOVER: + if self._sock.available(): + if self._debug: + print("* DHCP: Parsing OFFER") + msg_type, xid = self.parse_dhcp_response() + if msg_type == DHCP_OFFER: + # Check if transaction ID matches, otherwise it may be an offer + # for another device + if htonl(self._transaction_id) == int.from_bytes(xid, "l"): + if self._debug: + print( + "* DHCP: Send request to {}".format(self.dhcp_server_ip) + ) + self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF + self.send_dhcp_message( + DHCP_REQUEST, (time.monotonic() - self._start_time) + ) + self._dhcp_state = STATE_DHCP_REQUEST + else: + if self._debug: + print("* DHCP: Received OFFER with non-matching xid") else: - print("* Received DHCP Message is not OFFER") - elif STATE_DHCP_REQUEST: + if self._debug: + print("* DHCP: Received DHCP Message is not OFFER") + + elif self._dhcp_state == STATE_DHCP_REQUEST: + if self._sock.available(): if self._debug: print("* DHCP: Parsing ACK") - msg_type, xid = self.parse_dhcp_response(self._response_timeout) - if msg_type == DHCP_ACK: - self._dhcp_state = STATE_DHCP_LEASED - result = 1 - if self._lease_time == 0: - self._lease_time = DEFAULT_LEASE_TIME - if self._t1 == 0: - # T1 is 50% of _lease_time - self._t1 = self._lease_time >> 1 - if self._t2 == 0: - # T2 is 87.5% of _lease_time - self._t2 = self._lease_time - (self._lease_time >> 3) - self._renew_in_sec = self._t1 - self._rebind_in_sec = self._t2 - elif msg_type == DHCP_NAK: - self._dhcp_state = STATE_DHCP_START + msg_type, xid = self.parse_dhcp_response() + # Check if transaction ID matches, otherwise it may be + # for another device + if htonl(self._transaction_id) == int.from_bytes(xid, "l"): + if msg_type == DHCP_ACK: + if self._debug: + print("* DHCP: Successful lease") + self._sock.close() + self._sock = None + self._dhcp_state = STATE_DHCP_LEASED + self._last_lease_time = self._start_time + if self._lease_time == 0: + self._lease_time = DEFAULT_LEASE_TIME + if self._t1 == 0: + # T1 is 50% of _lease_time + self._t1 = self._lease_time >> 1 + if self._t2 == 0: + # T2 is 87.5% of _lease_time + self._t2 = self._lease_time - (self._lease_time >> 3) + self._renew_in_sec = self._t1 + self._rebind_in_sec = self._t2 + self._eth.ifconfig = ( + self.local_ip, + self.subnet_mask, + self.gateway_ip, + self.dns_server_ip, + ) + gc.collect() + else: + if self._debug: + print("* DHCP: Received DHCP Message is not ACK") else: - print("* Received DHCP Message is not OFFER") + if self._debug: + print("* DHCP: Received non-matching xid") - if msg_type == 255: - msg_type = 0 - self._dhcp_state = STATE_DHCP_START + elif self._dhcp_state == STATE_DHCP_WAIT: + if time.monotonic() > (self._start_time + DHCP_WAIT_TIME): + if self._debug: + print("* DHCP: Begin retry") + self._dhcp_state = STATE_DHCP_START + if time.monotonic() > (self._last_lease_time + self._rebind_in_sec): + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + if time.monotonic() > (self._last_lease_time + self._lease_time): + reset_ip = (0, 0, 0, 0) + self._eth.ifconfig = (reset_ip, reset_ip, reset_ip, reset_ip) + + elif self._dhcp_state == STATE_DHCP_LEASED: + if time.monotonic() > (self._last_lease_time + self._renew_in_sec): + self._dhcp_state = STATE_DHCP_START + if self._debug: + print("* DHCP: Time to renew lease") + + if ( + self._dhcp_state == STATE_DHCP_DISCOVER + or self._dhcp_state == STATE_DHCP_REQUEST + ) and time.monotonic() > (self._start_time + self._response_timeout): + self._dhcp_state = STATE_DHCP_WAIT + if self._sock is not None: + self._sock.close() + self._sock = None + + def request_dhcp_lease(self): + """Request to renew or acquire a DHCP lease.""" + if self._dhcp_state == STATE_DHCP_LEASED or self._dhcp_state == STATE_DHCP_WAIT: + self._dhcp_state = STATE_DHCP_START - if result != 1 and ( - (time.monotonic() - start_time > self._response_timeout) - ): - break + while ( + self._dhcp_state != STATE_DHCP_LEASED + and self._dhcp_state != STATE_DHCP_WAIT + ): + self._dhcp_state_machine() - self._transaction_id += 1 - self._last_check_lease_ms = time.monotonic() - # close the socket, we're done with it - self._sock.close() - gc.collect() - return result + return self._dhcp_state == STATE_DHCP_LEASED + + def maintain_dhcp_lease(self): + """Maintain DHCP lease""" + self._dhcp_state_machine() diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dns.py b/adafruit_wiznet5k/adafruit_wiznet5k_dns.py index d72b5d1..84a8791 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dns.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dns.py @@ -70,6 +70,7 @@ def gethostbyname(self, hostname): self._build_dns_question() # Send DNS request packet + self._sock.bind((None, DNS_PORT)) self._sock.connect((self._dns_server, DNS_PORT)) if self._debug: print("* DNS: Sending request packet...") diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_socket.py b/adafruit_wiznet5k/adafruit_wiznet5k_socket.py index 9be36cf..3b98267 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_socket.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_socket.py @@ -233,9 +233,13 @@ def connect(self, address, conntype=None): host = tuple(map(int, host.split("."))) except ValueError: host = _the_interface.get_host_by_name(host) - if not _the_interface.socket_connect( + if self._listen_port is not None: + _the_interface.src_port = self._listen_port + result = _the_interface.socket_connect( self.socknum, host, port, conn_mode=self._sock_type - ): + ) + _the_interface.src_port = 0 + if not result: raise RuntimeError("Failed to connect to host", host) self._buffer = b"" @@ -261,7 +265,9 @@ def recv(self, bufsize=0, flags=0): # pylint: disable=too-many-branches :param int bufsize: Maximum number of bytes to receive. :param int flags: ignored, present for compatibility. """ - # print("Socket read", bufsize) + if self.status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED: + return b"" + if bufsize == 0: # read everything on the socket while True: @@ -285,7 +291,6 @@ def recv(self, bufsize=0, flags=0): # pylint: disable=too-many-branches to_read = bufsize - len(self._buffer) received = [] while to_read > 0: - # print("Bytes to read:", to_read) avail = self.available() if avail: stamp = time.monotonic() diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py b/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py index 5104c0f..738c2b6 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py @@ -31,6 +31,7 @@ import io import gc from micropython import const +import adafruit_wiznet5k as wiznet5k import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket _the_interface = None # pylint: disable=invalid-name @@ -84,19 +85,25 @@ def update_poll(self): check for new incoming client requests. When a request comes in, the application callable will be invoked. """ - add_sock = [] for sock in self._client_sock: if sock.available(): environ = self._get_environ(sock) result = self.application(environ, self._start_response) self.finish_response(result, sock) self._client_sock.remove(sock) + break + for sock in self._client_sock: + if sock.status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED: + self._client_sock.remove(sock) + for _ in range(len(self._client_sock), MAX_SOCK_NUM): + try: new_sock = socket.socket() new_sock.settimeout(self._timeout) new_sock.bind((None, self.port)) new_sock.listen() - add_sock.append(new_sock) - self._client_sock.extend(add_sock) + self._client_sock.append(new_sock) + except RuntimeError: + pass def finish_response(self, result, client): """ From 57ca18b0869326b639552d9a94e82e54eea43b7a Mon Sep 17 00:00:00 2001 From: Patrick Van Oosterwijck Date: Sun, 2 May 2021 15:16:50 -0600 Subject: [PATCH 2/5] Fix getting stuck in loop in socket_listen If `socket_listen` was called, and a connection was made right away, it was possible that the state of the socket would change to `SNSR_SOCK_ESTABLISHED` before the check ever saw it enter `SNSR_SOCK_LISTEN`, and the code would be stuck in an endless loop. This fixes the issue by accepting both `SNSR_SOCK_LISTEN` and `SNSR_SOCK_ESTABLISHED` as valid states to continue. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 55db789..cba2beb 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -563,7 +563,7 @@ def socket_listen(self, socket_num, port): self._send_socket_cmd(socket_num, CMD_SOCK_LISTEN) # Wait until ready status = [SNSR_SOCK_CLOSED] - while status[0] != SNSR_SOCK_LISTEN: + while status[0] not in (SNSR_SOCK_LISTEN, SNSR_SOCK_ESTABLISHED): status = self._read_snsr(socket_num) if status[0] == SNSR_SOCK_CLOSED: raise RuntimeError("Listening socket closed.") From 8d59ef3822b04656183f1d099462de9f38c83ee0 Mon Sep 17 00:00:00 2001 From: Patrick Van Oosterwijck Date: Tue, 4 May 2021 11:47:54 -0600 Subject: [PATCH 3/5] Add example code README example now shows `maintain_dhcp_lease` function and a full detailed example showing other features was added to the `examples` directory. --- README.rst | 15 +-- examples/wiznet5k_wsgiserver_test.py | 133 +++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 examples/wiznet5k_wsgiserver_test.py diff --git a/README.rst b/README.rst index 75e8727..b3950a1 100644 --- a/README.rst +++ b/README.rst @@ -126,7 +126,7 @@ This example demonstrates a simple web server that allows setting the Neopixel c cs = digitalio.DigitalInOut(board.D10) spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) - # Initialize ethernet interface with DHCP and the MAC we have from the 24AA02E48 + # Initialize Ethernet interface with DHCP eth = WIZNET5K(spi_bus, cs) # Here we create our application, registering the @@ -134,21 +134,20 @@ This example demonstrates a simple web server that allows setting the Neopixel c web_app = WSGIApp() - @web_app.route("/led///") def led_on(request, r, g, b): - print("led handler") + print("LED handler") led.fill((int(r), int(g), int(b))) - return ("200 OK", [], ["led set!"]) + return ("200 OK", [], ["LED set!"]) @web_app.route("/") def root(request): - print("root handler") - return ("200 OK", [], ["root document"]) + print("Root handler") + return ("200 OK", [], ["Root document"]) @web_app.route("/large") def large(request): - print("large handler") + print("Large pattern handler") return ("200 OK", [], ["*-.-" * 2000]) @@ -163,6 +162,8 @@ This example demonstrates a simple web server that allows setting the Neopixel c while True: # Our main loop where we have the server poll for incoming requests wsgiServer.update_poll() + # Maintain DHCP lease + eth.maintain_dhcp_lease() # Could do any other background tasks here, like reading sensors Contributing diff --git a/examples/wiznet5k_wsgiserver_test.py b/examples/wiznet5k_wsgiserver_test.py new file mode 100644 index 0000000..b3fd0f6 --- /dev/null +++ b/examples/wiznet5k_wsgiserver_test.py @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: 2021 Patrick Van Oosterwijck @ Silicognition LLC +# +# SPDX-License-Identifier: MIT +# +# This demo was tested with the PoE-FeatherWing, which contains a 24AA02E48 +# chip to provide a globally unique MAC address, but can also work without +# this chip for testing purposes by using a hard coded MAC. +# +# It also contains a `get_static_file` function that demonstrates how to +# use a generator to serve large static files without using up too much +# memory. To avoid having to put extra files in the repo, it just serves +# `code.py` which isn't very large, but to properly test it, adjust the code +# to serve an image of several 100 kB to see how it works. +# +# There's also an endpoint that demonstrates that `requests` can be used to +# get data from another socket and serve it. +# + +import board +import busio +import digitalio +import neopixel +import time + +import adafruit_requests as requests +from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K +import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket +import adafruit_wiznet5k.adafruit_wiznet5k_wsgiserver as server +from adafruit_wsgi.wsgi_app import WSGIApp + + +print("Wiznet5k Web Server Test") + + +def get_mac(i2c): + "Read MAC from 24AA02E48 chip and return it" + mac = bytearray(6) + while not i2c.try_lock(): + pass + i2c.writeto(0x50, bytearray((0xFA,))) + i2c.readfrom_into(0x50, mac, start=0, end=6) + i2c.unlock() + return mac + + +def get_static_file(filename): + "Static file generator" + with open(filename, "rb") as f: + bytes = None + while bytes is None or len(bytes) == 2048: + bytes = f.read(2048) + yield bytes + + +# Status LED +led = neopixel.NeoPixel(board.NEOPIXEL, 1) +led.brightness = 0.3 +led[0] = (255, 0, 0) + +# PoE-FeatherWing connections +cs = digitalio.DigitalInOut(board.D10) +spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) +i2c = busio.I2C(board.SCL, board.SDA) + +try: + # Read the MAC from the 24AA02E48 chip + mac = get_mac(i2c) +except OSError: + mac = b"\xFE\xED\xDE\xAD\xBE\xEF" + +# Initialize Ethernet interface with DHCP +eth = WIZNET5K(spi_bus, cs, mac=mac) + +# Initialize a requests object with a socket and ethernet interface +requests.set_socket(socket, eth) + + +# Here we create our application, registering the +# following functions to be called on specific HTTP GET requests routes + +web_app = WSGIApp() + + +@web_app.route("/led///") +def led_on(request, r, g, b): # pylint: disable=unused-argument + print("LED handler") + led.fill((int(r), int(g), int(b))) + return ("200 OK", [], ["LED set!"]) + + +@web_app.route("/") +def root(request): # pylint: disable=unused-argument + print("Root WSGI handler") + return ("200 OK", [], ["Root document"]) + + +@web_app.route("/large") +def large(request): # pylint: disable=unused-argument + print("Large pattern handler") + return ("200 OK", [], ["*-.-" * 2000]) + + +@web_app.route("/code") +def large(request): # pylint: disable=unused-argument + print("Static file code.py handler") + return ("200 OK", [], get_static_file("code.py")) + + +@web_app.route("/btc") +def btc(request): + print("BTC handler") + r = requests.get("http://api.coindesk.com/v1/bpi/currentprice/USD.json") + result = r.text + r.close() + return ("200 OK", [], [result]) + + +# Here we setup our server, passing in our web_app as the application +server.set_interface(eth) +wsgiServer = server.WSGIServer(80, application=web_app) + +print("Open this IP in your browser: ", eth.pretty_ip(eth.ip_address)) + +# Start the server +wsgiServer.start() +led[0] = (0, 0, 255) + +while True: + # Our main loop where we have the server poll for incoming requests + wsgiServer.update_poll() + # Maintain DHCP lease + eth.maintain_dhcp_lease() + # Could do any other background tasks here, like reading sensors From a70ea39ff28c20da2ad7a3fbea6b387ac9053a86 Mon Sep 17 00:00:00 2001 From: Patrick Van Oosterwijck Date: Wed, 5 May 2021 11:10:18 -0600 Subject: [PATCH 4/5] Add Adafruit and Particle Ethernet FeatherWing to demo --- examples/wiznet5k_wsgiserver_test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/wiznet5k_wsgiserver_test.py b/examples/wiznet5k_wsgiserver_test.py index b3fd0f6..ef7eedc 100644 --- a/examples/wiznet5k_wsgiserver_test.py +++ b/examples/wiznet5k_wsgiserver_test.py @@ -57,8 +57,12 @@ def get_static_file(filename): led.brightness = 0.3 led[0] = (255, 0, 0) -# PoE-FeatherWing connections +# Chip Select for PoE-FeatherWing and Adafruit Ethernet FeatherWing cs = digitalio.DigitalInOut(board.D10) +# Chip Select for Particle Ethernet FeatherWing +# cs = digitalio.DigitalInOut(board.D5) + +# Initialize SPI and I2C bus spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) i2c = busio.I2C(board.SCL, board.SDA) @@ -66,6 +70,7 @@ def get_static_file(filename): # Read the MAC from the 24AA02E48 chip mac = get_mac(i2c) except OSError: + # Hard coded MAC if there is no 24AA02E48 mac = b"\xFE\xED\xDE\xAD\xBE\xEF" # Initialize Ethernet interface with DHCP From b04926d955172d9c0bad818803abe47dfb794f13 Mon Sep 17 00:00:00 2001 From: Patrick Van Oosterwijck Date: Wed, 5 May 2021 12:28:42 -0600 Subject: [PATCH 5/5] Use hard coded MAC if no 24AA02E48 on the I2C bus Had to catch exception when creating I2C device for boards that have no I2C pull-ups. --- examples/wiznet5k_wsgiserver_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/wiznet5k_wsgiserver_test.py b/examples/wiznet5k_wsgiserver_test.py index ef7eedc..a0ba423 100644 --- a/examples/wiznet5k_wsgiserver_test.py +++ b/examples/wiznet5k_wsgiserver_test.py @@ -62,14 +62,15 @@ def get_static_file(filename): # Chip Select for Particle Ethernet FeatherWing # cs = digitalio.DigitalInOut(board.D5) -# Initialize SPI and I2C bus +# Initialize SPI bus spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) -i2c = busio.I2C(board.SCL, board.SDA) try: + # Initialize the I2C bus to read the MAC + i2c = busio.I2C(board.SCL, board.SDA) # Read the MAC from the 24AA02E48 chip mac = get_mac(i2c) -except OSError: +except (RuntimeError, OSError): # Hard coded MAC if there is no 24AA02E48 mac = b"\xFE\xED\xDE\xAD\xBE\xEF"