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/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 54fb012..e4cb115 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,18 +176,21 @@ 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. @@ -202,52 +208,28 @@ def set_dhcp(self, hostname=None, response_timeout=3): if self._debug: print("My Link is:", self.link_status) - 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. @@ -256,14 +238,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 @@ -481,7 +461,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) @@ -561,13 +545,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 @@ -588,15 +566,16 @@ 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 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.") @@ -639,11 +618,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 1a42926..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,28 +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 """ - # before making send packet, shoule init _BUFF. - # if not, DHCP sometimes fails, wrong padding, garbage bytes, ... _BUFF[:] = b"\x00" * len(_BUFF) - # OP _BUFF[0] = DHCP_BOOT_REQUEST # HTYPE @@ -155,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 @@ -191,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 @@ -221,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: @@ -253,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 @@ -273,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 @@ -291,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 @@ -323,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, @@ -339,74 +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: + 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: Send discover to {}".format(self.dhcp_server_ip)) + self.send_dhcp_message( + STATE_DHCP_DISCOVER, (time.monotonic() - self._start_time) + ) + self._dhcp_state = STATE_DHCP_DISCOVER + else: if self._debug: - print("* DHCP: Request", xid) + print("* DHCP: Send request to {}".format(self.dhcp_server_ip)) self.send_dhcp_message( - DHCP_REQUEST, ((time.monotonic() - start_time) / 1000) + 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 self._dhcp_state == 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") + + 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 msg_type == 255: - msg_type = 0 - self._dhcp_state = STATE_DHCP_START + while ( + self._dhcp_state != STATE_DHCP_LEASED + and self._dhcp_state != STATE_DHCP_WAIT + ): + self._dhcp_state_machine() - if result != 1 and ( - (time.monotonic() - start_time > self._response_timeout) - ): - break + return self._dhcp_state == STATE_DHCP_LEASED - 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 + 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): """ diff --git a/examples/wiznet5k_wsgiserver_test.py b/examples/wiznet5k_wsgiserver_test.py new file mode 100644 index 0000000..a0ba423 --- /dev/null +++ b/examples/wiznet5k_wsgiserver_test.py @@ -0,0 +1,139 @@ +# 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) + +# 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 bus +spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) + +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 (RuntimeError, OSError): + # Hard coded MAC if there is no 24AA02E48 + 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