From 077c3308242f673862017ec048e3ec6f2a259f4b Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 17 Dec 2022 11:16:17 +0300 Subject: [PATCH 01/80] Refactored message parser and message generator. All tests pass. Began new state machine. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 437 ++++++++---------- ...iserver_test.py => wiznet5k_wsgiserver.py} | 0 tests/dummy_dhcp_data.py | 132 ++++++ tests/test_dhcp.py | 301 ++++++++++++ 4 files changed, 638 insertions(+), 232 deletions(-) rename examples/{wiznet5k_wsgiserver_test.py => wiznet5k_wsgiserver.py} (100%) create mode 100644 tests/dummy_dhcp_data.py create mode 100644 tests/test_dhcp.py diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 3243f77..41e35c5 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -29,7 +29,7 @@ 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 +from adafruit_wiznet5k.adafruit_wiznet5k_socket import htonl # DHCP State Machine @@ -42,6 +42,14 @@ STATE_DHCP_WAIT = const(0x06) STATE_DHCP_DISCONN = const(0x07) +STATE_INIT = const(0x01) +STATE_SELECTING = const(0x02) +STATE_REQUESTING = const(0x03) +STATE_BOUND = const(0x04) +STATE_RENEWING = const(0x05) +STATE_REBINDING = const(0x06) +STATE_RELEASING = const(0x07) + # DHCP wait time between attempts DHCP_WAIT_TIME = const(60) @@ -65,7 +73,7 @@ DHCP_HLENETHERNET = const(0x06) DHCP_HOPS = const(0x00) -MAGIC_COOKIE = const(0x63825363) +MAGIC_COOKIE = b"c\x82Sc" # Four bytes 99.130.83.99 MAX_DHCP_OPT = const(0x10) # Default DHCP Server port @@ -127,6 +135,8 @@ def __init__( self._initial_xid = 0 self._transaction_id = 0 self._start_time = 0 + self._next_resend = 0 + self._retries = 0 # DHCP server configuration self.dhcp_server_ip = BROADCAST_SERVER_ADDR @@ -152,225 +162,6 @@ def __init__( (hostname or "WIZnet{}").split(".")[0].format(mac_string)[:42], "utf-8" ) - # pylint: disable=too-many-statements - def send_dhcp_message( - self, - state: int, - time_elapsed: float, - renew: bool = False, - ) -> None: - """ - 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 DHCP process started - :param bool renew: Set True for renew and rebind, defaults to False - """ - _BUFF[:] = b"\x00" * len(_BUFF) - # OP - _BUFF[0] = DHCP_BOOT_REQUEST - # HTYPE - _BUFF[1] = DHCP_HTYPE10MB - # HLEN - _BUFF[2] = DHCP_HLENETHERNET - # HOPS - _BUFF[3] = DHCP_HOPS - - # Transaction ID (xid) - self._initial_xid = htonl(self._transaction_id) - self._initial_xid = self._initial_xid.to_bytes(4, "big") - _BUFF[4:7] = self._initial_xid - - # seconds elapsed - _BUFF[8] = (int(time_elapsed) & 0xFF00) >> 8 - _BUFF[9] = int(time_elapsed) & 0x00FF - - # flags - flags = htons(0x8000) - flags = flags.to_bytes(2, "big") - _BUFF[10] = flags[1] - _BUFF[11] = flags[0] - - # 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 - - # NOTE: 192 octets of 0's, BOOTP legacy - - # Magic Cookie - _BUFF[236] = (MAGIC_COOKIE >> 24) & 0xFF - _BUFF[237] = (MAGIC_COOKIE >> 16) & 0xFF - _BUFF[238] = (MAGIC_COOKIE >> 8) & 0xFF - _BUFF[239] = MAGIC_COOKIE & 0xFF - - # Option - DHCP Message Type - _BUFF[240] = 53 - _BUFF[241] = 0x01 - _BUFF[242] = state - - # Option - Client Identifier - _BUFF[243] = 61 - # Length - _BUFF[244] = 0x07 - # HW Type - ETH - _BUFF[245] = 0x01 - # Client MAC Address - for mac, val in enumerate(self._mac_address): - _BUFF[246 + mac] = val - - # Option - Host Name - _BUFF[252] = 12 - hostname_len = len(self._hostname) - after_hostname = 254 + hostname_len - _BUFF[253] = hostname_len - _BUFF[254:after_hostname] = self._hostname - - 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] = 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] = bytes(self.dhcp_server_ip) - - _BUFF[after_hostname + 12] = 55 - _BUFF[after_hostname + 13] = 0x06 - # subnet mask - _BUFF[after_hostname + 14] = 1 - # routers on subnet - _BUFF[after_hostname + 15] = 3 - # DNS - _BUFF[after_hostname + 16] = 6 - # domain name - _BUFF[after_hostname + 17] = 15 - # renewal (T1) value - _BUFF[after_hostname + 18] = 58 - # rebinding (T2) value - _BUFF[after_hostname + 19] = 59 - _BUFF[after_hostname + 20] = 255 - - # Send DHCP packet - self._sock.send(_BUFF) - - # pylint: disable=too-many-branches, too-many-statements - def parse_dhcp_response( - self, - ) -> Union[Tuple[int, bytes], Tuple[int, int]]: - """Parse DHCP response from DHCP server. - - :return Union[Tuple[int, bytes], Tuple[int, int]]: DHCP packet type. - """ - # store packet in buffer - _BUFF = self._sock.recv() - if self._debug: - print("DHCP Response: ", _BUFF) - - # -- Parse Packet, FIXED -- # - # Validate OP - assert ( - _BUFF[0] == DHCP_BOOT_REPLY - ), "Malformed Packet - \ - DHCP message OP is not expected BOOT Reply." - - xid = _BUFF[4:8] - if bytes(xid) < self._initial_xid: - print("f") - return 0, 0 - - self.local_ip = tuple(_BUFF[16:20]) - if _BUFF[28:34] == 0: - return 0, 0 - - if int.from_bytes(_BUFF[235:240], "big") != MAGIC_COOKIE: - return 0, 0 - - # -- Parse Packet, VARIABLE -- # - ptr = 240 - while _BUFF[ptr] != OPT_END: - if _BUFF[ptr] == MSG_TYPE: - ptr += 1 - opt_len = _BUFF[ptr] - ptr += opt_len - msg_type = _BUFF[ptr] - ptr += 1 - elif _BUFF[ptr] == SUBNET_MASK: - ptr += 1 - opt_len = _BUFF[ptr] - ptr += 1 - 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 = tuple(_BUFF[ptr : ptr + opt_len]) - ptr += opt_len - elif _BUFF[ptr] == LEASE_TIME: - ptr += 1 - opt_len = _BUFF[ptr] - ptr += 1 - self._lease_time = int.from_bytes(_BUFF[ptr : ptr + opt_len], "big") - ptr += opt_len - elif _BUFF[ptr] == ROUTERS_ON_SUBNET: - ptr += 1 - opt_len = _BUFF[ptr] - ptr += 1 - 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 = tuple(_BUFF[ptr : ptr + 4]) - ptr += opt_len # still increment even though we only read 1 addr. - elif _BUFF[ptr] == T1_VAL: - ptr += 1 - opt_len = _BUFF[ptr] - ptr += 1 - self._t1 = int.from_bytes(_BUFF[ptr : ptr + opt_len], "big") - ptr += opt_len - elif _BUFF[ptr] == T2_VAL: - ptr += 1 - opt_len = _BUFF[ptr] - ptr += 1 - self._t2 = int.from_bytes(_BUFF[ptr : ptr + opt_len], "big") - ptr += opt_len - elif _BUFF[ptr] == 0: - break - else: - # We're not interested in this option - ptr += 1 - opt_len = _BUFF[ptr] - ptr += 1 - # no-op - ptr += opt_len - - if self._debug: - print( - "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, - ) - ) - - gc.collect() - return msg_type, xid - # pylint: disable=too-many-branches, too-many-statements def _dhcp_state_machine(self) -> None: """ @@ -410,23 +201,23 @@ def _dhcp_state_machine(self) -> None: ): 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.send_dhcp_message( + # 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.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() + msg_type, xid = None, None # self.parse_dhcp_response() if msg_type == DHCP_OFFER: # Check if transaction ID matches, otherwise it may be an offer # for another device @@ -436,9 +227,9 @@ def _dhcp_state_machine(self) -> None: "* 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.send_dhcp_message( + # DHCP_REQUEST, (time.monotonic() - self._start_time) + # ) self._dhcp_state = STATE_DHCP_REQUEST else: if self._debug: @@ -451,7 +242,7 @@ def _dhcp_state_machine(self) -> None: if self._sock.available(): if self._debug: print("* DHCP: Parsing ACK") - msg_type, xid = self.parse_dhcp_response() + msg_type, xid = None, None # 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, "big"): @@ -525,3 +316,185 @@ def request_dhcp_lease(self) -> bool: def maintain_dhcp_lease(self) -> None: """Maintain DHCP lease""" self._dhcp_state_machine() + + def _dsm_reset(self): + """I'll get to it""" + self._retries = 0 + + def _resend_time(self) -> float: + """I'll get to it""" + self._retries += 1 + return self._retries + randint(0, 2) + time.monotonic() + + def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: + """I'll get to it""" + + global _BUFF # pylint: disable=global-variable-not-assigned, global-statement + wait_for_link = 5 + # discover_max_retries = 3 + + while blocking: + pass # Dummy for pylint + + while True: + if self._dhcp_state == STATE_BOUND: + if time.monotonic() > self._t1: + self._dhcp_state = STATE_RENEWING + elif time.monotonic() > self._t2: + self._dhcp_state = STATE_REBINDING + else: + return + + if self._dhcp_state == STATE_INIT: + self._dsm_reset() + time_to_stop = time.monotonic() + wait_for_link + while not self._eth.link_status: + if time.monotonic() > time_to_stop: + raise TimeoutError("Ethernet link is down") + time.sleep(1) + self._generate_dhcp_message(message_type=DHCP_DISCOVER, time_elapsed=0) + self._sock.send(_BUFF) + self._retries = 0 + self._next_resend = self._resend_time() + self._dhcp_state = STATE_DHCP_DISCOVER + + if self._dhcp_state == STATE_DHCP_DISCOVER: + while True: + if self._sock.available(): + _BUFF = self._sock.recv() + self._parse_dhcp_response() + + def _generate_dhcp_message( + self, + *, + message_type: int, + time_elapsed: float, + broadcast: bool = False, + renew: bool = False, + ) -> None: + """ + Assemble a DHCP message packet. + + :param int time_elapsed: Time in seconds since transaction began. + :param float time_elapsed: Number of seconds elapsed since DHCP process started. + :param bool renew: Set True for renew and rebind, defaults to False. + :param bool broadcast: Used to set the flag requiring a broadcast reply from the + DHCP server. + """ + _BUFF[:] = b"\x00" * len(_BUFF) + # OP.HTYPE.HLEN.HOPS + _BUFF[0:4] = (DHCP_BOOT_REQUEST, DHCP_HTYPE10MB, DHCP_HLENETHERNET, DHCP_HOPS) + # Transaction ID (xid) + _BUFF[4:8] = self._transaction_id.to_bytes(4, "big") + # seconds elapsed + _BUFF[8:10] = int(time_elapsed).to_bytes(2, "big") + # flags (only bit 0 is used) + if broadcast: + _BUFF[10] = 0b10000000 + if renew: + _BUFF[12:16] = bytes(self.local_ip) + # chaddr + _BUFF[28:34] = self._mac_address + # Magic Cookie + _BUFF[236:240] = MAGIC_COOKIE + + # Option - DHCP Message Type + _BUFF[240] = 53 + _BUFF[241] = 1 + _BUFF[242] = message_type + # Option - Host Name + _BUFF[243] = 12 + hostname_len = len(self._hostname) + _BUFF[244] = hostname_len + after_hostname = hostname_len + 245 + _BUFF[245:after_hostname] = self._hostname + # Mark end of message + _BUFF[after_hostname] = 0xFF + + def _parse_dhcp_response( + self, + ) -> int: + """Parse DHCP response from DHCP server. + + :return Tuple[int, bytearray]: DHCP packet type and ID. + """ + + def option_data(pointer: int) -> Tuple[int, int, bytes]: + """Helper function to extract DHCP option data from a + response. + + :param int pointer: Pointer to start of DHCP option. + + :returns Tuple[int, int, bytes]: Pointer to next option, + option type and option data. + """ + global _BUFF # pylint: disable=global-variable-not-assigned + data_type = _BUFF[pointer] + pointer += 1 + data_length = _BUFF[pointer] + pointer += 1 + data_end = pointer + data_length + data = _BUFF[pointer:data_end] + return data_end, data_type, data + + global _BUFF # pylint: disable=global-variable-not-assigned + # Validate OP + if _BUFF[0] != DHCP_BOOT_REPLY: + raise ValueError("DHCP message OP is not expected BOOTP Reply.") + # Confirm transaction IDs match. + xid = _BUFF[4:8] + if xid != self._transaction_id.to_bytes(4, "big"): + raise ValueError("DHCP response ID mismatch.") + # Set the IP address to Claddr + self.local_ip = tuple(_BUFF[16:20]) + # Check that there is a client ID. + if _BUFF[28:34] == b"\x00\x00\x00\x00\x00\x00": + raise ValueError("No client ID in the response.") + # Check for the magic cookie. + if _BUFF[236:240] != MAGIC_COOKIE: + raise ValueError("No DHCP Magic Cookie in the response.") + + # Parse options + msg_type = None + ptr = 240 + while _BUFF[ptr] != OPT_END: + ptr, data_type, data = option_data(ptr) + if data_type == MSG_TYPE: + msg_type = data[0] + elif data_type == SUBNET_MASK: + self.subnet_mask = tuple(data) + elif data_type == DHCP_SERVER_ID: + self.dhcp_server_ip = tuple(data) + elif data_type == LEASE_TIME: + self._lease_time = int.from_bytes(data, "big") + elif data_type == ROUTERS_ON_SUBNET: + self.gateway_ip = tuple(data[:4]) + elif data_type == DNS_SERVERS: + self.dns_server_ip = tuple(data[:4]) + elif data_type == T1_VAL: + self._t1 = int.from_bytes(data, "big") + elif data_type == T2_VAL: + self._t2 = int.from_bytes(data, "big") + elif data_type == 0: + break + + if self._debug: + print( + "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, + ) + ) + + gc.collect() + if msg_type is None: + raise ValueError("No valid message type in response.") + return msg_type diff --git a/examples/wiznet5k_wsgiserver_test.py b/examples/wiznet5k_wsgiserver.py similarity index 100% rename from examples/wiznet5k_wsgiserver_test.py rename to examples/wiznet5k_wsgiserver.py diff --git a/tests/dummy_dhcp_data.py b/tests/dummy_dhcp_data.py new file mode 100644 index 0000000..618e5cb --- /dev/null +++ b/tests/dummy_dhcp_data.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: 2022 Martin Stephens +# +# SPDX-License-Identifier: MIT +"""Data for use in test_dhcp.py""" + +# Data for testing send data. +DHCP_SEND_01 = bytearray( + b"\x01\x01\x06\x00o\xff\xff\xff\x00\x17\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" + b"\x08\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc" + b"5\x01\x01\x0c\x12WIZnet040506070809\xff\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00" +) + +DHCP_SEND_02 = bytearray( + b"\x01\x01\x06\x00\xff\xff\xffo\x00#\x80\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18#.9DO\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x02=\x07\x01\x18#.9DO" + b"\x0c\x04bert\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007\x06" + b"\x01\x03\x06\x0f:;\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) + +DHCP_SEND_03 = bytearray( + b"\x01\x01\x06\x00\xff\xff\xffo\x00#\x80\x00\n\n\n+\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00c\x82Sc5\x01\x02=\x07\x01\xffa$e*c\x0c\x05cl" + b"ash\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007" + b"\x06\x01\x03\x06\x0f:;\xff\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) + +# Data to test response parser. +# Basic case, no extra fields, one each of router and DNS. +GOOD_DATA_01 = bytearray( + b"\x02\x00\x00\x00\x7f\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xc0" + b"\xa8\x05\x16\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x05\x07\t\x0b\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01" + b"\x02\x01\x04\xc0\xa8\x06\x026\x04\xeao\xde{3\x04\x00\x01\x01\x00\x03" + b'\x04yy\x04\x05\x06\x04\x05\x06\x07\x08:\x04\x00""\x00;\x04\x0033\x00' + b"\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +# Complex case, extra field, 2 each router and DNS. +GOOD_DATA_02 = bytearray( + b"\x02\x00\x00\x004Vx\x9a\x00\x00\x00\x00\x00\x00\x00\x00\x12$@\n\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5" + b"\x01\x05<\x05\x01\x02\x03\x04\x05\x01\x04\n\x0b\x07\xde6\x04zN\x91\x03\x03" + b"\x08\n\x0b\x0e\x0f\xff\x00\xff\x00\x06\x08\x13\x11\x0b\x07****3\x04\x00\x00" + b"=;:\x04\x00\x0e\x17@;\x04\x02\x92]\xde\xff\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) + +# +BAD_DATA = bytearray( + b"\x02\x00\x00\x00\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x12$@\n\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc" +) diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py new file mode 100644 index 0000000..8f39d32 --- /dev/null +++ b/tests/test_dhcp.py @@ -0,0 +1,301 @@ +# SPDX-FileCopyrightText: 2022 Martin Stephens +# +# SPDX-License-Identifier: MIT +"""Tests to confirm that there are no changes in behaviour to public methods and functions.""" +# pylint: disable=no-self-use, redefined-outer-name, protected-access, invalid-name, too-many-arguments +import pytest +from micropython import const +import dummy_dhcp_data as dhcp_data +import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as wiz_dhcp + +# +DEFAULT_DEBUG_ON = True + + +@pytest.fixture +def wiznet(mocker): + return mocker.patch("adafruit_wiznet5k.adafruit_wiznet5k.WIZNET5K", autospec=True) + + +@pytest.fixture +def wrench(mocker): + return mocker.patch( + "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket", autospec=True + ) + + +@pytest.mark.skip() +class TestDHCPInit: + def test_constants(self): + # DHCP State Machine + assert wiz_dhcp.STATE_DHCP_START == const(0x00) + assert wiz_dhcp.STATE_DHCP_DISCOVER == const(0x01) + assert wiz_dhcp.STATE_DHCP_REQUEST == const(0x02) + assert wiz_dhcp.STATE_DHCP_LEASED == const(0x03) + assert wiz_dhcp.STATE_DHCP_REREQUEST == const(0x04) + assert wiz_dhcp.STATE_DHCP_RELEASE == const(0x05) + assert wiz_dhcp.STATE_DHCP_WAIT == const(0x06) + assert wiz_dhcp.STATE_DHCP_DISCONN == const(0x07) + + # DHCP wait time between attempts + assert wiz_dhcp.DHCP_WAIT_TIME == const(60) + + # DHCP Message Types + assert wiz_dhcp.DHCP_DISCOVER == const(1) + assert wiz_dhcp.DHCP_OFFER == const(2) + assert wiz_dhcp.DHCP_REQUEST == const(3) + assert wiz_dhcp.DHCP_DECLINE == const(4) + assert wiz_dhcp.DHCP_ACK == const(5) + assert wiz_dhcp.DHCP_NAK == const(6) + assert wiz_dhcp.DHCP_RELEASE == const(7) + assert wiz_dhcp.DHCP_INFORM == const(8) + + # DHCP Message OP Codes + assert wiz_dhcp.DHCP_BOOT_REQUEST == const(0x01) + assert wiz_dhcp.DHCP_BOOT_REPLY == const(0x02) + + assert wiz_dhcp.DHCP_HTYPE10MB == const(0x01) + assert wiz_dhcp.DHCP_HTYPE100MB == const(0x02) + + assert wiz_dhcp.DHCP_HLENETHERNET == const(0x06) + assert wiz_dhcp.DHCP_HOPS == const(0x00) + + assert wiz_dhcp.MAGIC_COOKIE == b"c\x82Sc" + assert wiz_dhcp.MAX_DHCP_OPT == const(0x10) + + # Default DHCP Server port + assert wiz_dhcp.DHCP_SERVER_PORT == const(67) + # DHCP Lease Time, in seconds + assert wiz_dhcp.DEFAULT_LEASE_TIME == const(900) + assert wiz_dhcp.BROADCAST_SERVER_ADDR == (255, 255, 255, 255) + + # DHCP Response Options + assert wiz_dhcp.MSG_TYPE == 53 + assert wiz_dhcp.SUBNET_MASK == 1 + assert wiz_dhcp.ROUTERS_ON_SUBNET == 3 + assert wiz_dhcp.DNS_SERVERS == 6 + assert wiz_dhcp.DHCP_SERVER_ID == 54 + assert wiz_dhcp.T1_VAL == 58 + assert wiz_dhcp.T2_VAL == 59 + assert wiz_dhcp.LEASE_TIME == 51 + assert wiz_dhcp.OPT_END == 255 + + # Packet buffer + assert wiz_dhcp._BUFF == bytearray(318) + + @pytest.mark.parametrize( + "mac_address", + ( + [1, 2, 3, 4, 5, 6], + (7, 8, 9, 10, 11, 12), + bytes([1, 2, 4, 6, 7, 8]), + ), + ) + def test_dhcp_setup_default(self, mocker, wiznet, wrench, mac_address): + # Test with mac address as tuple, list and bytes with default values. + mock_randint = mocker.patch( + "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", autospec=True + ) + mock_randint.return_value = 0x1234567 + dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address) + assert dhcp_client._eth == wiznet + assert dhcp_client._response_timeout == 30.0 + assert dhcp_client._debug is False + assert dhcp_client._mac_address == mac_address + wrench.set_interface.assert_called_once_with(wiznet) + assert dhcp_client._sock is None + assert dhcp_client._dhcp_state == wiz_dhcp.STATE_DHCP_START + assert dhcp_client._initial_xid == 0 + mock_randint.assert_called_once() + assert dhcp_client._transaction_id == 0x1234567 + assert dhcp_client._start_time == 0 + assert dhcp_client.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR + assert dhcp_client.local_ip == 0 + assert dhcp_client.gateway_ip == 0 + assert dhcp_client.subnet_mask == 0 + assert dhcp_client.dns_server_ip == 0 + assert dhcp_client._lease_time == 0 + assert dhcp_client._last_lease_time == 0 + assert dhcp_client._renew_in_sec == 0 + assert dhcp_client._rebind_in_sec == 0 + assert dhcp_client._t1 == 0 + assert dhcp_client._t2 == 0 + mac_string = "".join("{:02X}".format(o) for o in mac_address) + assert dhcp_client._hostname == bytes( + "WIZnet{}".split(".", maxsplit=1)[0].format(mac_string)[:42], "utf-8" + ) + + def test_dhcp_setup_other_args(self, wiznet): + mac_address = (7, 8, 9, 10, 11, 12) + dhcp_client = wiz_dhcp.DHCP( + wiznet, mac_address, hostname="fred.com", response_timeout=25.0, debug=True + ) + + assert dhcp_client._response_timeout == 25.0 + assert dhcp_client._debug is True + mac_string = "".join("{:02X}".format(o) for o in mac_address) + assert dhcp_client._hostname == bytes( + "fred.com".split(".", maxsplit=1)[0].format(mac_string)[:42], "utf-8" + ) + + +class TestSendDHCPMessage: + def test_generate_message_with_defaults(self, wiznet, wrench): + assert len(wiz_dhcp._BUFF) == 318 + dhcp_client = wiz_dhcp.DHCP(wiznet, (4, 5, 6, 7, 8, 9)) + dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) + dhcp_client._transaction_id = 0x6FFFFFFF + dhcp_client._generate_dhcp_message(message_type=1, time_elapsed=23.4) + assert wiz_dhcp._BUFF == dhcp_data.DHCP_SEND_01 + assert len(wiz_dhcp._BUFF) == 318 + + @pytest.mark.parametrize( + "mac_address, hostname, state, time_elapsed, renew, local_ip, server_ip, result", + ( + ( + (4, 5, 6, 7, 8, 9), + None, + wiz_dhcp.STATE_DHCP_DISCOVER, + 23.4, + False, + 0, + 0, + dhcp_data.DHCP_SEND_01, + ), + ( + (24, 35, 46, 57, 68, 79), + "bert.co.uk", + wiz_dhcp.STATE_DHCP_REQUEST, + 35.5, + False, + (192, 168, 3, 4), + (222, 123, 23, 10), + dhcp_data.DHCP_SEND_02, + ), + ( + (255, 97, 36, 101, 42, 99), + "clash.net", + wiz_dhcp.STATE_DHCP_REQUEST, + 35.5, + True, + (10, 10, 10, 43), + (145, 66, 45, 22), + dhcp_data.DHCP_SEND_03, + ), + ), + ) + def test_generate_dhcp_message( + self, + wiznet, + wrench, + mac_address, + hostname, + state, + time_elapsed, + renew, + local_ip, + server_ip, + # result, + ): + dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address, hostname=hostname) + # Mock out socket to check what is sent + dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) + # Set client attributes for test + dhcp_client.local_ip = local_ip + dhcp_client.dhcp_server_ip = server_ip + dhcp_client._transaction_id = 0x6FFFFFFF + # Test + dhcp_client._generate_dhcp_message( + message_type=state, time_elapsed=time_elapsed, renew=renew + ) + assert len(wiz_dhcp._BUFF) == 318 + + +class TestParseDhcpMessage: + @pytest.mark.parametrize( + "xid, local_ip, msg_type, subnet, dhcp_ip, gate_ip, dns_ip, lease, t1, t2, response", + ( + ( + 0x7FFFFFFF, + (192, 168, 5, 22), + 2, + (192, 168, 6, 2), + (234, 111, 222, 123), + (121, 121, 4, 5), + (5, 6, 7, 8), + 65792, + 2236928, + 3355392, + dhcp_data.GOOD_DATA_01, + ), + ( + 0x3456789A, + (18, 36, 64, 10), + 5, + (10, 11, 7, 222), + (122, 78, 145, 3), + (10, 11, 14, 15), + (19, 17, 11, 7), + 15675, + 923456, + 43146718, + dhcp_data.GOOD_DATA_02, + ), + ), + ) + # pylint: disable=too-many-locals + def test_parse_good_data( + self, + wiznet, + xid, + local_ip, + msg_type, + subnet, + dhcp_ip, + gate_ip, + dns_ip, + lease, + t1, + t2, + response, + ): + wiz_dhcp._BUFF = response + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client._transaction_id = xid + response_type = dhcp_client._parse_dhcp_response() + assert response_type == msg_type + assert dhcp_client.local_ip == local_ip + assert dhcp_client.subnet_mask == subnet + assert dhcp_client.dhcp_server_ip == dhcp_ip + assert dhcp_client.gateway_ip == gate_ip + assert dhcp_client.dns_server_ip == dns_ip + assert dhcp_client._lease_time == lease + assert dhcp_client._t1 == t1 + assert dhcp_client._t2 == t2 + + def test_parsing_failures(self, wiznet, wrench): + # Test for bad OP code, ID mismatch, no server ID, bad Magic Cookie + bad_data = dhcp_data.BAD_DATA + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) + dhcp_client._sock.recv.return_value = bad_data + # Transaction ID mismatch. + dhcp_client._transaction_id = 0x42424242 + with pytest.raises(ValueError): + dhcp_client._parse_dhcp_response() + # Bad OP code. + bad_data[0] = 0 + dhcp_client._transaction_id = 0x7FFFFFFF + dhcp_client._initial_xid = dhcp_client._transaction_id.to_bytes(4, "little") + with pytest.raises(ValueError): + dhcp_client._parse_dhcp_response() + bad_data[0] = 2 # Reset to good value + # No server ID. + bad_data[28:34] = (0, 0, 0, 0, 0, 0) + with pytest.raises(ValueError): + dhcp_client._parse_dhcp_response() + bad_data[28:34] = (1, 1, 1, 1, 1, 1) # Reset to good value + # Bad Magic Cookie. + bad_data[236] = 0 + with pytest.raises(ValueError): + dhcp_client._parse_dhcp_response() From 868cb51c3bbb21e82a0dad234438fe2370212099 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 17 Dec 2022 15:43:28 +0300 Subject: [PATCH 02/80] Updated tests for the broadcast flag. --- tests/dummy_dhcp_data.py | 49 ++++++++++++++++++++++++++++++---------- tests/test_dhcp.py | 44 +++++++++++++++++++++--------------- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/tests/dummy_dhcp_data.py b/tests/dummy_dhcp_data.py index 618e5cb..ccc9850 100644 --- a/tests/dummy_dhcp_data.py +++ b/tests/dummy_dhcp_data.py @@ -4,6 +4,7 @@ """Data for use in test_dhcp.py""" # Data for testing send data. +# Default settings (DISCOVER, broadcast=False, default hostname, renew=False) DHCP_SEND_01 = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00\x17\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" @@ -28,7 +29,29 @@ ) DHCP_SEND_02 = bytearray( - b"\x01\x01\x06\x00\xff\xff\xffo\x00#\x80\x00\x00\x00\x00\x00" + b"\x01\x01\x06\x00o\xff\xff\xff\x00\x17\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" + b"\x08\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc" + b"5\x01\x01\x0c\x12WIZnet040506070809\xff\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00" +) +DHCP_SEND_03 = bytearray( + b"\x01\x01\x06\x00o\xff\xff\xff\x00#\x80\x00\xc0\xa8\x03\x04" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18#.9DO\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" @@ -42,16 +65,19 @@ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x02=\x07\x01\x18#.9DO" - b"\x0c\x04bert\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007\x06" - b"\x01\x03\x06\x0f:;\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x03\x0c\x04ber" + b"t\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00" ) -DHCP_SEND_03 = bytearray( - b"\x01\x01\x06\x00\xff\xff\xffo\x00#\x80\x00\n\n\n+\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c\x00\x00\x00\x00\x00\x00" +DHCP_SEND_04 = bytearray( + b"\x01\x01\x06\x00o\xff\xff\xff\x00#\x80\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" @@ -62,13 +88,12 @@ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x03\x0c\x05cla" + b"sh\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00c\x82Sc5\x01\x02=\x07\x01\xffa$e*c\x0c\x05cl" - b"ash\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007" - b"\x06\x01\x03\x06\x0f:;\xff\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00" ) # Data to test response parser. diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py index 8f39d32..99bf8bf 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp.py @@ -145,70 +145,78 @@ def test_generate_message_with_defaults(self, wiznet, wrench): dhcp_client = wiz_dhcp.DHCP(wiznet, (4, 5, 6, 7, 8, 9)) dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) dhcp_client._transaction_id = 0x6FFFFFFF - dhcp_client._generate_dhcp_message(message_type=1, time_elapsed=23.4) + dhcp_client._generate_dhcp_message( + message_type=wiz_dhcp.DHCP_DISCOVER, time_elapsed=23.4 + ) assert wiz_dhcp._BUFF == dhcp_data.DHCP_SEND_01 assert len(wiz_dhcp._BUFF) == 318 @pytest.mark.parametrize( - "mac_address, hostname, state, time_elapsed, renew, local_ip, server_ip, result", + "mac_address, hostname, msg_type, time_elapsed, renew, \ + broadcast_only, local_ip, server_ip, result", ( ( (4, 5, 6, 7, 8, 9), None, - wiz_dhcp.STATE_DHCP_DISCOVER, + wiz_dhcp.DHCP_DISCOVER, 23.4, False, - 0, - 0, - dhcp_data.DHCP_SEND_01, + False, + (0, 0, 0, 0), + (0, 0, 0, 0), + dhcp_data.DHCP_SEND_02, ), ( (24, 35, 46, 57, 68, 79), "bert.co.uk", - wiz_dhcp.STATE_DHCP_REQUEST, + wiz_dhcp.DHCP_REQUEST, 35.5, - False, + True, + True, (192, 168, 3, 4), (222, 123, 23, 10), - dhcp_data.DHCP_SEND_02, + dhcp_data.DHCP_SEND_03, ), ( (255, 97, 36, 101, 42, 99), "clash.net", - wiz_dhcp.STATE_DHCP_REQUEST, + wiz_dhcp.DHCP_REQUEST, 35.5, + False, True, (10, 10, 10, 43), (145, 66, 45, 22), - dhcp_data.DHCP_SEND_03, + dhcp_data.DHCP_SEND_04, ), ), ) - def test_generate_dhcp_message( + def test_generate_dhcp_message_with_options( self, wiznet, - wrench, mac_address, hostname, - state, + msg_type, time_elapsed, renew, + broadcast_only, local_ip, server_ip, - # result, + result, ): dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address, hostname=hostname) - # Mock out socket to check what is sent - dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) # Set client attributes for test dhcp_client.local_ip = local_ip dhcp_client.dhcp_server_ip = server_ip dhcp_client._transaction_id = 0x6FFFFFFF # Test dhcp_client._generate_dhcp_message( - message_type=state, time_elapsed=time_elapsed, renew=renew + message_type=msg_type, + time_elapsed=time_elapsed, + renew=renew, + broadcast=broadcast_only, ) assert len(wiz_dhcp._BUFF) == 318 + assert wiz_dhcp._BUFF == result class TestParseDhcpMessage: From 5a767f142f7b7f780fb04889148496bd964c6dd0 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 17 Dec 2022 17:38:38 +0300 Subject: [PATCH 03/80] Added DHCP options for REQUEST to _generate_dhcp_message. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 73 +++++++++++++++++---- tests/dummy_dhcp_data.py | 50 +++++++++++++- tests/test_dhcp.py | 64 ++++++++++++++++-- 3 files changed, 169 insertions(+), 18 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 41e35c5..0116305 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -359,10 +359,23 @@ def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: self._dhcp_state = STATE_DHCP_DISCOVER if self._dhcp_state == STATE_DHCP_DISCOVER: - while True: + while time.monotonic() < self._next_resend: if self._sock.available(): _BUFF = self._sock.recv() - self._parse_dhcp_response() + try: + msg_type = self._parse_dhcp_response() + except ValueError as error: + if self._debug: + print(error) + else: + if msg_type == DHCP_OFFER: + self._generate_dhcp_message( + message_type=DHCP_REQUEST, time_elapsed=0 + ) + self._sock.send(_BUFF) + self._retries = 0 + self._next_resend = self._resend_time() + self._dhcp_state = STATE_REQUESTING def _generate_dhcp_message( self, @@ -381,6 +394,29 @@ def _generate_dhcp_message( :param bool broadcast: Used to set the flag requiring a broadcast reply from the DHCP server. """ + + def option_data( + pointer: int, option_code: int, option_data: Union[Tuple[int, ...], bytes] + ) -> int: + """Helper function to set DHCP option data for a DHCP + message. + + :param int pointer: Pointer to start of DHCP option. + :param int option_code: Type of option to add. + :param Tuple[int] option_data: The data for the option. + + :returns int: Pointer to next option. + """ + global _BUFF # pylint: disable=global-variable-not-assigned + _BUFF[pointer] = option_code + data_length = len(option_data) + pointer += 1 + _BUFF[pointer] = data_length + pointer += 1 + data_end = pointer + data_length + _BUFF[pointer:data_end] = option_data + return data_end + _BUFF[:] = b"\x00" * len(_BUFF) # OP.HTYPE.HLEN.HOPS _BUFF[0:4] = (DHCP_BOOT_REQUEST, DHCP_HTYPE10MB, DHCP_HLENETHERNET, DHCP_HOPS) @@ -398,18 +434,31 @@ def _generate_dhcp_message( # Magic Cookie _BUFF[236:240] = MAGIC_COOKIE + # Set DHCP options. + pointer = 240 + # Option - DHCP Message Type - _BUFF[240] = 53 - _BUFF[241] = 1 - _BUFF[242] = message_type + pointer = option_data( + pointer=pointer, option_code=53, option_data=(message_type,) + ) # Option - Host Name - _BUFF[243] = 12 - hostname_len = len(self._hostname) - _BUFF[244] = hostname_len - after_hostname = hostname_len + 245 - _BUFF[245:after_hostname] = self._hostname - # Mark end of message - _BUFF[after_hostname] = 0xFF + pointer = option_data( + pointer=pointer, option_code=12, option_data=self._hostname + ) + if message_type == DHCP_REQUEST: + # Request subnet mask, router and DNS server. + pointer = option_data( + pointer=pointer, option_code=55, option_data=(1, 3, 6) + ) + # Set Requested IP Address to offered IP address. + pointer = option_data( + pointer=pointer, option_code=50, option_data=self.local_ip + ) + # Set Server ID to chosen DHCP server IP address. + pointer = option_data( + pointer=pointer, option_code=54, option_data=self.dhcp_server_ip + ) + _BUFF[pointer] = 0xFF def _parse_dhcp_response( self, diff --git a/tests/dummy_dhcp_data.py b/tests/dummy_dhcp_data.py index ccc9850..b9caa82 100644 --- a/tests/dummy_dhcp_data.py +++ b/tests/dummy_dhcp_data.py @@ -4,6 +4,7 @@ """Data for use in test_dhcp.py""" # Data for testing send data. +# DHCP DISCOVER messages. # Default settings (DISCOVER, broadcast=False, default hostname, renew=False) DHCP_SEND_01 = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00\x17\x00\x00\x00\x00\x00\x00" @@ -65,7 +66,7 @@ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x03\x0c\x04ber" + b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x01\x0c\x04ber" b"t\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" @@ -88,7 +89,7 @@ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x03\x0c\x05cla" + b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x01\x0c\x05cla" b"sh\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" @@ -96,6 +97,51 @@ b"\x00\x00\x00\x00\x00\x00" ) +# DHCP REQUEST messages. +DHCP_SEND_05 = bytearray( + b"\x01\x01\x06\x00o\xff\xff\xff\x00\x10\x80\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x03\x0c\nhelicopter7" + b"\x03\x01\x03\x062\x04\n\n\n+6\x04\x91B-\x16\xff\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) + +DHCP_SEND_06 = bytearray( + b"\x01\x01\x06\x00o\xff\xff\xff\x00H\x80\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00K?\xa6\x04" + b"\xc8e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc" + b"5\x01\x03\x0c\x12WIZnet4B3FA604C8657\x03\x01\x03\x062\x04de" + b"f\x046\x04\xf5\xa6\x05\x0b\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) + # Data to test response parser. # Basic case, no extra fields, one each of router and DNS. GOOD_DATA_01 = bytearray( diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py index 99bf8bf..ac93fa9 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp.py @@ -140,7 +140,7 @@ def test_dhcp_setup_other_args(self, wiznet): class TestSendDHCPMessage: - def test_generate_message_with_defaults(self, wiznet, wrench): + def test_generate_message_discover_with_defaults(self, wiznet, wrench): assert len(wiz_dhcp._BUFF) == 318 dhcp_client = wiz_dhcp.DHCP(wiznet, (4, 5, 6, 7, 8, 9)) dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) @@ -169,7 +169,7 @@ def test_generate_message_with_defaults(self, wiznet, wrench): ( (24, 35, 46, 57, 68, 79), "bert.co.uk", - wiz_dhcp.DHCP_REQUEST, + wiz_dhcp.DHCP_DISCOVER, 35.5, True, True, @@ -180,7 +180,7 @@ def test_generate_message_with_defaults(self, wiznet, wrench): ( (255, 97, 36, 101, 42, 99), "clash.net", - wiz_dhcp.DHCP_REQUEST, + wiz_dhcp.DHCP_DISCOVER, 35.5, False, True, @@ -190,7 +190,63 @@ def test_generate_message_with_defaults(self, wiznet, wrench): ), ), ) - def test_generate_dhcp_message_with_options( + def test_generate_dhcp_message_discover_with_non_defaults( + self, + wiznet, + mac_address, + hostname, + msg_type, + time_elapsed, + renew, + broadcast_only, + local_ip, + server_ip, + result, + ): + dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address, hostname=hostname) + # Set client attributes for test + dhcp_client.local_ip = local_ip + dhcp_client.dhcp_server_ip = server_ip + dhcp_client._transaction_id = 0x6FFFFFFF + # Test + dhcp_client._generate_dhcp_message( + message_type=msg_type, + time_elapsed=time_elapsed, + renew=renew, + broadcast=broadcast_only, + ) + assert len(wiz_dhcp._BUFF) == 318 + assert wiz_dhcp._BUFF == result + + @pytest.mark.parametrize( + "mac_address, hostname, msg_type, time_elapsed, renew, \ + broadcast_only, local_ip, server_ip, result", + ( + ( + (255, 97, 36, 101, 42, 99), + "helicopter.org", + wiz_dhcp.DHCP_REQUEST, + 16.3, + False, + True, + (10, 10, 10, 43), + (145, 66, 45, 22), + dhcp_data.DHCP_SEND_05, + ), + ( + (75, 63, 166, 4, 200, 101), + None, + wiz_dhcp.DHCP_REQUEST, + 72.4, + False, + True, + (100, 101, 102, 4), + (245, 166, 5, 11), + dhcp_data.DHCP_SEND_06, + ), + ), + ) + def test_generate_dhcp_message_with_request_options( self, wiznet, mac_address, From e0f17a2f62fad799d5f32e21c416ff61d7b4edf8 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 17 Dec 2022 20:29:12 +0300 Subject: [PATCH 04/80] Added IP collision detection via ARP request. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 103 +++++++++++++++----- 1 file changed, 81 insertions(+), 22 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 0116305..de3c82f 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -137,6 +137,7 @@ def __init__( self._start_time = 0 self._next_resend = 0 self._retries = 0 + self._max_retries = 0 # DHCP server configuration self.dhcp_server_ip = BROADCAST_SERVER_ADDR @@ -326,6 +327,37 @@ def _resend_time(self) -> float: self._retries += 1 return self._retries + randint(0, 2) + time.monotonic() + def _set_next_state(self, *, next_state: int, max_retries: int) -> None: + """I'll get to it""" + self._sock.send(_BUFF) + self._retries = 0 + self._max_retries = max_retries + self._next_resend = self._resend_time() + self._dhcp_state = next_state + + def _arp_check_for_ip_collision(self) -> bool: + """I'll get to it""" + timeout = 0.25 + arp_packet = bytearray(b"0x00" * 28) + # Set ARP headers + arp_packet[:8] = (0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01) + arp_packet[8:14] = self._mac_address + arp_packet[24:] = self.local_ip + for _ in range(3): + self._sock.send(arp_packet) + stop_time = time.monotonic() + timeout + while time.monotonic() < stop_time: + if self._sock.available(): + buffer = self._sock.recv() + if ( + tuple(buffer[18:24]) == self._mac_address + and tuple(buffer[14:18]) == self.local_ip + ): + # Another device is already using this IP address. + return False + # No response to ARP request, can accept this IP address. + return True + def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: """I'll get to it""" @@ -335,7 +367,7 @@ def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: while blocking: pass # Dummy for pylint - + # pylint: disable=too-many-nested-blocks while True: if self._dhcp_state == STATE_BOUND: if time.monotonic() > self._t1: @@ -353,29 +385,56 @@ def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: raise TimeoutError("Ethernet link is down") time.sleep(1) self._generate_dhcp_message(message_type=DHCP_DISCOVER, time_elapsed=0) - self._sock.send(_BUFF) - self._retries = 0 - self._next_resend = self._resend_time() - self._dhcp_state = STATE_DHCP_DISCOVER + self._set_next_state(next_state=STATE_DHCP_DISCOVER, max_retries=3) if self._dhcp_state == STATE_DHCP_DISCOVER: - while time.monotonic() < self._next_resend: - if self._sock.available(): - _BUFF = self._sock.recv() - try: - msg_type = self._parse_dhcp_response() - except ValueError as error: - if self._debug: - print(error) - else: - if msg_type == DHCP_OFFER: - self._generate_dhcp_message( - message_type=DHCP_REQUEST, time_elapsed=0 - ) - self._sock.send(_BUFF) - self._retries = 0 - self._next_resend = self._resend_time() - self._dhcp_state = STATE_REQUESTING + while True: + while time.monotonic() < self._next_resend: + if self._sock.available(): + _BUFF = self._sock.recv() + try: + msg_type = self._parse_dhcp_response() + except ValueError as error: + if self._debug: + print(error) + else: + if msg_type == DHCP_OFFER: + self._generate_dhcp_message( + message_type=DHCP_REQUEST, time_elapsed=0 + ) + self._set_next_state( + next_state=STATE_REQUESTING, max_retries=3 + ) + break + if not blocking: + break + self._next_resend = self._resend_time() + if not blocking: + break + + if self._dhcp_state == STATE_REQUESTING: + while True: + while time.monotonic() < self._next_resend: + if self._sock.available(): + _BUFF = self._sock.recv() + try: + msg_type = self._parse_dhcp_response() + except ValueError as error: + if self._debug: + print(error) + else: + if msg_type == DHCP_NAK: + self._set_next_state( + next_state=STATE_INIT, max_retries=0 + ) + break + if msg_type == DHCP_ACK: + ... + if not blocking: + break + self._next_resend = self._resend_time() + if not blocking: + break def _generate_dhcp_message( self, From b740d14886f5a9d324fe4366b89709c29b737589 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 17 Dec 2022 22:40:26 +0300 Subject: [PATCH 05/80] Added all state machine states. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 158 ++++++++++++++------ 1 file changed, 110 insertions(+), 48 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index de3c82f..68c8623 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -81,6 +81,7 @@ # DHCP Lease Time, in seconds DEFAULT_LEASE_TIME = const(900) BROADCAST_SERVER_ADDR = (255, 255, 255, 255) +UNASSIGNED_IP_ADDR = (0, 0, 0, 0) # DHCP Response Options MSG_TYPE = 53 @@ -138,13 +139,15 @@ def __init__( self._next_resend = 0 self._retries = 0 self._max_retries = 0 + self._blocking = False + self._renew = False # DHCP server configuration self.dhcp_server_ip = BROADCAST_SERVER_ADDR - self.local_ip = 0 - self.gateway_ip = 0 - self.subnet_mask = 0 - self.dns_server_ip = 0 + self.local_ip = UNASSIGNED_IP_ADDR + self.gateway_ip = UNASSIGNED_IP_ADDR + self.subnet_mask = UNASSIGNED_IP_ADDR + self.dns_server_ip = UNASSIGNED_IP_ADDR # Lease configuration self._lease_time = 0 @@ -320,74 +323,110 @@ def maintain_dhcp_lease(self) -> None: def _dsm_reset(self): """I'll get to it""" + if self._sock: + self._sock.close() + self._sock = None + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + self._eth.ifconfig = ( + UNASSIGNED_IP_ADDR, + UNASSIGNED_IP_ADDR, + UNASSIGNED_IP_ADDR, + UNASSIGNED_IP_ADDR, + ) + self.gateway_ip = UNASSIGNED_IP_ADDR + self.local_ip = UNASSIGNED_IP_ADDR + self.subnet_mask = UNASSIGNED_IP_ADDR + self._renew = False self._retries = 0 + self._increment_transaction_id() + self._start_time = int(time.monotonic()) + + def _increment_transaction_id(self): + self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF def _resend_time(self) -> float: """I'll get to it""" self._retries += 1 - return self._retries + randint(0, 2) + time.monotonic() + return self._retries * 4 + randint(0, 2) + int(time.monotonic()) - def _set_next_state(self, *, next_state: int, max_retries: int) -> None: + def _set_next_state( + self, *, next_state: int, max_retries: int, send_msg: bool = True + ) -> None: """I'll get to it""" - self._sock.send(_BUFF) + if send_msg: + self._sock.send(_BUFF) self._retries = 0 self._max_retries = max_retries self._next_resend = self._resend_time() self._dhcp_state = next_state - def _arp_check_for_ip_collision(self) -> bool: + def _ip_collision(self) -> bool: """I'll get to it""" - timeout = 0.25 - arp_packet = bytearray(b"0x00" * 28) - # Set ARP headers - arp_packet[:8] = (0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01) - arp_packet[8:14] = self._mac_address - arp_packet[24:] = self.local_ip - for _ in range(3): - self._sock.send(arp_packet) - stop_time = time.monotonic() + timeout - while time.monotonic() < stop_time: - if self._sock.available(): - buffer = self._sock.recv() - if ( - tuple(buffer[18:24]) == self._mac_address - and tuple(buffer[14:18]) == self.local_ip - ): - # Another device is already using this IP address. - return False - # No response to ARP request, can accept this IP address. - return True + if not self._renew: # Don't check if the lease is being renewed. + timeout = 0.25 + arp_packet = bytearray(b"0x00" * 28) + # Set ARP headers + arp_packet[:8] = (0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01) + arp_packet[8:14] = self._mac_address + arp_packet[24:] = self.local_ip + for _ in range(3): + self._sock.send(arp_packet) + stop_time = time.monotonic() + timeout + while time.monotonic() < stop_time: + if self._sock.available(): + buffer = self._sock.recv() + if ( + tuple(buffer[18:24]) == self._mac_address + and tuple(buffer[14:18]) == self.local_ip + ): + # Another device is already using this IP address. + return True + # No response to ARP request or renewing, accept this IP address. + return False def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: """I'll get to it""" global _BUFF # pylint: disable=global-variable-not-assigned, global-statement - wait_for_link = 5 - # discover_max_retries = 3 + self._blocking = blocking - while blocking: - pass # Dummy for pylint # pylint: disable=too-many-nested-blocks while True: if self._dhcp_state == STATE_BOUND: - if time.monotonic() > self._t1: - self._dhcp_state = STATE_RENEWING - elif time.monotonic() > self._t2: + now = time.monotonic() + if now < self._t1: + return + if now > self._lease_time: + self._blocking = True + self._dhcp_state = STATE_INIT + elif now > self._t2: self._dhcp_state = STATE_REBINDING else: - return + self._dhcp_state = STATE_RENEWING + + if self._dhcp_state == STATE_RENEWING: + self._renew = True + self._generate_dhcp_message(message_type=DHCP_REQUEST) + self._set_next_state(next_state=STATE_REQUESTING, max_retries=3) + + if self._dhcp_state == STATE_REBINDING: + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + self._renew = True + self._generate_dhcp_message(message_type=DHCP_REQUEST) + self._set_next_state(next_state=STATE_REQUESTING, max_retries=3) if self._dhcp_state == STATE_INIT: self._dsm_reset() + wait_for_link = 5 time_to_stop = time.monotonic() + wait_for_link while not self._eth.link_status: if time.monotonic() > time_to_stop: raise TimeoutError("Ethernet link is down") - time.sleep(1) - self._generate_dhcp_message(message_type=DHCP_DISCOVER, time_elapsed=0) - self._set_next_state(next_state=STATE_DHCP_DISCOVER, max_retries=3) + time.sleep(0.5) + self._generate_dhcp_message(message_type=DHCP_DISCOVER) + self._set_next_state(next_state=STATE_SELECTING, max_retries=3) - if self._dhcp_state == STATE_DHCP_DISCOVER: + if self._dhcp_state == STATE_SELECTING: while True: while time.monotonic() < self._next_resend: if self._sock.available(): @@ -400,16 +439,16 @@ def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: else: if msg_type == DHCP_OFFER: self._generate_dhcp_message( - message_type=DHCP_REQUEST, time_elapsed=0 + message_type=DHCP_REQUEST ) self._set_next_state( next_state=STATE_REQUESTING, max_retries=3 ) break - if not blocking: + if not self._blocking: break self._next_resend = self._resend_time() - if not blocking: + if not self._blocking: break if self._dhcp_state == STATE_REQUESTING: @@ -429,18 +468,41 @@ def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: ) break if msg_type == DHCP_ACK: - ... - if not blocking: + if self._ip_collision(): + self._generate_dhcp_message( + message_type=DHCP_DECLINE + ) + self._set_next_state( + next_state=STATE_INIT, max_retries=0 + ) + else: + if self._lease_time == 0: + self._lease_time = DEFAULT_LEASE_TIME + self._t1 = ( + self._start_time + self._lease_time // 2 + ) + self._t2 = ( + self._start_time + + self._lease_time + - self._lease_time // 8 + ) + self._lease_time += self._start_time + self._increment_transaction_id() + self._renew = False + self._blocking = False + self._dhcp_state = STATE_BOUND + if msg_type == DHCP_NAK: + self._dhcp_state = STATE_INIT + if not self._blocking: break self._next_resend = self._resend_time() - if not blocking: + if not self._blocking: break def _generate_dhcp_message( self, *, message_type: int, - time_elapsed: float, broadcast: bool = False, renew: bool = False, ) -> None: @@ -482,7 +544,7 @@ def option_data( # Transaction ID (xid) _BUFF[4:8] = self._transaction_id.to_bytes(4, "big") # seconds elapsed - _BUFF[8:10] = int(time_elapsed).to_bytes(2, "big") + _BUFF[8:10] = int(time.monotonic() - self._start_time).to_bytes(2, "big") # flags (only bit 0 is used) if broadcast: _BUFF[10] = 0b10000000 From 16def1dddca728bb5257aac18f0f26b429bc8948 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sun, 18 Dec 2022 16:10:47 +0300 Subject: [PATCH 06/80] Removed IP collision detection, AF_PACKET not support in CP sockets. Added some tests for helper functions. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 577 ++++++++++---------- tests/test_dhcp.py | 67 ++- 2 files changed, 336 insertions(+), 308 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 68c8623..131a07f 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -23,25 +23,13 @@ except ImportError: pass - import gc import time from random import randint from micropython import const import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket -from adafruit_wiznet5k.adafruit_wiznet5k_socket import htonl - # DHCP State Machine -STATE_DHCP_START = const(0x00) -STATE_DHCP_DISCOVER = const(0x01) -STATE_DHCP_REQUEST = const(0x02) -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) - STATE_INIT = const(0x01) STATE_SELECTING = const(0x02) STATE_REQUESTING = const(0x03) @@ -50,9 +38,6 @@ STATE_REBINDING = const(0x06) STATE_RELEASING = const(0x07) -# DHCP wait time between attempts -DHCP_WAIT_TIME = const(60) - # DHCP Message Types DHCP_DISCOVER = const(1) DHCP_OFFER = const(2) @@ -132,9 +117,9 @@ def __init__( self._sock = None # DHCP state machine - self._dhcp_state = STATE_DHCP_START - self._initial_xid = 0 - self._transaction_id = 0 + self._dhcp_state = STATE_INIT + # self._initial_xid = 0 + self._transaction_id = randint(1, 0x7FFFFFFF) self._start_time = 0 self._next_resend = 0 self._retries = 0 @@ -142,7 +127,7 @@ def __init__( self._blocking = False self._renew = False - # DHCP server configuration + # DHCP binding configuration self.dhcp_server_ip = BROADCAST_SERVER_ADDR self.local_ip = UNASSIGNED_IP_ADDR self.gateway_ip = UNASSIGNED_IP_ADDR @@ -151,181 +136,170 @@ def __init__( # Lease configuration self._lease_time = 0 - self._last_lease_time = 0 - self._renew_in_sec = 0 - self._rebind_in_sec = 0 + # self._last_lease_time = 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" ) - # pylint: disable=too-many-branches, too-many-statements - def _dhcp_state_machine(self) -> None: - """ - DHCP state machine without wait loops to enable cooperative multitasking. - 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: 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: 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 = None, None # 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, "big"): - 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: - 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 = None, None # 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, "big"): - 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: - 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 in ( - STATE_DHCP_DISCOVER, - 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 + # # pylint: disable=too-many-branches, too-many-statements + # def _dhcp_state_machine(self) -> None: + # """ + # DHCP state machine without wait loops to enable cooperative multitasking. + # 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: 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: 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 = None, None # 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, "big"): + # 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: + # 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 = None, None # 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, "big"): + # 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: + # 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 in ( + # STATE_DHCP_DISCOVER, + # 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) -> bool: """Request to renew or acquire a DHCP lease.""" - if self._dhcp_state in (STATE_DHCP_LEASED, STATE_DHCP_WAIT): - self._dhcp_state = STATE_DHCP_START - - while self._dhcp_state not in (STATE_DHCP_LEASED, STATE_DHCP_WAIT): - self._dhcp_state_machine() - - return self._dhcp_state == STATE_DHCP_LEASED + self._dhcp_state_machine(blocking=True) + return self._dhcp_state == STATE_BOUND def maintain_dhcp_lease(self) -> None: """Maintain DHCP lease""" self._dhcp_state_machine() - def _dsm_reset(self): - """I'll get to it""" - if self._sock: - self._sock.close() - self._sock = None + def _dsm_reset(self) -> None: + """Close the socket and set attributes to default values used by the + state machine INIT state.""" + self._socket_release() self.dhcp_server_ip = BROADCAST_SERVER_ADDR self._eth.ifconfig = ( UNASSIGNED_IP_ADDR, @@ -336,61 +310,124 @@ def _dsm_reset(self): self.gateway_ip = UNASSIGNED_IP_ADDR self.local_ip = UNASSIGNED_IP_ADDR self.subnet_mask = UNASSIGNED_IP_ADDR + self.dns_server_ip = UNASSIGNED_IP_ADDR self._renew = False self._retries = 0 self._increment_transaction_id() self._start_time = int(time.monotonic()) - def _increment_transaction_id(self): + def _socket_release(self) -> None: + """Close the socket if it exists.""" + if self._sock: + self._sock.close() + self._sock = None + + def _socket_setup(self, timeout: int = 5) -> None: + """I'll get to it.""" + self._socket_release() + stop_time = self._retry_time(interval=timeout) + while not time.monotonic() > stop_time: + try: + self._sock = socket.socket(type=socket.SOCK_DGRAM) + except RuntimeError: + if self._debug: + print("DHCP client failed to allocate socket") + if self._blocking: + print("Retrying…") + else: + return + else: + self._sock.settimeout(self._response_timeout) + self._sock.bind((None, 68)) + self._sock.connect((self.dhcp_server_ip, DHCP_SERVER_PORT)) + return + raise RuntimeError( + "DHCP client failed to allocate socket. Retried for {} seconds.".format( + timeout + ) + ) + + def _increment_transaction_id(self) -> None: + """I'll get to it.""" self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF - def _resend_time(self) -> float: + def _retry_time(self, *, interval: int = 4, exponential: bool = True) -> int: """I'll get to it""" self._retries += 1 - return self._retries * 4 + randint(0, 2) + int(time.monotonic()) + if exponential: + delay = int(self._retries * interval + randint(0, 2) + time.monotonic()) + else: + delay = interval + return delay + int(time.monotonic()) - def _set_next_state( - self, *, next_state: int, max_retries: int, send_msg: bool = True + def _send_message_set_next_state( + self, + *, + message_type: int, + next_state: int, + max_retries: int, ) -> None: """I'll get to it""" - if send_msg: - self._sock.send(_BUFF) + self._generate_dhcp_message(message_type=message_type) + self._sock.send(_BUFF) self._retries = 0 self._max_retries = max_retries - self._next_resend = self._resend_time() + self._next_resend = self._retry_time() self._dhcp_state = next_state - def _ip_collision(self) -> bool: - """I'll get to it""" - if not self._renew: # Don't check if the lease is being renewed. - timeout = 0.25 - arp_packet = bytearray(b"0x00" * 28) - # Set ARP headers - arp_packet[:8] = (0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01) - arp_packet[8:14] = self._mac_address - arp_packet[24:] = self.local_ip - for _ in range(3): - self._sock.send(arp_packet) - stop_time = time.monotonic() + timeout - while time.monotonic() < stop_time: - if self._sock.available(): - buffer = self._sock.recv() - if ( - tuple(buffer[18:24]) == self._mac_address - and tuple(buffer[14:18]) == self.local_ip - ): - # Another device is already using this IP address. - return True - # No response to ARP request or renewing, accept this IP address. - return False - - def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: + def _handle_dhcp_message(self) -> None: + while True: + while time.monotonic() < self._next_resend: + if self._sock.available(): + _BUFF = self._sock.recv() + try: + msg_type = self._parse_dhcp_response() + except ValueError as error: + if self._debug: + print(error) + else: + if msg_type == DHCP_OFFER: + self._send_message_set_next_state( + message_type=DHCP_REQUEST, + next_state=STATE_REQUESTING, + max_retries=3, + ) + return + if msg_type == DHCP_NAK: + self._dhcp_state = STATE_INIT + return + if msg_type == DHCP_ACK: + if self._lease_time == 0: + self._lease_time = DEFAULT_LEASE_TIME + self._t1 = self._start_time + self._lease_time // 2 + self._t2 = ( + self._start_time + + self._lease_time + - self._lease_time // 8 + ) + self._lease_time += self._start_time + self._increment_transaction_id() + self._renew = False + self._sock.close() + self._sock = None + self._dhcp_state = STATE_BOUND + return + if not self._blocking: + return + self._next_resend = self._retry_time() + if self._retries > self._max_retries: + raise RuntimeError( + "No response from DHCP server after {}".format(self._max_retries) + ) + if not self._blocking: + return + + def _dhcp_state_machine(self, *, blocking: bool = False) -> None: """I'll get to it""" global _BUFF # pylint: disable=global-variable-not-assigned, global-statement self._blocking = blocking - # pylint: disable=too-many-nested-blocks while True: if self._dhcp_state == STATE_BOUND: now = time.monotonic() @@ -406,98 +443,40 @@ def _new_dhcp_state_machine(self, *, blocking: bool = False) -> None: if self._dhcp_state == STATE_RENEWING: self._renew = True - self._generate_dhcp_message(message_type=DHCP_REQUEST) - self._set_next_state(next_state=STATE_REQUESTING, max_retries=3) + self._socket_setup() + self._start_time = time.monotonic() + self._send_message_set_next_state( + message_type=DHCP_REQUEST, + next_state=STATE_REQUESTING, + max_retries=3, + ) if self._dhcp_state == STATE_REBINDING: - self.dhcp_server_ip = BROADCAST_SERVER_ADDR self._renew = True - self._generate_dhcp_message(message_type=DHCP_REQUEST) - self._set_next_state(next_state=STATE_REQUESTING, max_retries=3) + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + self._socket_setup() + self._send_message_set_next_state( + message_type=DHCP_REQUEST, + next_state=STATE_REQUESTING, + max_retries=3, + ) if self._dhcp_state == STATE_INIT: self._dsm_reset() - wait_for_link = 5 - time_to_stop = time.monotonic() + wait_for_link - while not self._eth.link_status: - if time.monotonic() > time_to_stop: - raise TimeoutError("Ethernet link is down") - time.sleep(0.5) - self._generate_dhcp_message(message_type=DHCP_DISCOVER) - self._set_next_state(next_state=STATE_SELECTING, max_retries=3) + self._send_message_set_next_state( + message_type=DHCP_DISCOVER, + next_state=STATE_SELECTING, + max_retries=3, + ) if self._dhcp_state == STATE_SELECTING: - while True: - while time.monotonic() < self._next_resend: - if self._sock.available(): - _BUFF = self._sock.recv() - try: - msg_type = self._parse_dhcp_response() - except ValueError as error: - if self._debug: - print(error) - else: - if msg_type == DHCP_OFFER: - self._generate_dhcp_message( - message_type=DHCP_REQUEST - ) - self._set_next_state( - next_state=STATE_REQUESTING, max_retries=3 - ) - break - if not self._blocking: - break - self._next_resend = self._resend_time() - if not self._blocking: - break - - if self._dhcp_state == STATE_REQUESTING: - while True: - while time.monotonic() < self._next_resend: - if self._sock.available(): - _BUFF = self._sock.recv() - try: - msg_type = self._parse_dhcp_response() - except ValueError as error: - if self._debug: - print(error) - else: - if msg_type == DHCP_NAK: - self._set_next_state( - next_state=STATE_INIT, max_retries=0 - ) - break - if msg_type == DHCP_ACK: - if self._ip_collision(): - self._generate_dhcp_message( - message_type=DHCP_DECLINE - ) - self._set_next_state( - next_state=STATE_INIT, max_retries=0 - ) - else: - if self._lease_time == 0: - self._lease_time = DEFAULT_LEASE_TIME - self._t1 = ( - self._start_time + self._lease_time // 2 - ) - self._t2 = ( - self._start_time - + self._lease_time - - self._lease_time // 8 - ) - self._lease_time += self._start_time - self._increment_transaction_id() - self._renew = False - self._blocking = False - self._dhcp_state = STATE_BOUND - if msg_type == DHCP_NAK: - self._dhcp_state = STATE_INIT - if not self._blocking: - break - self._next_resend = self._resend_time() - if not self._blocking: - break + self._handle_dhcp_message() + + if self._dhcp_state == STATE_REQUESTING: + self._handle_dhcp_message() + if not self._blocking: + break + self._blocking = False def _generate_dhcp_message( self, @@ -507,13 +486,13 @@ def _generate_dhcp_message( renew: bool = False, ) -> None: """ - Assemble a DHCP message packet. + Assemble a DHCP message. The content will vary depending on which type of + message is being sent and whether the lease is new or being renewed. - :param int time_elapsed: Time in seconds since transaction began. - :param float time_elapsed: Number of seconds elapsed since DHCP process started. - :param bool renew: Set True for renew and rebind, defaults to False. + :param int message_type: Type of message to generate. :param bool broadcast: Used to set the flag requiring a broadcast reply from the DHCP server. + :param bool renew: Set True for renew and rebind, defaults to False. """ def option_data( diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py index ac93fa9..6c4b190 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp.py @@ -1,9 +1,13 @@ # SPDX-FileCopyrightText: 2022 Martin Stephens # # SPDX-License-Identifier: MIT -"""Tests to confirm that there are no changes in behaviour to public methods and functions.""" +"""Tests to confirm that there are no changes in behaviour to methods and functions. +These test are not exhaustive, but are a sanity check while making changes to the module.""" +import time + # pylint: disable=no-self-use, redefined-outer-name, protected-access, invalid-name, too-many-arguments import pytest +from freezegun import freeze_time from micropython import const import dummy_dhcp_data as dhcp_data import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as wiz_dhcp @@ -139,15 +143,15 @@ def test_dhcp_setup_other_args(self, wiznet): ) +@freeze_time("2022-10-20") class TestSendDHCPMessage: def test_generate_message_discover_with_defaults(self, wiznet, wrench): assert len(wiz_dhcp._BUFF) == 318 dhcp_client = wiz_dhcp.DHCP(wiznet, (4, 5, 6, 7, 8, 9)) dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) dhcp_client._transaction_id = 0x6FFFFFFF - dhcp_client._generate_dhcp_message( - message_type=wiz_dhcp.DHCP_DISCOVER, time_elapsed=23.4 - ) + dhcp_client._start_time = time.monotonic() - 23.4 + dhcp_client._generate_dhcp_message(message_type=wiz_dhcp.DHCP_DISCOVER) assert wiz_dhcp._BUFF == dhcp_data.DHCP_SEND_01 assert len(wiz_dhcp._BUFF) == 318 @@ -208,10 +212,10 @@ def test_generate_dhcp_message_discover_with_non_defaults( dhcp_client.local_ip = local_ip dhcp_client.dhcp_server_ip = server_ip dhcp_client._transaction_id = 0x6FFFFFFF + dhcp_client._start_time = time.monotonic() - time_elapsed # Test dhcp_client._generate_dhcp_message( message_type=msg_type, - time_elapsed=time_elapsed, renew=renew, broadcast=broadcast_only, ) @@ -264,12 +268,10 @@ def test_generate_dhcp_message_with_request_options( dhcp_client.local_ip = local_ip dhcp_client.dhcp_server_ip = server_ip dhcp_client._transaction_id = 0x6FFFFFFF + dhcp_client._start_time = time.monotonic() - time_elapsed # Test dhcp_client._generate_dhcp_message( - message_type=msg_type, - time_elapsed=time_elapsed, - renew=renew, - broadcast=broadcast_only, + message_type=msg_type, renew=renew, broadcast=broadcast_only ) assert len(wiz_dhcp._BUFF) == 318 assert wiz_dhcp._BUFF == result @@ -363,3 +365,50 @@ def test_parsing_failures(self, wiznet, wrench): bad_data[236] = 0 with pytest.raises(ValueError): dhcp_client._parse_dhcp_response() + + +class TestResetDsmReset: + def test_socket_reset(self, wiznet, wrench): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + + dhcp_client._dsm_reset() + assert dhcp_client._sock is None + + dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) + + dhcp_client._dsm_reset() + assert dhcp_client._sock is None + + @freeze_time("2022-11-10") + def test_reset_dsm_parameters(self, wiznet): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client.dhcp_server_ip = (1, 2, 3, 4) + dhcp_client.local_ip = (2, 3, 4, 5) + dhcp_client.subnet_mask = (3, 4, 5, 6) + dhcp_client.dns_server_ip = (7, 8, 8, 10) + dhcp_client._renew = True + dhcp_client._retries = 4 + dhcp_client._transaction_id = 3 + dhcp_client._start_time = None + + dhcp_client._dsm_reset() + + assert dhcp_client.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR + assert dhcp_client.local_ip == wiz_dhcp.UNASSIGNED_IP_ADDR + assert dhcp_client.subnet_mask == wiz_dhcp.UNASSIGNED_IP_ADDR + assert dhcp_client.dns_server_ip == wiz_dhcp.UNASSIGNED_IP_ADDR + assert dhcp_client._renew is False + assert dhcp_client._retries == 0 + assert dhcp_client._transaction_id == 4 + assert dhcp_client._start_time == time.monotonic() + + +class TestSocketRelease: + def test_socket_set_to_none(self, wiznet, wrench): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client._socket_release() + assert dhcp_client._sock is None + + dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) + dhcp_client._socket_release() + assert dhcp_client._sock is None From 861c30aae3c122c0e2083839d23cb4d365ee43e6 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 19 Dec 2022 12:23:15 +0300 Subject: [PATCH 07/80] Refactored dummy data, added freezegun to opt_reqs, added some tests. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 44 ++-- optional_requirements.txt | 2 + tests/dummy_dhcp_data.py | 236 ++++++-------------- tests/test_dhcp.py | 44 ++-- 4 files changed, 120 insertions(+), 206 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 131a07f..92af830 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -355,7 +355,7 @@ def _retry_time(self, *, interval: int = 4, exponential: bool = True) -> int: """I'll get to it""" self._retries += 1 if exponential: - delay = int(self._retries * interval + randint(0, 2) + time.monotonic()) + delay = int(self._retries * interval + randint(-1, 1) + time.monotonic()) else: delay = interval return delay + int(time.monotonic()) @@ -386,31 +386,35 @@ def _handle_dhcp_message(self) -> None: if self._debug: print(error) else: - if msg_type == DHCP_OFFER: + if ( + self._dhcp_state == STATE_SELECTING + and msg_type == DHCP_OFFER + ): self._send_message_set_next_state( message_type=DHCP_REQUEST, next_state=STATE_REQUESTING, max_retries=3, ) return - if msg_type == DHCP_NAK: - self._dhcp_state = STATE_INIT - return - if msg_type == DHCP_ACK: - if self._lease_time == 0: - self._lease_time = DEFAULT_LEASE_TIME - self._t1 = self._start_time + self._lease_time // 2 - self._t2 = ( - self._start_time - + self._lease_time - - self._lease_time // 8 - ) - self._lease_time += self._start_time - self._increment_transaction_id() - self._renew = False - self._sock.close() - self._sock = None - self._dhcp_state = STATE_BOUND + if self._dhcp_state == STATE_REQUESTING: + if msg_type == DHCP_NAK: + self._dhcp_state = STATE_INIT + return + if msg_type == DHCP_ACK: + if self._lease_time == 0: + self._lease_time = DEFAULT_LEASE_TIME + self._t1 = self._start_time + self._lease_time // 2 + self._t2 = ( + self._start_time + + self._lease_time + - self._lease_time // 8 + ) + self._lease_time += self._start_time + self._increment_transaction_id() + self._renew = False + self._sock.close() + self._sock = None + self._dhcp_state = STATE_BOUND return if not self._blocking: return diff --git a/optional_requirements.txt b/optional_requirements.txt index d4e27c4..09a40c7 100644 --- a/optional_requirements.txt +++ b/optional_requirements.txt @@ -1,3 +1,5 @@ # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries # # SPDX-License-Identifier: Unlicense +pytest +freezegun diff --git a/tests/dummy_dhcp_data.py b/tests/dummy_dhcp_data.py index b9caa82..c457ca2 100644 --- a/tests/dummy_dhcp_data.py +++ b/tests/dummy_dhcp_data.py @@ -3,201 +3,109 @@ # SPDX-License-Identifier: MIT """Data for use in test_dhcp.py""" + +def _pad_message(message_section: bytearray, target_length: int) -> bytearray: + """Pad the message with 0x00.""" + return message_section + bytearray(b"\00" * (target_length - len(message_section))) + + +def _build_message(message_body: bytearray, message_options: bytearray) -> bytearray: + """Assemble the padded message and body to make a 318 byte packet. The 'header' + section must be 236 bytes and the entire message must be 318 bytes.""" + dhcp_message = _pad_message(message_body, 236) + _pad_message(message_options, 82) + assert len(dhcp_message) == 318 + return dhcp_message + + # Data for testing send data. # DHCP DISCOVER messages. # Default settings (DISCOVER, broadcast=False, default hostname, renew=False) -DHCP_SEND_01 = bytearray( +message = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00\x17\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" - b"\x08\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc" - b"5\x01\x01\x0c\x12WIZnet040506070809\xff\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00" + b"\x08\t\x00\x00\x00\x00\x00\x00\x00\x00" ) +options = bytearray(b"c\x82Sc5\x01\x01\x0c\x12WIZnet040506070809\xff") +DHCP_SEND_01 = _build_message(message, options) -DHCP_SEND_02 = bytearray( +message = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00\x17\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" - b"\x08\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc" - b"5\x01\x01\x0c\x12WIZnet040506070809\xff\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00" + b"\x08\t" ) -DHCP_SEND_03 = bytearray( +options = bytearray(b"c\x82Sc5\x01\x01\x0c\x12WIZnet040506070809\xff") +DHCP_SEND_02 = _build_message(message, options) + +message = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00#\x80\x00\xc0\xa8\x03\x04" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18#.9DO\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x01\x0c\x04ber" - b"t\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18#.9DO" ) +options = bytearray(b"c\x82Sc5\x01\x01\x0c\x04bert\xff") +DHCP_SEND_03 = _build_message(message, options) -DHCP_SEND_04 = bytearray( +message = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00#\x80\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x01\x0c\x05cla" - b"sh\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c" ) +options = bytearray(b"c\x82Sc5\x01\x01\x0c\x05clash\xff") +DHCP_SEND_04 = _build_message(message, options) # DHCP REQUEST messages. -DHCP_SEND_05 = bytearray( +message = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00\x10\x80\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x03\x0c\nhelicopter7" - b"\x03\x01\x03\x062\x04\n\n\n+6\x04\x91B-\x16\xff\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c" +) + +options = bytearray( + b"c\x82Sc5\x01\x03\x0c\nhelicopter7\x03\x01\x03\x062" + b"\x04\n\n\n+6\x04\x91B-\x16\xff\x00\x00\x00" ) +DHCP_SEND_05 = _build_message(message, options) -DHCP_SEND_06 = bytearray( +message = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00H\x80\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00K?\xa6\x04" - b"\xc8e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc" - b"5\x01\x03\x0c\x12WIZnet4B3FA604C8657\x03\x01\x03\x062\x04de" - b"f\x046\x04\xf5\xa6\x05\x0b\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\xc8e" ) +options = bytearray( + b"c\x82Sc5\x01\x03\x0c\x12WIZnet4B3FA604C8657\x03\x01\x03" + b"\x062\x04def\x046\x04\xf5\xa6\x05\x0b\xff" +) +DHCP_SEND_06 = _build_message(message, options) + # Data to test response parser. # Basic case, no extra fields, one each of router and DNS. -GOOD_DATA_01 = bytearray( +message = bytearray( b"\x02\x00\x00\x00\x7f\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xc0" - b"\xa8\x05\x16\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x05\x07\t\x0b\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01" - b"\x02\x01\x04\xc0\xa8\x06\x026\x04\xeao\xde{3\x04\x00\x01\x01\x00\x03" - b'\x04yy\x04\x05\x06\x04\x05\x06\x07\x08:\x04\x00""\x00;\x04\x0033\x00' - b"\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\xa8\x05\x16\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x05\x07\t\x0b" +) + +options = bytearray( + b"c\x82Sc5\x01\x02\x01\x04\xc0\xa8\x06\x026\x04\xeao\xde" + b"{3\x04\x00\x01\x01\x00\x03\x04yy\x04\x05\x06\x04\x05\x06" + b'\x07\x08:\x04\x00""\x00;\x04\x0033\x00\xff' ) -# Complex case, extra field, 2 each router and DNS. -GOOD_DATA_02 = bytearray( +GOOD_DATA_01 = _build_message(message, options) + +# Complex case, extra field, 2 routers and 2 DNS servers. +message = bytearray( b"\x02\x00\x00\x004Vx\x9a\x00\x00\x00\x00\x00\x00\x00\x00\x12$@\n\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5" - b"\x01\x05<\x05\x01\x02\x03\x04\x05\x01\x04\n\x0b\x07\xde6\x04zN\x91\x03\x03" - b"\x08\n\x0b\x0e\x0f\xff\x00\xff\x00\x06\x08\x13\x11\x0b\x07****3\x04\x00\x00" - b"=;:\x04\x00\x0e\x17@;\x04\x02\x92]\xde\xff\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x01" +) +options = bytearray( + b"c\x82Sc5\x01\x05<\x05\x01\x02\x03\x04\x05\x01\x04\n\x0b" + b"\x07\xde6\x04zN\x91\x03\x03\x08\n\x0b\x0e\x0f\xff\x00" + b"\xff\x00\x06\x08\x13\x11\x0b\x07****3\x04\x00\x00=;:\x04" + b"\x00\x0e\x17@;\x04\x02\x92]\xde\xff" ) +GOOD_DATA_02 = _build_message(message, options) + # -BAD_DATA = bytearray( +message = bytearray( b"\x02\x00\x00\x00\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x12$@\n\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc" + b"\x00\x00\x00\x00\x00\x00\x01" ) +options = bytearray(b"c\x82Sc") +BAD_DATA = _build_message(message, options) diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py index 6c4b190..a5fadb5 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp.py @@ -18,31 +18,29 @@ @pytest.fixture def wiznet(mocker): + """Mock WIZNET5K so that the DHCP class can be tested without hardware.""" return mocker.patch("adafruit_wiznet5k.adafruit_wiznet5k.WIZNET5K", autospec=True) @pytest.fixture def wrench(mocker): + """Mock socket module to allow test data to be read and written by the DHCP module.""" return mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket", autospec=True ) -@pytest.mark.skip() class TestDHCPInit: def test_constants(self): + """Test all the constants in the DHCP module.""" # DHCP State Machine - assert wiz_dhcp.STATE_DHCP_START == const(0x00) - assert wiz_dhcp.STATE_DHCP_DISCOVER == const(0x01) - assert wiz_dhcp.STATE_DHCP_REQUEST == const(0x02) - assert wiz_dhcp.STATE_DHCP_LEASED == const(0x03) - assert wiz_dhcp.STATE_DHCP_REREQUEST == const(0x04) - assert wiz_dhcp.STATE_DHCP_RELEASE == const(0x05) - assert wiz_dhcp.STATE_DHCP_WAIT == const(0x06) - assert wiz_dhcp.STATE_DHCP_DISCONN == const(0x07) - - # DHCP wait time between attempts - assert wiz_dhcp.DHCP_WAIT_TIME == const(60) + assert wiz_dhcp.STATE_INIT == const(0x01) + assert wiz_dhcp.STATE_SELECTING == const(0x02) + assert wiz_dhcp.STATE_REQUESTING == const(0x03) + assert wiz_dhcp.STATE_BOUND == const(0x04) + assert wiz_dhcp.STATE_RENEWING == const(0x05) + assert wiz_dhcp.STATE_REBINDING == const(0x06) + assert wiz_dhcp.STATE_RELEASING == const(0x07) # DHCP Message Types assert wiz_dhcp.DHCP_DISCOVER == const(1) @@ -72,6 +70,7 @@ def test_constants(self): # DHCP Lease Time, in seconds assert wiz_dhcp.DEFAULT_LEASE_TIME == const(900) assert wiz_dhcp.BROADCAST_SERVER_ADDR == (255, 255, 255, 255) + assert wiz_dhcp.UNASSIGNED_IP_ADDR == (0, 0, 0, 0) # DHCP Response Options assert wiz_dhcp.MSG_TYPE == 53 @@ -96,6 +95,7 @@ def test_constants(self): ), ) def test_dhcp_setup_default(self, mocker, wiznet, wrench, mac_address): + """Test intial settings from DHCP.__init__.""" # Test with mac address as tuple, list and bytes with default values. mock_randint = mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", autospec=True @@ -108,20 +108,16 @@ def test_dhcp_setup_default(self, mocker, wiznet, wrench, mac_address): assert dhcp_client._mac_address == mac_address wrench.set_interface.assert_called_once_with(wiznet) assert dhcp_client._sock is None - assert dhcp_client._dhcp_state == wiz_dhcp.STATE_DHCP_START - assert dhcp_client._initial_xid == 0 + assert dhcp_client._dhcp_state == wiz_dhcp.STATE_INIT mock_randint.assert_called_once() assert dhcp_client._transaction_id == 0x1234567 assert dhcp_client._start_time == 0 assert dhcp_client.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR - assert dhcp_client.local_ip == 0 - assert dhcp_client.gateway_ip == 0 - assert dhcp_client.subnet_mask == 0 - assert dhcp_client.dns_server_ip == 0 + assert dhcp_client.local_ip == wiz_dhcp.UNASSIGNED_IP_ADDR + assert dhcp_client.gateway_ip == wiz_dhcp.UNASSIGNED_IP_ADDR + assert dhcp_client.subnet_mask == wiz_dhcp.UNASSIGNED_IP_ADDR + assert dhcp_client.dns_server_ip == wiz_dhcp.UNASSIGNED_IP_ADDR assert dhcp_client._lease_time == 0 - assert dhcp_client._last_lease_time == 0 - assert dhcp_client._renew_in_sec == 0 - assert dhcp_client._rebind_in_sec == 0 assert dhcp_client._t1 == 0 assert dhcp_client._t2 == 0 mac_string = "".join("{:02X}".format(o) for o in mac_address) @@ -130,6 +126,7 @@ def test_dhcp_setup_default(self, mocker, wiznet, wrench, mac_address): ) def test_dhcp_setup_other_args(self, wiznet): + """Test instantiating DHCP with none default values.""" mac_address = (7, 8, 9, 10, 11, 12) dhcp_client = wiz_dhcp.DHCP( wiznet, mac_address, hostname="fred.com", response_timeout=25.0, debug=True @@ -145,7 +142,8 @@ def test_dhcp_setup_other_args(self, wiznet): @freeze_time("2022-10-20") class TestSendDHCPMessage: - def test_generate_message_discover_with_defaults(self, wiznet, wrench): + def test_generate_message_with_default_attributes(self, wiznet, wrench): + """Test the _generate_message function with default values.""" assert len(wiz_dhcp._BUFF) == 318 dhcp_client = wiz_dhcp.DHCP(wiznet, (4, 5, 6, 7, 8, 9)) dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) @@ -207,6 +205,8 @@ def test_generate_dhcp_message_discover_with_non_defaults( server_ip, result, ): + """Test the generate_dhcp_message function with different message types and + none default attributes.""" dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address, hostname=hostname) # Set client attributes for test dhcp_client.local_ip = local_ip From 5435203877c283d1fd350bee3084fe34964d66c5 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 19 Dec 2022 17:42:30 +0300 Subject: [PATCH 08/80] Added some tests. Changed _BUFF reset in _generate_dhcp_message to a fixed length, other small changes. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 340 ++++++++++---------- tests/test_dhcp.py | 50 ++- 2 files changed, 225 insertions(+), 165 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 92af830..77c1ff9 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -80,7 +80,8 @@ OPT_END = 255 # Packet buffer -_BUFF = bytearray(318) +BUFF_LENGTH = 318 +_BUFF = bytearray(BUFF_LENGTH) class DHCP: @@ -118,7 +119,6 @@ def __init__( # DHCP state machine self._dhcp_state = STATE_INIT - # self._initial_xid = 0 self._transaction_id = randint(1, 0x7FFFFFFF) self._start_time = 0 self._next_resend = 0 @@ -136,7 +136,6 @@ def __init__( # Lease configuration self._lease_time = 0 - # self._last_lease_time = 0 self._t1 = 0 self._t2 = 0 @@ -146,147 +145,6 @@ def __init__( (hostname or "WIZnet{}").split(".")[0].format(mac_string)[:42], "utf-8" ) - # # pylint: disable=too-many-branches, too-many-statements - # def _dhcp_state_machine(self) -> None: - # """ - # DHCP state machine without wait loops to enable cooperative multitasking. - # 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: 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: 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 = None, None # 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, "big"): - # 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: - # 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 = None, None # 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, "big"): - # 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: - # 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 in ( - # STATE_DHCP_DISCOVER, - # 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) -> bool: """Request to renew or acquire a DHCP lease.""" self._dhcp_state_machine(blocking=True) @@ -325,7 +183,7 @@ def _socket_release(self) -> None: def _socket_setup(self, timeout: int = 5) -> None: """I'll get to it.""" self._socket_release() - stop_time = self._retry_time(interval=timeout) + stop_time = time.monotonic() + timeout while not time.monotonic() > stop_time: try: self._sock = socket.socket(type=socket.SOCK_DGRAM) @@ -348,17 +206,29 @@ def _socket_setup(self, timeout: int = 5) -> None: ) def _increment_transaction_id(self) -> None: - """I'll get to it.""" + """Increment the transaction ID and roll over from 0x7fffffff to 0.""" self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF - def _retry_time(self, *, interval: int = 4, exponential: bool = True) -> int: - """I'll get to it""" + def _next_retry_time(self, *, interval: int = 4) -> int: + """Calculate a retry stop time. + + The interval is calculated as an exponential fallback with a random variation to + prevent DHCP packet collisions. This timeout is intended to be compared with + time.monotonic(). Uses self._retries as the exponent, and increments this value + each time it is called. + + :param int interval: The base retry interval in seconds. Defaults to 4 as per the + DHCP standard for Ethernet connections. + + :returns int: The timeout in time.monotonic() seconds. + + :raises ValueError: If the calculated interval is < 1 second. + """ + delay = int(2**self._retries * interval + randint(-1, 1) + time.monotonic()) + if delay < 1: + raise ValueError("Retry delay must be >= 1 second") self._retries += 1 - if exponential: - delay = int(self._retries * interval + randint(-1, 1) + time.monotonic()) - else: - delay = interval - return delay + int(time.monotonic()) + return delay def _send_message_set_next_state( self, @@ -372,10 +242,11 @@ def _send_message_set_next_state( self._sock.send(_BUFF) self._retries = 0 self._max_retries = max_retries - self._next_resend = self._retry_time() + self._next_resend = self._next_retry_time() self._dhcp_state = next_state def _handle_dhcp_message(self) -> None: + # pylint: disable=too-many-branches while True: while time.monotonic() < self._next_resend: if self._sock.available(): @@ -418,7 +289,7 @@ def _handle_dhcp_message(self) -> None: return if not self._blocking: return - self._next_resend = self._retry_time() + self._next_resend = self._next_retry_time() if self._retries > self._max_retries: raise RuntimeError( "No response from DHCP server after {}".format(self._max_retries) @@ -432,7 +303,7 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: global _BUFF # pylint: disable=global-variable-not-assigned, global-statement self._blocking = blocking - while True: + while self._eth.link_status: if self._dhcp_state == STATE_BOUND: now = time.monotonic() if now < self._t1: @@ -474,9 +345,11 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: ) if self._dhcp_state == STATE_SELECTING: + self._max_retries = 3 self._handle_dhcp_message() if self._dhcp_state == STATE_REQUESTING: + self._max_retries = 3 self._handle_dhcp_message() if not self._blocking: break @@ -495,8 +368,8 @@ def _generate_dhcp_message( :param int message_type: Type of message to generate. :param bool broadcast: Used to set the flag requiring a broadcast reply from the - DHCP server. - :param bool renew: Set True for renew and rebind, defaults to False. + DHCP server. Defaults to False to match DHCP standard. + :param bool renew: Set True for renewing and rebinding operations, defaults to False. """ def option_data( @@ -505,7 +378,7 @@ def option_data( """Helper function to set DHCP option data for a DHCP message. - :param int pointer: Pointer to start of DHCP option. + :param int pointer: Pointer to start of a DHCP option. :param int option_code: Type of option to add. :param Tuple[int] option_data: The data for the option. @@ -521,7 +394,7 @@ def option_data( _BUFF[pointer:data_end] = option_data return data_end - _BUFF[:] = b"\x00" * len(_BUFF) + _BUFF[:] = b"\x00" * BUFF_LENGTH # OP.HTYPE.HLEN.HOPS _BUFF[0:4] = (DHCP_BOOT_REQUEST, DHCP_HTYPE10MB, DHCP_HLENETHERNET, DHCP_HOPS) # Transaction ID (xid) @@ -571,15 +444,15 @@ def _parse_dhcp_response( :return Tuple[int, bytearray]: DHCP packet type and ID. """ - + # pylint: disable=too-many-branches def option_data(pointer: int) -> Tuple[int, int, bytes]: """Helper function to extract DHCP option data from a response. - :param int pointer: Pointer to start of DHCP option. + :param int pointer: Pointer to start of a DHCP option. :returns Tuple[int, int, bytes]: Pointer to next option, - option type and option data. + option type, and option data. """ global _BUFF # pylint: disable=global-variable-not-assigned data_type = _BUFF[pointer] @@ -651,3 +524,144 @@ def option_data(pointer: int) -> Tuple[int, int, bytes]: if msg_type is None: raise ValueError("No valid message type in response.") return msg_type + + # # pylint: disable=too-many-branches, too-many-statements + # def _dhcp_state_machine(self) -> None: + # """ + # DHCP state machine without wait loops to enable cooperative multitasking. + # 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: 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: 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 = None, None # 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, "big"): + # 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: + # 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 = None, None # 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, "big"): + # 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: + # 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 in ( + # STATE_DHCP_DISCOVER, + # 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 diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py index a5fadb5..df7dd94 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp.py @@ -84,6 +84,7 @@ def test_constants(self): assert wiz_dhcp.OPT_END == 255 # Packet buffer + assert wiz_dhcp.BUFF_LENGTH == 318 assert wiz_dhcp._BUFF == bytearray(318) @pytest.mark.parametrize( @@ -352,7 +353,6 @@ def test_parsing_failures(self, wiznet, wrench): # Bad OP code. bad_data[0] = 0 dhcp_client._transaction_id = 0x7FFFFFFF - dhcp_client._initial_xid = dhcp_client._transaction_id.to_bytes(4, "little") with pytest.raises(ValueError): dhcp_client._parse_dhcp_response() bad_data[0] = 2 # Reset to good value @@ -360,7 +360,7 @@ def test_parsing_failures(self, wiznet, wrench): bad_data[28:34] = (0, 0, 0, 0, 0, 0) with pytest.raises(ValueError): dhcp_client._parse_dhcp_response() - bad_data[28:34] = (1, 1, 1, 1, 1, 1) # Reset to good value + bad_data[28:34] = (1, 1, 1, 1, 1, 1) # Reset to a good value for next test. # Bad Magic Cookie. bad_data[236] = 0 with pytest.raises(ValueError): @@ -412,3 +412,49 @@ def test_socket_set_to_none(self, wiznet, wrench): dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) dhcp_client._socket_release() assert dhcp_client._sock is None + + +class TestSmallHelperFunctions: + def test_increment_transaction_id(self, wiznet): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + # Test that transaction_id increments. + dhcp_client._transaction_id = 4 + dhcp_client._increment_transaction_id() + assert dhcp_client._transaction_id == 5 + # Test that transaction_id rolls over from 0x7fffffff to zero + dhcp_client._transaction_id = 0x7FFFFFFF + dhcp_client._increment_transaction_id() + assert dhcp_client._transaction_id == 0 + + @freeze_time("2022-10-10") + @pytest.mark.parametrize("rand_int", (-1, 0, 1)) + def test_next_retry_time_default_attrs(self, mocker, wiznet, rand_int): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + mocker.patch( + "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", + autospec=True, + return_value=rand_int, + ) + now = time.monotonic() + for retry in range(3): + dhcp_client._retries = retry + assert dhcp_client._next_retry_time() == int( + 2**retry * 4 + rand_int + now + ) + assert dhcp_client._retries == retry + 1 + + @freeze_time("2022-10-10") + @pytest.mark.parametrize("interval", (2, 7, 10)) + def test_next_retry_time_optional_attrs(self, mocker, wiznet, interval): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + mocker.patch( + "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", + autospec=True, + return_value=0, + ) + now = time.monotonic() + for retry in range(3): + dhcp_client._retries = retry + assert dhcp_client._next_retry_time(interval=interval) == int( + 2**retry * interval + now + ) From b6afd52186c25c3a6e331a4b9b518cff49e88040 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 19 Dec 2022 20:06:01 +0300 Subject: [PATCH 09/80] Removed unused releasing state. Added doc string to DHCP. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 50 ++++++++++++++++++--- tests/test_dhcp.py | 1 - 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 77c1ff9..0739518 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -10,7 +10,7 @@ Pure-Python implementation of Jordan Terrell's DHCP library v0.3 -* Author(s): Jordan Terrell, Brent Rubell +* Author(s): Jordan Terrell, Brent Rubell, Martin Stephens """ from __future__ import annotations @@ -36,7 +36,6 @@ STATE_BOUND = const(0x04) STATE_RENEWING = const(0x05) STATE_REBINDING = const(0x06) -STATE_RELEASING = const(0x07) # DHCP Message Types DHCP_DISCOVER = const(1) @@ -85,7 +84,40 @@ class DHCP: - """W5k DHCP Client implementation.""" + """Wiznet5k DHCP Client. + + Implements a DHCP client using a finite state machine (FSM). This allows the DHCP client + to run in a non-blocking mode suitable for CircuitPython. + + The DHCP client obtains a lease and maintains it. The process of obtaining the initial + lease is best run in a blocking mode, as several messages must be exchanged with the DHCP + server. Once the lease has been allocated, lease maintenance can be performed in + non-blocking mode as nothing needs to be done until it is time to reallocate the + lease. Renewing or rebinding is a simpler process which may be repeated periodically + until successful. If the lease expires, the client attempts to obtain a new lease in + blocking mode when the maintenance routine is run. + + In most circumstances, call `DHCP.request_lease` in blocking mode to obtain a + lease, then periodically call `DHCP.maintain_lease` in non-blocking mode so that the + FSM can check whether the lease needs to be renewed, and can then renew it. + + Since DHCP uses UDP, messages may be lost. The DHCP protocol uses exponential backoff + for retrying. Retries occur after 4, 8, and 16 seconds (the final retry isfollowed by + a wait of 32 seconds) so it will take about a minute to decide that no DHCP server + is available. + + Use of DHCP relay agents is not implemented. The DHCP server must be on the same + physical network as the client. + + The DHCP client cannot check whether the allocated IP address is already in use because + the ARP protocol is not available. Therefore, it is possible that the IP address has been + statically assigned to another device. In most cases, the DHCP server will make this + check before allocating an address, but some do not. + + The DHCPRELEASE message is not implemented. The DHCP protocol does not require it and + DHCP servers can handle disappearing clients and clients that ask for 'replacement' + IP addressed. + """ # pylint: disable=too-many-arguments, too-many-instance-attributes, invalid-name def __init__( @@ -442,7 +474,15 @@ def _parse_dhcp_response( ) -> int: """Parse DHCP response from DHCP server. - :return Tuple[int, bytearray]: DHCP packet type and ID. + Check that the message is for this client. Extract data from the fixed positions + in the first 236 bytes of the message, then cycle through the options for + additional data. + + :returns Tuple[int, bytearray]: DHCP packet type and ID. + + :raises ValueError: Checks that the message is a reply, the transaction ID + matches, a client ID exists and the 'magic cookie' is set. If any of these tests + fail or no message type is found in the options, raises a ValueError. """ # pylint: disable=too-many-branches def option_data(pointer: int) -> Tuple[int, int, bytes]: @@ -475,7 +515,7 @@ def option_data(pointer: int) -> Tuple[int, int, bytes]: self.local_ip = tuple(_BUFF[16:20]) # Check that there is a client ID. if _BUFF[28:34] == b"\x00\x00\x00\x00\x00\x00": - raise ValueError("No client ID in the response.") + raise ValueError("No client hardware MAC address in the response.") # Check for the magic cookie. if _BUFF[236:240] != MAGIC_COOKIE: raise ValueError("No DHCP Magic Cookie in the response.") diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py index df7dd94..0711401 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp.py @@ -40,7 +40,6 @@ def test_constants(self): assert wiz_dhcp.STATE_BOUND == const(0x04) assert wiz_dhcp.STATE_RENEWING == const(0x05) assert wiz_dhcp.STATE_REBINDING == const(0x06) - assert wiz_dhcp.STATE_RELEASING == const(0x07) # DHCP Message Types assert wiz_dhcp.DHCP_DISCOVER == const(1) From 9bfe09d298e676464804742058b02171f41cc541 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 21 Dec 2022 04:58:54 +0300 Subject: [PATCH 10/80] Added more tests. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 42 +++++++++++++------- tests/test_dhcp.py | 44 ++++++++++++++++++++- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 0739518..461bd51 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -102,7 +102,7 @@ class DHCP: FSM can check whether the lease needs to be renewed, and can then renew it. Since DHCP uses UDP, messages may be lost. The DHCP protocol uses exponential backoff - for retrying. Retries occur after 4, 8, and 16 seconds (the final retry isfollowed by + for retrying. Retries occur after 4, 8, and 16 seconds (the final retry is followed by a wait of 32 seconds) so it will take about a minute to decide that no DHCP server is available. @@ -110,13 +110,13 @@ class DHCP: physical network as the client. The DHCP client cannot check whether the allocated IP address is already in use because - the ARP protocol is not available. Therefore, it is possible that the IP address has been - statically assigned to another device. In most cases, the DHCP server will make this - check before allocating an address, but some do not. + the ARP protocol is not available. Therefore, it is possible that the IP address + allocated by the server has been manually assigned to another device. In most cases, + the DHCP server will make this check before allocating an address, but some do not. The DHCPRELEASE message is not implemented. The DHCP protocol does not require it and DHCP servers can handle disappearing clients and clients that ask for 'replacement' - IP addressed. + IP addresses. """ # pylint: disable=too-many-arguments, too-many-instance-attributes, invalid-name @@ -213,7 +213,19 @@ def _socket_release(self) -> None: self._sock = None def _socket_setup(self, timeout: int = 5) -> None: - """I'll get to it.""" + """Initialise a UDP socket. + + Attempt to initialise a UDP socket. If the finite state machine (FSM) is in + blocking mode, repeat failed attempts until a socket is initialised or + the operation times out, then raise an exception. If the FSM is in non-blocking + mode, ignore the error and continue. + + :param int timeout: Time to keep retrying if the FSM is in blocking mode. + Defaults to 5. + + :raises TimeoutError: If the FSM is in blocking mode and a socket cannot be + initialised. + """ self._socket_release() stop_time = time.monotonic() + timeout while not time.monotonic() > stop_time: @@ -222,17 +234,17 @@ def _socket_setup(self, timeout: int = 5) -> None: except RuntimeError: if self._debug: print("DHCP client failed to allocate socket") - if self._blocking: - print("Retrying…") - else: - return + if self._blocking: + print("Retrying…") + else: + return else: self._sock.settimeout(self._response_timeout) self._sock.bind((None, 68)) self._sock.connect((self.dhcp_server_ip, DHCP_SERVER_PORT)) return - raise RuntimeError( - "DHCP client failed to allocate socket. Retried for {} seconds.".format( + raise TimeoutError( + "DHCP client failed to allocate a socket. Retried for {} seconds.".format( timeout ) ) @@ -241,7 +253,7 @@ def _increment_transaction_id(self) -> None: """Increment the transaction ID and roll over from 0x7fffffff to 0.""" self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF - def _next_retry_time(self, *, interval: int = 4) -> int: + def _next_retry_time_and_retry(self, *, interval: int = 4) -> int: """Calculate a retry stop time. The interval is calculated as an exponential fallback with a random variation to @@ -274,7 +286,7 @@ def _send_message_set_next_state( self._sock.send(_BUFF) self._retries = 0 self._max_retries = max_retries - self._next_resend = self._next_retry_time() + self._next_resend = self._next_retry_time_and_retry() self._dhcp_state = next_state def _handle_dhcp_message(self) -> None: @@ -321,7 +333,7 @@ def _handle_dhcp_message(self) -> None: return if not self._blocking: return - self._next_resend = self._next_retry_time() + self._next_resend = self._next_retry_time_and_retry() if self._retries > self._max_retries: raise RuntimeError( "No response from DHCP server after {}".format(self._max_retries) diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py index 0711401..acf9702 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp.py @@ -437,7 +437,7 @@ def test_next_retry_time_default_attrs(self, mocker, wiznet, rand_int): now = time.monotonic() for retry in range(3): dhcp_client._retries = retry - assert dhcp_client._next_retry_time() == int( + assert dhcp_client._next_retry_time_and_retry() == int( 2**retry * 4 + rand_int + now ) assert dhcp_client._retries == retry + 1 @@ -454,6 +454,46 @@ def test_next_retry_time_optional_attrs(self, mocker, wiznet, interval): now = time.monotonic() for retry in range(3): dhcp_client._retries = retry - assert dhcp_client._next_retry_time(interval=interval) == int( + assert dhcp_client._next_retry_time_and_retry(interval=interval) == int( 2**retry * interval + now ) + + def test_setup_socket_with_no_error(self, wiznet, wrench): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client._socket_setup() + wrench.socket.assert_called_once() + dhcp_client._sock.settimeout.assert_called_once_with( + dhcp_client._response_timeout + ) + dhcp_client._sock.bind.assert_called_once_with((None, 68)) + dhcp_client._sock.connect.assert_called_once_with( + (wiz_dhcp.BROADCAST_SERVER_ADDR, 67) + ) + + def test_setup_socket_with_error_then_ok_blocking(self, mocker, wiznet, wrench): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client._blocking = True + wrench.socket.side_effect = [RuntimeError, mocker.Mock()] + assert dhcp_client._socket_setup() is None + # Function should ignore the error, then set the socket and call settimeout + dhcp_client._sock.settimeout.assert_called_once_with( + dhcp_client._response_timeout + ) + + def test_setup_socket_with_error_then_ok_nonblocking(self, mocker, wiznet, wrench): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client._blocking = False + wrench.socket.side_effect = [RuntimeError, mocker.Mock()] + assert dhcp_client._socket_setup() is None + # Function should ignore the error, then return, _sock is None + assert dhcp_client._sock is None + + @freeze_time("2022-10-02", auto_tick_seconds=2) + def test_setup_socket_with_error_then_timeout(self, wiznet, wrench): + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client._blocking = True + wrench.socket.side_effect = [RuntimeError, RuntimeError] + # Function should timeout, raise an exception and not set a socket + with pytest.raises(TimeoutError): + dhcp_client._socket_setup() + assert dhcp_client._sock is None From 08ed3d781838e4b5628ebc9b0841f3da9d126f82 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 21 Dec 2022 19:18:06 +0300 Subject: [PATCH 11/80] Added tests for helper methods. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 61 ++++++++++++--------- tests/test_dhcp.py | 46 +++++++++++++++- 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 461bd51..f752a59 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2009 Jordan Terrell (blog.jordanterrell.com) # SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries # SPDX-FileCopyrightText: 2021 Patrick Van Oosterwijck @ Silicognition LLC +# SPDX-FileCopyrightText: 2022 Martin Stephens # # SPDX-License-Identifier: MIT @@ -106,9 +107,6 @@ class DHCP: a wait of 32 seconds) so it will take about a minute to decide that no DHCP server is available. - Use of DHCP relay agents is not implemented. The DHCP server must be on the same - physical network as the client. - The DHCP client cannot check whether the allocated IP address is already in use because the ARP protocol is not available. Therefore, it is possible that the IP address allocated by the server has been manually assigned to another device. In most cases, @@ -218,7 +216,7 @@ def _socket_setup(self, timeout: int = 5) -> None: Attempt to initialise a UDP socket. If the finite state machine (FSM) is in blocking mode, repeat failed attempts until a socket is initialised or the operation times out, then raise an exception. If the FSM is in non-blocking - mode, ignore the error and continue. + mode, ignore the error and return. :param int timeout: Time to keep retrying if the FSM is in blocking mode. Defaults to 5. @@ -277,20 +275,36 @@ def _next_retry_time_and_retry(self, *, interval: int = 4) -> int: def _send_message_set_next_state( self, *, - message_type: int, next_state: int, max_retries: int, ) -> None: - """I'll get to it""" + """Generate a DHCP message and update the finite state machine state (FSM). + + Creates and sends a DHCP message, resets retry parameters and updates the + FSM state. + + :param int next_state: The state that the FSM will be set to. + :param int max_retries: Maximum attempts to resend the DHCP message. + + :raises ValueError: If the next FSM state does not handle DHCP messages. + """ + if next_state not in (STATE_SELECTING, STATE_REQUESTING): + raise ValueError("The next state must be SELECTING or REQUESTING.") + if next_state == STATE_SELECTING: + message_type = DHCP_DISCOVER + else: + message_type = DHCP_REQUEST self._generate_dhcp_message(message_type=message_type) self._sock.send(_BUFF) self._retries = 0 self._max_retries = max_retries self._next_resend = self._next_retry_time_and_retry() + self._retries = 0 self._dhcp_state = next_state def _handle_dhcp_message(self) -> None: # pylint: disable=too-many-branches + global _BUFF # pylint: disable=global-statement while True: while time.monotonic() < self._next_resend: if self._sock.available(): @@ -306,7 +320,6 @@ def _handle_dhcp_message(self) -> None: and msg_type == DHCP_OFFER ): self._send_message_set_next_state( - message_type=DHCP_REQUEST, next_state=STATE_REQUESTING, max_retries=3, ) @@ -365,7 +378,6 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._socket_setup() self._start_time = time.monotonic() self._send_message_set_next_state( - message_type=DHCP_REQUEST, next_state=STATE_REQUESTING, max_retries=3, ) @@ -375,7 +387,6 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self.dhcp_server_ip = BROADCAST_SERVER_ADDR self._socket_setup() self._send_message_set_next_state( - message_type=DHCP_REQUEST, next_state=STATE_REQUESTING, max_retries=3, ) @@ -383,7 +394,6 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: if self._dhcp_state == STATE_INIT: self._dsm_reset() self._send_message_set_next_state( - message_type=DHCP_DISCOVER, next_state=STATE_SELECTING, max_retries=3, ) @@ -412,11 +422,11 @@ def _generate_dhcp_message( :param int message_type: Type of message to generate. :param bool broadcast: Used to set the flag requiring a broadcast reply from the - DHCP server. Defaults to False to match DHCP standard. + DHCP server. Defaults to False which matches the DHCP standard. :param bool renew: Set True for renewing and rebinding operations, defaults to False. """ - def option_data( + def option_writer( pointer: int, option_code: int, option_data: Union[Tuple[int, ...], bytes] ) -> int: """Helper function to set DHCP option data for a DHCP @@ -438,14 +448,15 @@ def option_data( _BUFF[pointer:data_end] = option_data return data_end - _BUFF[:] = b"\x00" * BUFF_LENGTH + global _BUFF # pylint: disable=global-variable-not-assigned + _BUFF[:] = bytearray(b"\x00" * BUFF_LENGTH) # OP.HTYPE.HLEN.HOPS _BUFF[0:4] = (DHCP_BOOT_REQUEST, DHCP_HTYPE10MB, DHCP_HLENETHERNET, DHCP_HOPS) # Transaction ID (xid) _BUFF[4:8] = self._transaction_id.to_bytes(4, "big") - # seconds elapsed + # Seconds elapsed _BUFF[8:10] = int(time.monotonic() - self._start_time).to_bytes(2, "big") - # flags (only bit 0 is used) + # Flags (only bit 0 is used, all other bits must be 0) if broadcast: _BUFF[10] = 0b10000000 if renew: @@ -459,24 +470,24 @@ def option_data( pointer = 240 # Option - DHCP Message Type - pointer = option_data( + pointer = option_writer( pointer=pointer, option_code=53, option_data=(message_type,) ) # Option - Host Name - pointer = option_data( + pointer = option_writer( pointer=pointer, option_code=12, option_data=self._hostname ) if message_type == DHCP_REQUEST: # Request subnet mask, router and DNS server. - pointer = option_data( + pointer = option_writer( pointer=pointer, option_code=55, option_data=(1, 3, 6) ) # Set Requested IP Address to offered IP address. - pointer = option_data( + pointer = option_writer( pointer=pointer, option_code=50, option_data=self.local_ip ) # Set Server ID to chosen DHCP server IP address. - pointer = option_data( + pointer = option_writer( pointer=pointer, option_code=54, option_data=self.dhcp_server_ip ) _BUFF[pointer] = 0xFF @@ -497,7 +508,7 @@ def _parse_dhcp_response( fail or no message type is found in the options, raises a ValueError. """ # pylint: disable=too-many-branches - def option_data(pointer: int) -> Tuple[int, int, bytes]: + def option_reader(pointer: int) -> Tuple[int, int, bytes]: """Helper function to extract DHCP option data from a response. @@ -507,13 +518,13 @@ def option_data(pointer: int) -> Tuple[int, int, bytes]: option type, and option data. """ global _BUFF # pylint: disable=global-variable-not-assigned - data_type = _BUFF[pointer] + option_type = _BUFF[pointer] pointer += 1 data_length = _BUFF[pointer] pointer += 1 data_end = pointer + data_length - data = _BUFF[pointer:data_end] - return data_end, data_type, data + option_data = _BUFF[pointer:data_end] + return data_end, option_type, option_data global _BUFF # pylint: disable=global-variable-not-assigned # Validate OP @@ -536,7 +547,7 @@ def option_data(pointer: int) -> Tuple[int, int, bytes]: msg_type = None ptr = 240 while _BUFF[ptr] != OPT_END: - ptr, data_type, data = option_data(ptr) + ptr, data_type, data = option_reader(ptr) if data_type == MSG_TYPE: msg_type = data[0] elif data_type == SUBNET_MASK: diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py index acf9702..9404c78 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp.py @@ -12,7 +12,6 @@ import dummy_dhcp_data as dhcp_data import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as wiz_dhcp -# DEFAULT_DEBUG_ON = True @@ -497,3 +496,48 @@ def test_setup_socket_with_error_then_timeout(self, wiznet, wrench): with pytest.raises(TimeoutError): dhcp_client._socket_setup() assert dhcp_client._sock is None + + @pytest.mark.parametrize( + "next_state, msg_type, retries", + ( + (wiz_dhcp.STATE_SELECTING, wiz_dhcp.DHCP_DISCOVER, 2), + (wiz_dhcp.STATE_REQUESTING, wiz_dhcp.DHCP_REQUEST, 3), + ), + ) + def test_send_message_set_next_state_good_data( + self, mocker, wiznet, next_state, msg_type, retries + ): + mocker.patch.object(wiz_dhcp.DHCP, "_generate_dhcp_message") + mocker.patch.object(wiz_dhcp.DHCP, "_next_retry_time_and_retry") + message = bytearray(b"HelloWorld") + wiz_dhcp._BUFF = bytearray(message) + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client._sock = mocker.Mock() + dhcp_client._send_message_set_next_state( + next_state=next_state, max_retries=retries + ) + assert dhcp_client._retries == 0 + assert dhcp_client._max_retries == retries + assert dhcp_client._dhcp_state == next_state + dhcp_client._generate_dhcp_message.assert_called_once_with( + message_type=msg_type + ) + dhcp_client._sock.send.assert_called_once_with(message) + dhcp_client._next_retry_time_and_retry.assert_called_once() + + @pytest.mark.parametrize( + "next_state", + ( + wiz_dhcp.STATE_INIT, + wiz_dhcp.STATE_REBINDING, + wiz_dhcp.STATE_RENEWING, + wiz_dhcp.STATE_BOUND, + ), + ) + def test_send_message_set_next_state_bad_state(self, wiznet, next_state): + # Test with all states that should not call this function. + dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + with pytest.raises(ValueError): + dhcp_client._send_message_set_next_state( + next_state=next_state, max_retries=4 + ) From fa07b95837309f1dd9ab8d3283e51884c32e02e7 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 24 Dec 2022 12:52:37 +0300 Subject: [PATCH 12/80] Added tests for _handle_dhcp_message --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 13 +- tests/test_dhcp_main_logic.py | 128 ++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/test_dhcp_main_logic.py diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index f752a59..1f15f9f 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -303,6 +303,17 @@ def _send_message_set_next_state( self._dhcp_state = next_state def _handle_dhcp_message(self) -> None: + """Receive and process DHCP message then update the finite state machine (FSM). + + Wait for a response from the DHCP server, resending on an exponential fallback + schedule if no response is received. Process the response, sending messages, + setting attributes, and setting next FSM state according to the current state. + + Only called when the FSM is in SELECTING or REQUESTING states. + + :raises RuntimeError: If the FSM is in blocking mode and no valid response has + been received before the timeout expires. + """ # pylint: disable=too-many-branches global _BUFF # pylint: disable=global-statement while True: @@ -348,7 +359,7 @@ def _handle_dhcp_message(self) -> None: return self._next_resend = self._next_retry_time_and_retry() if self._retries > self._max_retries: - raise RuntimeError( + raise TimeoutError( "No response from DHCP server after {}".format(self._max_retries) ) if not self._blocking: diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py new file mode 100644 index 0000000..228b4fd --- /dev/null +++ b/tests/test_dhcp_main_logic.py @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: 2022 Martin Stephens +# +# SPDX-License-Identifier: MIT +"""Tests to confirm that there are no changes in behaviour to methods and functions. +These test are not exhaustive, but are a sanity check while making changes to the module.""" +import time + +# pylint: disable=no-self-use, redefined-outer-name, protected-access, invalid-name, too-many-arguments +import pytest +from freezegun import freeze_time +import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as wiz_dhcp + + +@pytest.fixture +def mock_wiznet5k(mocker): + """Mock WIZNET5K so that the DHCP class can be tested without hardware.""" + return mocker.patch("adafruit_wiznet5k.adafruit_wiznet5k.WIZNET5K", autospec=True) + + +@pytest.fixture +def mock_socket(mocker): + """Mock socket module to allow test data to be read and written by the DHCP module.""" + return mocker.patch( + "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket.socket", autospec=True + ) + + +@pytest.fixture +def dhcp_with_mock_5k(mock_wiznet5k): + """Instance of DHCP with mock WIZNET5K interface.""" + return wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) + + +@pytest.fixture +def dhcp_mock_5k_socket(dhcp_with_mock_5k, mock_socket): + """Instance of DHCP with mock WIZNET5K interface and a mock socket.""" + dhcp_with_mock_5k._sock = mock_socket() + return dhcp_with_mock_5k + + +class TestHandleDhcpMessage: + @freeze_time("2022-06-10") + def test_with_valid_data_on_socket_selecting(self, mocker, dhcp_mock_5k_socket): + # Mock the methods that will be checked for this test. + mocker.patch.object( + dhcp_mock_5k_socket, + "_parse_dhcp_response", + autospec=True, + return_value=wiz_dhcp.DHCP_OFFER, + ) + mocker.patch.object( + dhcp_mock_5k_socket, "_send_message_set_next_state", autospec=True + ) + # Set up initial values for the test + wiz_dhcp._BUFF = b"" + dhcp_mock_5k_socket._sock.available.return_value = 24 + dhcp_mock_5k_socket._sock.recv.return_value = b"HelloWorld" + dhcp_mock_5k_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Test + dhcp_mock_5k_socket._handle_dhcp_message() + # Check response + assert wiz_dhcp._BUFF == b"HelloWorld" + dhcp_mock_5k_socket._parse_dhcp_response.assert_called_once() + dhcp_mock_5k_socket._send_message_set_next_state.assert_called_once_with( + next_state=wiz_dhcp.STATE_BOUND, max_retries=3 + ) + + @freeze_time("2022-06-10") + def test_with_valid_data_on_socket_requesting(self, mocker, dhcp_mock_5k_socket): + # Mock the methods that will be checked for this test. + mocker.patch.object( + dhcp_mock_5k_socket, + "_parse_dhcp_response", + autospec=True, + return_value=wiz_dhcp.DHCP_ACK, + ) + mocker.patch.object( + dhcp_mock_5k_socket, "_send_message_set_next_state", autospec=True + ) + # Set up initial values for the test + wiz_dhcp._BUFF = b"" + dhcp_mock_5k_socket._sock.available.return_value = 24 + dhcp_mock_5k_socket._sock.recv.return_value = b"HelloWorld" + dhcp_mock_5k_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Test + dhcp_mock_5k_socket._handle_dhcp_message() + # Check response + assert wiz_dhcp._BUFF == b"HelloWorld" + dhcp_mock_5k_socket._parse_dhcp_response.assert_called_once() + dhcp_mock_5k_socket._send_message_set_next_state.assert_not_called() + + @freeze_time("2022-06-10") + @pytest.mark.parametrize( + "fsm_state, msg_type, next_state", + ( + (wiz_dhcp.STATE_SELECTING, wiz_dhcp.DHCP_ACK, wiz_dhcp.STATE_SELECTING), + (wiz_dhcp.STATE_REQUESTING, wiz_dhcp.DHCP_OFFER, wiz_dhcp.STATE_REQUESTING), + ), + ) + def test_with_wrong_message_type_on_socket( + self, mocker, dhcp_mock_5k_socket, fsm_state, msg_type, next_state + ): + # Mock the methods that will be checked for this test. + mocker.patch.object( + dhcp_mock_5k_socket, + "_parse_dhcp_response", + autospec=True, + return_value=msg_type, + ) + mocker.patch.object( + dhcp_mock_5k_socket, "_send_message_set_next_state", autospec=True + ) + # Set up initial values for the test + wiz_dhcp._BUFF = b"" + dhcp_mock_5k_socket._sock.available.return_value = 32 + dhcp_mock_5k_socket._sock.recv.return_value = b"TweetTweet" + dhcp_mock_5k_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_socket._dhcp_state = fsm_state + # Test + dhcp_mock_5k_socket._handle_dhcp_message() + # Check response + dhcp_mock_5k_socket._sock.recv.assert_called_once() + assert wiz_dhcp._BUFF == b"TweetTweet" + dhcp_mock_5k_socket._parse_dhcp_response.assert_called_once() + dhcp_mock_5k_socket._send_message_set_next_state.assert_not_called() + assert dhcp_mock_5k_socket._dhcp_state == next_state From e8f7fe8364cb5679dcff4e3d9f38802a7d3bfa27 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 24 Dec 2022 12:52:56 +0300 Subject: [PATCH 13/80] Added tests for _handle_dhcp_message --- ...{dummy_dhcp_data.py => dhcp_dummy_data.py} | 2 +- ...test_dhcp.py => test_dhcp_helper_files.py} | 112 +++++++++--------- 2 files changed, 60 insertions(+), 54 deletions(-) rename tests/{dummy_dhcp_data.py => dhcp_dummy_data.py} (98%) rename tests/{test_dhcp.py => test_dhcp_helper_files.py} (82%) diff --git a/tests/dummy_dhcp_data.py b/tests/dhcp_dummy_data.py similarity index 98% rename from tests/dummy_dhcp_data.py rename to tests/dhcp_dummy_data.py index c457ca2..5959988 100644 --- a/tests/dummy_dhcp_data.py +++ b/tests/dhcp_dummy_data.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022 Martin Stephens # # SPDX-License-Identifier: MIT -"""Data for use in test_dhcp.py""" +"""Data for use in test_dhcp_helper_files.py""" def _pad_message(message_section: bytearray, target_length: int) -> bytearray: diff --git a/tests/test_dhcp.py b/tests/test_dhcp_helper_files.py similarity index 82% rename from tests/test_dhcp.py rename to tests/test_dhcp_helper_files.py index 9404c78..23bf11c 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp_helper_files.py @@ -9,20 +9,18 @@ import pytest from freezegun import freeze_time from micropython import const -import dummy_dhcp_data as dhcp_data +import dhcp_dummy_data as dhcp_data import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as wiz_dhcp -DEFAULT_DEBUG_ON = True - @pytest.fixture -def wiznet(mocker): +def mock_wiznet5k(mocker): """Mock WIZNET5K so that the DHCP class can be tested without hardware.""" return mocker.patch("adafruit_wiznet5k.adafruit_wiznet5k.WIZNET5K", autospec=True) @pytest.fixture -def wrench(mocker): +def mock_socket(mocker): """Mock socket module to allow test data to be read and written by the DHCP module.""" return mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket", autospec=True @@ -93,19 +91,19 @@ def test_constants(self): bytes([1, 2, 4, 6, 7, 8]), ), ) - def test_dhcp_setup_default(self, mocker, wiznet, wrench, mac_address): + def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mock_socket, mac_address): """Test intial settings from DHCP.__init__.""" # Test with mac address as tuple, list and bytes with default values. mock_randint = mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", autospec=True ) mock_randint.return_value = 0x1234567 - dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address) - assert dhcp_client._eth == wiznet + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, mac_address) + assert dhcp_client._eth == mock_wiznet5k assert dhcp_client._response_timeout == 30.0 assert dhcp_client._debug is False assert dhcp_client._mac_address == mac_address - wrench.set_interface.assert_called_once_with(wiznet) + mock_socket.set_interface.assert_called_once_with(mock_wiznet5k) assert dhcp_client._sock is None assert dhcp_client._dhcp_state == wiz_dhcp.STATE_INIT mock_randint.assert_called_once() @@ -124,11 +122,15 @@ def test_dhcp_setup_default(self, mocker, wiznet, wrench, mac_address): "WIZnet{}".split(".", maxsplit=1)[0].format(mac_string)[:42], "utf-8" ) - def test_dhcp_setup_other_args(self, wiznet): + def test_dhcp_setup_other_args(self, mock_wiznet5k): """Test instantiating DHCP with none default values.""" mac_address = (7, 8, 9, 10, 11, 12) dhcp_client = wiz_dhcp.DHCP( - wiznet, mac_address, hostname="fred.com", response_timeout=25.0, debug=True + mock_wiznet5k, + mac_address, + hostname="fred.com", + response_timeout=25.0, + debug=True, ) assert dhcp_client._response_timeout == 25.0 @@ -141,11 +143,11 @@ def test_dhcp_setup_other_args(self, wiznet): @freeze_time("2022-10-20") class TestSendDHCPMessage: - def test_generate_message_with_default_attributes(self, wiznet, wrench): + def test_generate_message_with_default_attributes(self, mock_wiznet5k, mock_socket): """Test the _generate_message function with default values.""" assert len(wiz_dhcp._BUFF) == 318 - dhcp_client = wiz_dhcp.DHCP(wiznet, (4, 5, 6, 7, 8, 9)) - dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) + dhcp_client._sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) dhcp_client._transaction_id = 0x6FFFFFFF dhcp_client._start_time = time.monotonic() - 23.4 dhcp_client._generate_dhcp_message(message_type=wiz_dhcp.DHCP_DISCOVER) @@ -193,7 +195,7 @@ def test_generate_message_with_default_attributes(self, wiznet, wrench): ) def test_generate_dhcp_message_discover_with_non_defaults( self, - wiznet, + mock_wiznet5k, mac_address, hostname, msg_type, @@ -206,7 +208,7 @@ def test_generate_dhcp_message_discover_with_non_defaults( ): """Test the generate_dhcp_message function with different message types and none default attributes.""" - dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address, hostname=hostname) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, mac_address, hostname=hostname) # Set client attributes for test dhcp_client.local_ip = local_ip dhcp_client.dhcp_server_ip = server_ip @@ -251,7 +253,7 @@ def test_generate_dhcp_message_discover_with_non_defaults( ) def test_generate_dhcp_message_with_request_options( self, - wiznet, + mock_wiznet5k, mac_address, hostname, msg_type, @@ -262,7 +264,7 @@ def test_generate_dhcp_message_with_request_options( server_ip, result, ): - dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address, hostname=hostname) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, mac_address, hostname=hostname) # Set client attributes for test dhcp_client.local_ip = local_ip dhcp_client.dhcp_server_ip = server_ip @@ -311,7 +313,7 @@ class TestParseDhcpMessage: # pylint: disable=too-many-locals def test_parse_good_data( self, - wiznet, + mock_wiznet5k, xid, local_ip, msg_type, @@ -325,7 +327,7 @@ def test_parse_good_data( response, ): wiz_dhcp._BUFF = response - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._transaction_id = xid response_type = dhcp_client._parse_dhcp_response() assert response_type == msg_type @@ -338,11 +340,11 @@ def test_parse_good_data( assert dhcp_client._t1 == t1 assert dhcp_client._t2 == t2 - def test_parsing_failures(self, wiznet, wrench): + def test_parsing_failures(self, mock_wiznet5k, mock_socket): # Test for bad OP code, ID mismatch, no server ID, bad Magic Cookie bad_data = dhcp_data.BAD_DATA - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) - dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client._sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) dhcp_client._sock.recv.return_value = bad_data # Transaction ID mismatch. dhcp_client._transaction_id = 0x42424242 @@ -366,20 +368,20 @@ def test_parsing_failures(self, wiznet, wrench): class TestResetDsmReset: - def test_socket_reset(self, wiznet, wrench): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_socket_reset(self, mock_wiznet5k, mock_socket): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._dsm_reset() assert dhcp_client._sock is None - dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) + dhcp_client._sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) dhcp_client._dsm_reset() assert dhcp_client._sock is None @freeze_time("2022-11-10") - def test_reset_dsm_parameters(self, wiznet): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_reset_dsm_parameters(self, mock_wiznet5k): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client.dhcp_server_ip = (1, 2, 3, 4) dhcp_client.local_ip = (2, 3, 4, 5) dhcp_client.subnet_mask = (3, 4, 5, 6) @@ -402,19 +404,19 @@ def test_reset_dsm_parameters(self, wiznet): class TestSocketRelease: - def test_socket_set_to_none(self, wiznet, wrench): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_socket_set_to_none(self, mock_wiznet5k, mock_socket): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._socket_release() assert dhcp_client._sock is None - dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) + dhcp_client._sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) dhcp_client._socket_release() assert dhcp_client._sock is None class TestSmallHelperFunctions: - def test_increment_transaction_id(self, wiznet): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_increment_transaction_id(self, mock_wiznet5k): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) # Test that transaction_id increments. dhcp_client._transaction_id = 4 dhcp_client._increment_transaction_id() @@ -426,8 +428,8 @@ def test_increment_transaction_id(self, wiznet): @freeze_time("2022-10-10") @pytest.mark.parametrize("rand_int", (-1, 0, 1)) - def test_next_retry_time_default_attrs(self, mocker, wiznet, rand_int): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_next_retry_time_default_attrs(self, mocker, mock_wiznet5k, rand_int): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", autospec=True, @@ -443,8 +445,8 @@ def test_next_retry_time_default_attrs(self, mocker, wiznet, rand_int): @freeze_time("2022-10-10") @pytest.mark.parametrize("interval", (2, 7, 10)) - def test_next_retry_time_optional_attrs(self, mocker, wiznet, interval): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_next_retry_time_optional_attrs(self, mocker, mock_wiznet5k, interval): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", autospec=True, @@ -457,10 +459,10 @@ def test_next_retry_time_optional_attrs(self, mocker, wiznet, interval): 2**retry * interval + now ) - def test_setup_socket_with_no_error(self, wiznet, wrench): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_setup_socket_with_no_error(self, mock_wiznet5k, mock_socket): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._socket_setup() - wrench.socket.assert_called_once() + mock_socket.socket.assert_called_once() dhcp_client._sock.settimeout.assert_called_once_with( dhcp_client._response_timeout ) @@ -469,29 +471,33 @@ def test_setup_socket_with_no_error(self, wiznet, wrench): (wiz_dhcp.BROADCAST_SERVER_ADDR, 67) ) - def test_setup_socket_with_error_then_ok_blocking(self, mocker, wiznet, wrench): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_setup_socket_with_error_then_ok_blocking( + self, mocker, mock_wiznet5k, mock_socket + ): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._blocking = True - wrench.socket.side_effect = [RuntimeError, mocker.Mock()] + mock_socket.socket.side_effect = [RuntimeError, mocker.Mock()] assert dhcp_client._socket_setup() is None # Function should ignore the error, then set the socket and call settimeout dhcp_client._sock.settimeout.assert_called_once_with( dhcp_client._response_timeout ) - def test_setup_socket_with_error_then_ok_nonblocking(self, mocker, wiznet, wrench): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_setup_socket_with_error_then_ok_nonblocking( + self, mocker, mock_wiznet5k, mock_socket + ): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._blocking = False - wrench.socket.side_effect = [RuntimeError, mocker.Mock()] + mock_socket.socket.side_effect = [RuntimeError, mocker.Mock()] assert dhcp_client._socket_setup() is None # Function should ignore the error, then return, _sock is None assert dhcp_client._sock is None @freeze_time("2022-10-02", auto_tick_seconds=2) - def test_setup_socket_with_error_then_timeout(self, wiznet, wrench): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + def test_setup_socket_with_error_then_timeout(self, mock_wiznet5k, mock_socket): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._blocking = True - wrench.socket.side_effect = [RuntimeError, RuntimeError] + mock_socket.socket.side_effect = [RuntimeError, RuntimeError] # Function should timeout, raise an exception and not set a socket with pytest.raises(TimeoutError): dhcp_client._socket_setup() @@ -505,13 +511,13 @@ def test_setup_socket_with_error_then_timeout(self, wiznet, wrench): ), ) def test_send_message_set_next_state_good_data( - self, mocker, wiznet, next_state, msg_type, retries + self, mocker, mock_wiznet5k, next_state, msg_type, retries ): mocker.patch.object(wiz_dhcp.DHCP, "_generate_dhcp_message") mocker.patch.object(wiz_dhcp.DHCP, "_next_retry_time_and_retry") message = bytearray(b"HelloWorld") wiz_dhcp._BUFF = bytearray(message) - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._sock = mocker.Mock() dhcp_client._send_message_set_next_state( next_state=next_state, max_retries=retries @@ -534,9 +540,9 @@ def test_send_message_set_next_state_good_data( wiz_dhcp.STATE_BOUND, ), ) - def test_send_message_set_next_state_bad_state(self, wiznet, next_state): + def test_send_message_set_next_state_bad_state(self, mock_wiznet5k, next_state): # Test with all states that should not call this function. - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) with pytest.raises(ValueError): dhcp_client._send_message_set_next_state( next_state=next_state, max_retries=4 From 6cb5a50fd981b26660cc493467e3ad11df68db64 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 26 Dec 2022 18:44:13 +0300 Subject: [PATCH 14/80] Added more tests for _handle_dhcp_message function. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 2 +- tests/test_dhcp_main_logic.py | 140 ++++++++++++++------ 2 files changed, 103 insertions(+), 39 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 1f15f9f..4eb7fad 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -354,7 +354,7 @@ def _handle_dhcp_message(self) -> None: self._sock.close() self._sock = None self._dhcp_state = STATE_BOUND - return + return if not self._blocking: return self._next_resend = self._next_retry_time_and_retry() diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index 228b4fd..c2b5c3f 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -32,7 +32,7 @@ def dhcp_with_mock_5k(mock_wiznet5k): @pytest.fixture -def dhcp_mock_5k_socket(dhcp_with_mock_5k, mock_socket): +def dhcp_mock_5k_with_socket(dhcp_with_mock_5k, mock_socket): """Instance of DHCP with mock WIZNET5K interface and a mock socket.""" dhcp_with_mock_5k._sock = mock_socket() return dhcp_with_mock_5k @@ -40,89 +40,153 @@ def dhcp_mock_5k_socket(dhcp_with_mock_5k, mock_socket): class TestHandleDhcpMessage: @freeze_time("2022-06-10") - def test_with_valid_data_on_socket_selecting(self, mocker, dhcp_mock_5k_socket): + def test_with_valid_data_on_socket_selecting( + self, mocker, dhcp_mock_5k_with_socket + ): # Mock the methods that will be checked for this test. mocker.patch.object( - dhcp_mock_5k_socket, + dhcp_mock_5k_with_socket, "_parse_dhcp_response", autospec=True, return_value=wiz_dhcp.DHCP_OFFER, ) mocker.patch.object( - dhcp_mock_5k_socket, "_send_message_set_next_state", autospec=True + dhcp_mock_5k_with_socket, "_send_message_set_next_state", autospec=True ) # Set up initial values for the test wiz_dhcp._BUFF = b"" - dhcp_mock_5k_socket._sock.available.return_value = 24 - dhcp_mock_5k_socket._sock.recv.return_value = b"HelloWorld" - dhcp_mock_5k_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._sock.available.return_value = 24 + dhcp_mock_5k_with_socket._sock.recv.return_value = b"HelloWorld" + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING # Test - dhcp_mock_5k_socket._handle_dhcp_message() + dhcp_mock_5k_with_socket._handle_dhcp_message() # Check response assert wiz_dhcp._BUFF == b"HelloWorld" - dhcp_mock_5k_socket._parse_dhcp_response.assert_called_once() - dhcp_mock_5k_socket._send_message_set_next_state.assert_called_once_with( - next_state=wiz_dhcp.STATE_BOUND, max_retries=3 + dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @freeze_time("2022-06-10") - def test_with_valid_data_on_socket_requesting(self, mocker, dhcp_mock_5k_socket): + def test_with_valid_data_on_socket_requesting( + self, mocker, dhcp_mock_5k_with_socket + ): # Mock the methods that will be checked for this test. mocker.patch.object( - dhcp_mock_5k_socket, + dhcp_mock_5k_with_socket, "_parse_dhcp_response", autospec=True, return_value=wiz_dhcp.DHCP_ACK, ) mocker.patch.object( - dhcp_mock_5k_socket, "_send_message_set_next_state", autospec=True + dhcp_mock_5k_with_socket, "_send_message_set_next_state", autospec=True ) # Set up initial values for the test wiz_dhcp._BUFF = b"" - dhcp_mock_5k_socket._sock.available.return_value = 24 - dhcp_mock_5k_socket._sock.recv.return_value = b"HelloWorld" - dhcp_mock_5k_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + dhcp_mock_5k_with_socket._sock.available.return_value = 24 + dhcp_mock_5k_with_socket._sock.recv.return_value = b"HelloWorld" + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + initial_transaction_id = dhcp_mock_5k_with_socket._transaction_id # Test - dhcp_mock_5k_socket._handle_dhcp_message() + dhcp_mock_5k_with_socket._handle_dhcp_message() # Check response assert wiz_dhcp._BUFF == b"HelloWorld" - dhcp_mock_5k_socket._parse_dhcp_response.assert_called_once() - dhcp_mock_5k_socket._send_message_set_next_state.assert_not_called() + assert dhcp_mock_5k_with_socket._transaction_id == initial_transaction_id + 1 + assert dhcp_mock_5k_with_socket._renew is False + assert dhcp_mock_5k_with_socket._sock is None + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND + dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() @freeze_time("2022-06-10") @pytest.mark.parametrize( - "fsm_state, msg_type, next_state", + "fsm_state, msg_type", ( - (wiz_dhcp.STATE_SELECTING, wiz_dhcp.DHCP_ACK, wiz_dhcp.STATE_SELECTING), - (wiz_dhcp.STATE_REQUESTING, wiz_dhcp.DHCP_OFFER, wiz_dhcp.STATE_REQUESTING), + (wiz_dhcp.STATE_SELECTING, wiz_dhcp.DHCP_ACK), + (wiz_dhcp.STATE_REQUESTING, wiz_dhcp.DHCP_OFFER), ), ) - def test_with_wrong_message_type_on_socket( - self, mocker, dhcp_mock_5k_socket, fsm_state, msg_type, next_state + def test_with_wrong_message_type_on_socket_nonblocking( + self, + mocker, + dhcp_mock_5k_with_socket, + fsm_state, + msg_type, ): # Mock the methods that will be checked for this test. mocker.patch.object( - dhcp_mock_5k_socket, + dhcp_mock_5k_with_socket, "_parse_dhcp_response", autospec=True, return_value=msg_type, ) mocker.patch.object( - dhcp_mock_5k_socket, "_send_message_set_next_state", autospec=True + dhcp_mock_5k_with_socket, "_send_message_set_next_state", autospec=True ) # Set up initial values for the test wiz_dhcp._BUFF = b"" - dhcp_mock_5k_socket._sock.available.return_value = 32 - dhcp_mock_5k_socket._sock.recv.return_value = b"TweetTweet" - dhcp_mock_5k_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_socket._dhcp_state = fsm_state + dhcp_mock_5k_with_socket._sock.available.return_value = 32 + dhcp_mock_5k_with_socket._sock.recv.return_value = b"TweetTweet" + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = fsm_state # Test - dhcp_mock_5k_socket._handle_dhcp_message() + dhcp_mock_5k_with_socket._handle_dhcp_message() # Check response - dhcp_mock_5k_socket._sock.recv.assert_called_once() + dhcp_mock_5k_with_socket._sock.recv.assert_called_once() assert wiz_dhcp._BUFF == b"TweetTweet" - dhcp_mock_5k_socket._parse_dhcp_response.assert_called_once() - dhcp_mock_5k_socket._send_message_set_next_state.assert_not_called() - assert dhcp_mock_5k_socket._dhcp_state == next_state + dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() + assert dhcp_mock_5k_with_socket._dhcp_state == fsm_state + + @freeze_time("2022-06-10") + @pytest.mark.parametrize( + "fsm_state, msg_type, next_state", + ( + ( + wiz_dhcp.STATE_SELECTING, + [wiz_dhcp.DHCP_ACK, wiz_dhcp.DHCP_ACK, wiz_dhcp.DHCP_OFFER], + wiz_dhcp.STATE_REQUESTING, + ), + ( + wiz_dhcp.STATE_REQUESTING, + [ + wiz_dhcp.DHCP_OFFER, + wiz_dhcp.DHCP_OFFER, + wiz_dhcp.DHCP_ACK, + ], + wiz_dhcp.STATE_BOUND, + ), + ), + ) + def test_with_wrong_message_type_on_socket_blocking( + self, mocker, dhcp_mock_5k_with_socket, fsm_state, msg_type, next_state + ): + # Mock the methods that will be checked for this test. + mocker.patch.object( + dhcp_mock_5k_with_socket, + "_parse_dhcp_response", + autospec=True, + side_effect=msg_type, + ) + mocker.patch.object( + dhcp_mock_5k_with_socket, + "_send_message_set_next_state", + autospec=True, + ) + # Set up initial values for the test + wiz_dhcp._BUFF = b"" + dhcp_mock_5k_with_socket._sock.available.return_value = 32 + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = fsm_state + dhcp_mock_5k_with_socket._blocking = True + # Test + dhcp_mock_5k_with_socket._handle_dhcp_message() + # Check response + assert dhcp_mock_5k_with_socket._parse_dhcp_response.call_count == 3 + if fsm_state == wiz_dhcp.STATE_SELECTING: + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once() + elif fsm_state == wiz_dhcp.STATE_REQUESTING: # Not called for STATE_REQUESTING + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() + assert dhcp_mock_5k_with_socket._dhcp_state == next_state From 152a2f8b5155d14cbe2a7993dce310b54c393470 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 26 Dec 2022 21:21:40 +0300 Subject: [PATCH 15/80] Refactored fixtures to make tests more readable. --- tests/test_dhcp_main_logic.py | 100 ++++++++-------------------------- 1 file changed, 23 insertions(+), 77 deletions(-) diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index c2b5c3f..b476d8e 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -12,87 +12,56 @@ @pytest.fixture -def mock_wiznet5k(mocker): - """Mock WIZNET5K so that the DHCP class can be tested without hardware.""" - return mocker.patch("adafruit_wiznet5k.adafruit_wiznet5k.WIZNET5K", autospec=True) - - -@pytest.fixture -def mock_socket(mocker): - """Mock socket module to allow test data to be read and written by the DHCP module.""" - return mocker.patch( +def dhcp_mock_5k_with_socket(mocker): + """Instance of DHCP with mock WIZNET5K interface and a mock socket.""" + wiz_dhcp._BUFF = b"" + mock_wiznet5k = mocker.patch( + "adafruit_wiznet5k.adafruit_wiznet5k.WIZNET5K", autospec=True + ) + dhcp = wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) + dhcp._sock = mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket.socket", autospec=True ) - - -@pytest.fixture -def dhcp_with_mock_5k(mock_wiznet5k): - """Instance of DHCP with mock WIZNET5K interface.""" - return wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) - - -@pytest.fixture -def dhcp_mock_5k_with_socket(dhcp_with_mock_5k, mock_socket): - """Instance of DHCP with mock WIZNET5K interface and a mock socket.""" - dhcp_with_mock_5k._sock = mock_socket() - return dhcp_with_mock_5k + mocker.patch.object(dhcp, "_parse_dhcp_response", autospec=True) + mocker.patch.object(dhcp, "_send_message_set_next_state", autospec=True) + yield dhcp class TestHandleDhcpMessage: + """ + Test that DHCP._handle_dhcp_message responds correctly with good and bad data while in + both blocking and none blocking modes. + """ + @freeze_time("2022-06-10") - def test_with_valid_data_on_socket_selecting( - self, mocker, dhcp_mock_5k_with_socket - ): + def test_with_valid_data_on_socket_selecting(self, dhcp_mock_5k_with_socket): # Mock the methods that will be checked for this test. - mocker.patch.object( - dhcp_mock_5k_with_socket, - "_parse_dhcp_response", - autospec=True, - return_value=wiz_dhcp.DHCP_OFFER, - ) - mocker.patch.object( - dhcp_mock_5k_with_socket, "_send_message_set_next_state", autospec=True - ) + dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER # Set up initial values for the test wiz_dhcp._BUFF = b"" dhcp_mock_5k_with_socket._sock.available.return_value = 24 - dhcp_mock_5k_with_socket._sock.recv.return_value = b"HelloWorld" dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING # Test dhcp_mock_5k_with_socket._handle_dhcp_message() # Check response - assert wiz_dhcp._BUFF == b"HelloWorld" dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @freeze_time("2022-06-10") - def test_with_valid_data_on_socket_requesting( - self, mocker, dhcp_mock_5k_with_socket - ): + def test_with_valid_data_on_socket_requesting(self, dhcp_mock_5k_with_socket): # Mock the methods that will be checked for this test. - mocker.patch.object( - dhcp_mock_5k_with_socket, - "_parse_dhcp_response", - autospec=True, - return_value=wiz_dhcp.DHCP_ACK, - ) - mocker.patch.object( - dhcp_mock_5k_with_socket, "_send_message_set_next_state", autospec=True - ) + dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK # Set up initial values for the test - wiz_dhcp._BUFF = b"" dhcp_mock_5k_with_socket._sock.available.return_value = 24 - dhcp_mock_5k_with_socket._sock.recv.return_value = b"HelloWorld" dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING initial_transaction_id = dhcp_mock_5k_with_socket._transaction_id # Test dhcp_mock_5k_with_socket._handle_dhcp_message() # Check response - assert wiz_dhcp._BUFF == b"HelloWorld" assert dhcp_mock_5k_with_socket._transaction_id == initial_transaction_id + 1 assert dhcp_mock_5k_with_socket._renew is False assert dhcp_mock_5k_with_socket._sock is None @@ -110,32 +79,20 @@ def test_with_valid_data_on_socket_requesting( ) def test_with_wrong_message_type_on_socket_nonblocking( self, - mocker, dhcp_mock_5k_with_socket, fsm_state, msg_type, ): # Mock the methods that will be checked for this test. - mocker.patch.object( - dhcp_mock_5k_with_socket, - "_parse_dhcp_response", - autospec=True, - return_value=msg_type, - ) - mocker.patch.object( - dhcp_mock_5k_with_socket, "_send_message_set_next_state", autospec=True - ) + dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = msg_type # Set up initial values for the test - wiz_dhcp._BUFF = b"" dhcp_mock_5k_with_socket._sock.available.return_value = 32 - dhcp_mock_5k_with_socket._sock.recv.return_value = b"TweetTweet" dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = fsm_state # Test dhcp_mock_5k_with_socket._handle_dhcp_message() # Check response dhcp_mock_5k_with_socket._sock.recv.assert_called_once() - assert wiz_dhcp._BUFF == b"TweetTweet" dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() assert dhcp_mock_5k_with_socket._dhcp_state == fsm_state @@ -161,22 +118,11 @@ def test_with_wrong_message_type_on_socket_nonblocking( ), ) def test_with_wrong_message_type_on_socket_blocking( - self, mocker, dhcp_mock_5k_with_socket, fsm_state, msg_type, next_state + self, dhcp_mock_5k_with_socket, fsm_state, msg_type, next_state ): # Mock the methods that will be checked for this test. - mocker.patch.object( - dhcp_mock_5k_with_socket, - "_parse_dhcp_response", - autospec=True, - side_effect=msg_type, - ) - mocker.patch.object( - dhcp_mock_5k_with_socket, - "_send_message_set_next_state", - autospec=True, - ) + dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = msg_type # Set up initial values for the test - wiz_dhcp._BUFF = b"" dhcp_mock_5k_with_socket._sock.available.return_value = 32 dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = fsm_state From 2888d04a6cb4e2adf6de883426f0156b464f12e1 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 28 Dec 2022 07:17:54 +0300 Subject: [PATCH 16/80] Finished tests for _handle_dhcp_message --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 6 +- tests/test_dhcp_main_logic.py | 109 ++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 4eb7fad..03f7b25 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -311,7 +311,7 @@ def _handle_dhcp_message(self) -> None: Only called when the FSM is in SELECTING or REQUESTING states. - :raises RuntimeError: If the FSM is in blocking mode and no valid response has + :raises TimeoutError: If the FSM is in blocking mode and no valid response has been received before the timeout expires. """ # pylint: disable=too-many-branches @@ -360,7 +360,9 @@ def _handle_dhcp_message(self) -> None: self._next_resend = self._next_retry_time_and_retry() if self._retries > self._max_retries: raise TimeoutError( - "No response from DHCP server after {}".format(self._max_retries) + "No response from DHCP server after {} retries.".format( + self._max_retries + ) ) if not self._blocking: return diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index b476d8e..d075dce 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -136,3 +136,112 @@ def test_with_wrong_message_type_on_socket_blocking( elif fsm_state == wiz_dhcp.STATE_REQUESTING: # Not called for STATE_REQUESTING dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() assert dhcp_mock_5k_with_socket._dhcp_state == next_state + + @freeze_time("2022-06-10") + def test_with_no_data_on_socket_blocking( + self, + dhcp_mock_5k_with_socket, + ): + # Mock the methods that will be checked for this test. + dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER + dhcp_mock_5k_with_socket._sock.available.side_effect = [0, 0, 32] + # Set up initial values for the test + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._blocking = True + # Test + dhcp_mock_5k_with_socket._handle_dhcp_message() + # Check response + assert dhcp_mock_5k_with_socket._sock.available.call_count == 3 + dhcp_mock_5k_with_socket._sock.recv.assert_called_once() + + @freeze_time("2022-06-10") + def test_with_no_data_on_socket_nonblocking( + self, + dhcp_mock_5k_with_socket, + ): + # Mock the methods that will be checked for this test. + dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER + dhcp_mock_5k_with_socket._sock.available.side_effect = [0, 0, 32] + # Set up initial values for the test + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._blocking = False + # Test + dhcp_mock_5k_with_socket._handle_dhcp_message() + # Check response + assert dhcp_mock_5k_with_socket._sock.available.call_count == 1 + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._sock.recv.assert_not_called() + + @freeze_time("2022-06-10") + def test_with_valueerror_nonblocking( + self, + dhcp_mock_5k_with_socket, + ): + # Mock the methods that will be checked for this test. + dhcp_mock_5k_with_socket._sock.available.return_value = 32 + dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = [ValueError] + # Set up initial values for the test + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._blocking = False + # Test + dhcp_mock_5k_with_socket._handle_dhcp_message() + # Check response + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_SELECTING + + @freeze_time("2022-06-10") + def test_with_valueerror_blocking( + self, + dhcp_mock_5k_with_socket, + ): + # Mock the methods that will be checked for this test. + dhcp_mock_5k_with_socket._sock.available.return_value = 32 + dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = [ + ValueError, + ValueError, + wiz_dhcp.DHCP_OFFER, + ] + # Set up initial values for the test + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._blocking = True + # Test + dhcp_mock_5k_with_socket._handle_dhcp_message() + # Check response + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 + ) + + @freeze_time("2022-06-10", auto_tick_seconds=1) + def test_timeout_blocking( + self, + dhcp_mock_5k_with_socket, + ): + # Mock the methods that will be checked for this test. + dhcp_mock_5k_with_socket._sock.available.return_value = 0 + # Set up initial values for the test + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._max_retries = 3 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._blocking = True + # Test + with pytest.raises(TimeoutError): + dhcp_mock_5k_with_socket._handle_dhcp_message() + + @freeze_time("2022-06-10", auto_tick_seconds=1) + def test_timeout_nonblocking( + self, + dhcp_mock_5k_with_socket, + ): + # Mock the methods that will be checked for this test. + dhcp_mock_5k_with_socket._sock.available.return_value = 0 + # Set up initial values for the test + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._max_retries = 3 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._blocking = False + # Test + dhcp_mock_5k_with_socket._handle_dhcp_message() + assert dhcp_mock_5k_with_socket._retries == 0 From f28030e3f37818ed57502ddc848e6638e40044b2 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 28 Dec 2022 19:02:10 +0300 Subject: [PATCH 17/80] Finished tests for _dhcp_state_machine --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 91 +++++------ ...files.py => test_dhcp_helper_functions.py} | 0 tests/test_dhcp_main_logic.py | 154 +++++++++++++++++- 3 files changed, 197 insertions(+), 48 deletions(-) rename tests/{test_dhcp_helper_files.py => test_dhcp_helper_functions.py} (100%) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 03f7b25..6b62928 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -316,6 +316,8 @@ def _handle_dhcp_message(self) -> None: """ # pylint: disable=too-many-branches global _BUFF # pylint: disable=global-statement + if self._dhcp_state == STATE_REQUESTING and self._renew: + self._dhcp_state = STATE_BOUND while True: while time.monotonic() < self._next_resend: if self._sock.available(): @@ -358,7 +360,7 @@ def _handle_dhcp_message(self) -> None: if not self._blocking: return self._next_resend = self._next_retry_time_and_retry() - if self._retries > self._max_retries: + if self._retries > self._max_retries and not self._renew: raise TimeoutError( "No response from DHCP server after {} retries.".format( self._max_retries @@ -370,57 +372,52 @@ def _handle_dhcp_message(self) -> None: def _dhcp_state_machine(self, *, blocking: bool = False) -> None: """I'll get to it""" - global _BUFF # pylint: disable=global-variable-not-assigned, global-statement self._blocking = blocking + if self._dhcp_state == STATE_BOUND: + now = time.monotonic() + if now < self._t1: + return + if now > self._lease_time: + self._blocking = True + self._dhcp_state = STATE_INIT + elif now > self._t2: + self._dhcp_state = STATE_REBINDING + else: + self._dhcp_state = STATE_RENEWING + + if self._dhcp_state == STATE_RENEWING: + self._renew = True + self._socket_setup() + self._start_time = time.monotonic() + self._send_message_set_next_state( + next_state=STATE_REQUESTING, + max_retries=3, + ) - while self._eth.link_status: - if self._dhcp_state == STATE_BOUND: - now = time.monotonic() - if now < self._t1: - return - if now > self._lease_time: - self._blocking = True - self._dhcp_state = STATE_INIT - elif now > self._t2: - self._dhcp_state = STATE_REBINDING - else: - self._dhcp_state = STATE_RENEWING - - if self._dhcp_state == STATE_RENEWING: - self._renew = True - self._socket_setup() - self._start_time = time.monotonic() - self._send_message_set_next_state( - next_state=STATE_REQUESTING, - max_retries=3, - ) - - if self._dhcp_state == STATE_REBINDING: - self._renew = True - self.dhcp_server_ip = BROADCAST_SERVER_ADDR - self._socket_setup() - self._send_message_set_next_state( - next_state=STATE_REQUESTING, - max_retries=3, - ) + if self._dhcp_state == STATE_REBINDING: + self._renew = True + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + self._socket_setup() + self._start_time = time.monotonic() + self._send_message_set_next_state( + next_state=STATE_REQUESTING, + max_retries=3, + ) - if self._dhcp_state == STATE_INIT: - self._dsm_reset() - self._send_message_set_next_state( - next_state=STATE_SELECTING, - max_retries=3, - ) + if self._dhcp_state == STATE_INIT: + self._dsm_reset() + self._send_message_set_next_state( + next_state=STATE_SELECTING, + max_retries=3, + ) - if self._dhcp_state == STATE_SELECTING: - self._max_retries = 3 - self._handle_dhcp_message() + if self._dhcp_state == STATE_SELECTING: + self._max_retries = 3 + self._handle_dhcp_message() - if self._dhcp_state == STATE_REQUESTING: - self._max_retries = 3 - self._handle_dhcp_message() - if not self._blocking: - break - self._blocking = False + if self._dhcp_state == STATE_REQUESTING: + self._max_retries = 3 + self._handle_dhcp_message() def _generate_dhcp_message( self, diff --git a/tests/test_dhcp_helper_files.py b/tests/test_dhcp_helper_functions.py similarity index 100% rename from tests/test_dhcp_helper_files.py rename to tests/test_dhcp_helper_functions.py diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index d075dce..59eb07c 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -27,6 +27,19 @@ def dhcp_mock_5k_with_socket(mocker): yield dhcp +@pytest.mark.parametrize("blocking", (True, False)) +def test_state_machine_blocking_set_correctly(dhcp_mock_5k_with_socket, blocking): + dhcp_mock_5k_with_socket._blocking = not blocking + dhcp_mock_5k_with_socket._dhcp_state_machine(blocking=blocking) + assert dhcp_mock_5k_with_socket._blocking is blocking + + +def test_state_machine_default_blocking(dhcp_mock_5k_with_socket): + dhcp_mock_5k_with_socket._blocking = True + dhcp_mock_5k_with_socket._dhcp_state_machine() + assert dhcp_mock_5k_with_socket._blocking is False + + class TestHandleDhcpMessage: """ Test that DHCP._handle_dhcp_message responds correctly with good and bad data while in @@ -51,13 +64,16 @@ def test_with_valid_data_on_socket_selecting(self, dhcp_mock_5k_with_socket): ) @freeze_time("2022-06-10") - def test_with_valid_data_on_socket_requesting(self, dhcp_mock_5k_with_socket): + def test_with_valid_data_on_socket_requesting_not_renew( + self, dhcp_mock_5k_with_socket + ): # Mock the methods that will be checked for this test. dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK # Set up initial values for the test dhcp_mock_5k_with_socket._sock.available.return_value = 24 dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + dhcp_mock_5k_with_socket._renew = False initial_transaction_id = dhcp_mock_5k_with_socket._transaction_id # Test dhcp_mock_5k_with_socket._handle_dhcp_message() @@ -89,6 +105,7 @@ def test_with_wrong_message_type_on_socket_nonblocking( dhcp_mock_5k_with_socket._sock.available.return_value = 32 dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = fsm_state + dhcp_mock_5k_with_socket._renew = False # Test dhcp_mock_5k_with_socket._handle_dhcp_message() # Check response @@ -126,6 +143,7 @@ def test_with_wrong_message_type_on_socket_blocking( dhcp_mock_5k_with_socket._sock.available.return_value = 32 dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = fsm_state + dhcp_mock_5k_with_socket._renew = False dhcp_mock_5k_with_socket._blocking = True # Test dhcp_mock_5k_with_socket._handle_dhcp_message() @@ -148,6 +166,7 @@ def test_with_no_data_on_socket_blocking( # Set up initial values for the test dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._renew = False dhcp_mock_5k_with_socket._blocking = True # Test dhcp_mock_5k_with_socket._handle_dhcp_message() @@ -166,6 +185,7 @@ def test_with_no_data_on_socket_nonblocking( # Set up initial values for the test dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._renew = False dhcp_mock_5k_with_socket._blocking = False # Test dhcp_mock_5k_with_socket._handle_dhcp_message() @@ -185,6 +205,7 @@ def test_with_valueerror_nonblocking( # Set up initial values for the test dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._renew = False dhcp_mock_5k_with_socket._blocking = False # Test dhcp_mock_5k_with_socket._handle_dhcp_message() @@ -206,6 +227,7 @@ def test_with_valueerror_blocking( # Set up initial values for the test dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._renew = False dhcp_mock_5k_with_socket._blocking = True # Test dhcp_mock_5k_with_socket._handle_dhcp_message() @@ -225,6 +247,7 @@ def test_timeout_blocking( dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._max_retries = 3 dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._renew = False dhcp_mock_5k_with_socket._blocking = True # Test with pytest.raises(TimeoutError): @@ -241,7 +264,136 @@ def test_timeout_nonblocking( dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 dhcp_mock_5k_with_socket._max_retries = 3 dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + dhcp_mock_5k_with_socket._renew = False dhcp_mock_5k_with_socket._blocking = False # Test dhcp_mock_5k_with_socket._handle_dhcp_message() assert dhcp_mock_5k_with_socket._retries == 0 + + @freeze_time("2022-06-10") + def test_requesting_with_renew_nak(self, dhcp_mock_5k_with_socket): + dhcp_mock_5k_with_socket._sock.available.return_value = 32 + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_NAK + dhcp_mock_5k_with_socket._renew = True + + dhcp_mock_5k_with_socket._handle_dhcp_message() + + assert dhcp_mock_5k_with_socket._renew is True + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND + + @freeze_time("2022-06-10") + def test_requesting_with_renew_no_data(self, dhcp_mock_5k_with_socket): + dhcp_mock_5k_with_socket._sock.available.return_value = 0 + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + dhcp_mock_5k_with_socket._renew = True + + dhcp_mock_5k_with_socket._handle_dhcp_message() + + assert dhcp_mock_5k_with_socket._renew is True + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND + + @freeze_time("2022-06-10", auto_tick_seconds=1000) + def test_requesting_with_timeout(self, dhcp_mock_5k_with_socket): + dhcp_mock_5k_with_socket._sock.available.return_value = 0 + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._retries = 3 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + dhcp_mock_5k_with_socket._renew = True + + dhcp_mock_5k_with_socket._handle_dhcp_message() + + assert dhcp_mock_5k_with_socket._renew is True + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND + + +class TestStateMachine: + def test_init_state(self, mocker, dhcp_mock_5k_with_socket): + mocker.patch.object(dhcp_mock_5k_with_socket, "_dsm_reset") + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_INIT + + dhcp_mock_5k_with_socket._dhcp_state_machine() + + dhcp_mock_5k_with_socket._dsm_reset.assert_called_once() + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + next_state=wiz_dhcp.STATE_SELECTING, max_retries=3 + ) + + def test_selecting_state(self, mocker, dhcp_mock_5k_with_socket): + mocker.patch.object(dhcp_mock_5k_with_socket, "_handle_dhcp_message") + + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + + dhcp_mock_5k_with_socket._dhcp_state_machine() + + assert dhcp_mock_5k_with_socket._max_retries == 3 + dhcp_mock_5k_with_socket._handle_dhcp_message.assert_called_once() + + def test_requesting_state(self, mocker, dhcp_mock_5k_with_socket): + mocker.patch.object(dhcp_mock_5k_with_socket, "_handle_dhcp_message") + + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + + dhcp_mock_5k_with_socket._dhcp_state_machine() + + assert dhcp_mock_5k_with_socket._max_retries == 3 + dhcp_mock_5k_with_socket._handle_dhcp_message.assert_called_once() + + @pytest.mark.parametrize( + "elapsed_time, expected_state", + ( + ( + 20.0, + wiz_dhcp.STATE_BOUND, + ), + (60.0, wiz_dhcp.STATE_RENEWING), + (110.0, wiz_dhcp.STATE_REBINDING), + (160.0, wiz_dhcp.STATE_INIT), + ), + ) + def test_bound_state(self, dhcp_mock_5k_with_socket, elapsed_time, expected_state): + with freeze_time("2022-10-12") as frozen_datetime: + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_BOUND + dhcp_mock_5k_with_socket._t1 = time.monotonic() + 50 + dhcp_mock_5k_with_socket._t2 = time.monotonic() + 100 + dhcp_mock_5k_with_socket._lease_time = time.monotonic() + 150 + + frozen_datetime.tick(elapsed_time) + + dhcp_mock_5k_with_socket._dhcp_state_machine() + + assert dhcp_mock_5k_with_socket._dhcp_state == expected_state + if expected_state == wiz_dhcp.STATE_INIT: + assert dhcp_mock_5k_with_socket._blocking is True + + @freeze_time("2022-10-15") + def test_renewing_state(self, mocker, dhcp_mock_5k_with_socket): + mocker.patch.object(dhcp_mock_5k_with_socket, "_socket_setup") + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_RENEWING + + dhcp_mock_5k_with_socket._dhcp_state_machine() + + assert dhcp_mock_5k_with_socket._renew is True + assert dhcp_mock_5k_with_socket._start_time == time.monotonic() + dhcp_mock_5k_with_socket._socket_setup.assert_called_once() + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 + ) + + @freeze_time("2022-10-15") + def test_rebinding_state(self, mocker, dhcp_mock_5k_with_socket): + mocker.patch.object(dhcp_mock_5k_with_socket, "_socket_setup") + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REBINDING + dhcp_mock_5k_with_socket._dhcp_server_ip = (8, 8, 8, 8) + + dhcp_mock_5k_with_socket._dhcp_state_machine() + + assert dhcp_mock_5k_with_socket.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR + assert dhcp_mock_5k_with_socket._renew is True + assert dhcp_mock_5k_with_socket._start_time == time.monotonic() + dhcp_mock_5k_with_socket._socket_setup.assert_called_once() + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 + ) From eb351df4e5c5cf9c5be71d43f847f2807fb6f328 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 28 Dec 2022 19:54:11 +0300 Subject: [PATCH 18/80] Refactored _handle_dhcp_message for readability. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 96 +++++++++++---------- tests/test_dhcp_main_logic.py | 22 ++++- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 6b62928..ad7c531 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -314,10 +314,40 @@ def _handle_dhcp_message(self) -> None: :raises TimeoutError: If the FSM is in blocking mode and no valid response has been received before the timeout expires. """ - # pylint: disable=too-many-branches global _BUFF # pylint: disable=global-statement - if self._dhcp_state == STATE_REQUESTING and self._renew: - self._dhcp_state = STATE_BOUND + + def state_selecting_processed(): + """Process a message while the FSM is in SELECTING state.""" + if self._dhcp_state == STATE_SELECTING and msg_type == DHCP_OFFER: + self._send_message_set_next_state( + next_state=STATE_REQUESTING, + max_retries=3, + ) + return True + return False + + def state_requesting_processed(): + """Process a message while the FSM is in REQUESTING state.""" + if self._dhcp_state == STATE_REQUESTING: + if msg_type == DHCP_NAK: + self._dhcp_state = STATE_INIT + return True + if msg_type == DHCP_ACK: + if self._lease_time == 0: + self._lease_time = DEFAULT_LEASE_TIME + self._t1 = self._start_time + self._lease_time // 2 + self._t2 = ( + self._start_time + self._lease_time - self._lease_time // 8 + ) + self._lease_time += self._start_time + self._increment_transaction_id() + self._renew = False + self._sock.close() + self._sock = None + self._dhcp_state = STATE_BOUND + return True + return False + while True: while time.monotonic() < self._next_resend: if self._sock.available(): @@ -328,35 +358,10 @@ def _handle_dhcp_message(self) -> None: if self._debug: print(error) else: - if ( - self._dhcp_state == STATE_SELECTING - and msg_type == DHCP_OFFER - ): - self._send_message_set_next_state( - next_state=STATE_REQUESTING, - max_retries=3, - ) + if state_selecting_processed(): + return + if state_requesting_processed(): return - if self._dhcp_state == STATE_REQUESTING: - if msg_type == DHCP_NAK: - self._dhcp_state = STATE_INIT - return - if msg_type == DHCP_ACK: - if self._lease_time == 0: - self._lease_time = DEFAULT_LEASE_TIME - self._t1 = self._start_time + self._lease_time // 2 - self._t2 = ( - self._start_time - + self._lease_time - - self._lease_time // 8 - ) - self._lease_time += self._start_time - self._increment_transaction_id() - self._renew = False - self._sock.close() - self._sock = None - self._dhcp_state = STATE_BOUND - return if not self._blocking: return self._next_resend = self._next_retry_time_and_retry() @@ -419,6 +424,9 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._max_retries = 3 self._handle_dhcp_message() + if self._renew: + self._dhcp_state = STATE_BOUND + def _generate_dhcp_message( self, *, @@ -437,25 +445,25 @@ def _generate_dhcp_message( """ def option_writer( - pointer: int, option_code: int, option_data: Union[Tuple[int, ...], bytes] + offset: int, option_code: int, option_data: Union[Tuple[int, ...], bytes] ) -> int: """Helper function to set DHCP option data for a DHCP message. - :param int pointer: Pointer to start of a DHCP option. + :param int offset: Pointer to start of a DHCP option. :param int option_code: Type of option to add. :param Tuple[int] option_data: The data for the option. :returns int: Pointer to next option. """ global _BUFF # pylint: disable=global-variable-not-assigned - _BUFF[pointer] = option_code + _BUFF[offset] = option_code data_length = len(option_data) - pointer += 1 - _BUFF[pointer] = data_length - pointer += 1 - data_end = pointer + data_length - _BUFF[pointer:data_end] = option_data + offset += 1 + _BUFF[offset] = data_length + offset += 1 + data_end = offset + data_length + _BUFF[offset:data_end] = option_data return data_end global _BUFF # pylint: disable=global-variable-not-assigned @@ -481,24 +489,24 @@ def option_writer( # Option - DHCP Message Type pointer = option_writer( - pointer=pointer, option_code=53, option_data=(message_type,) + offset=pointer, option_code=53, option_data=(message_type,) ) # Option - Host Name pointer = option_writer( - pointer=pointer, option_code=12, option_data=self._hostname + offset=pointer, option_code=12, option_data=self._hostname ) if message_type == DHCP_REQUEST: # Request subnet mask, router and DNS server. pointer = option_writer( - pointer=pointer, option_code=55, option_data=(1, 3, 6) + offset=pointer, option_code=55, option_data=(1, 3, 6) ) # Set Requested IP Address to offered IP address. pointer = option_writer( - pointer=pointer, option_code=50, option_data=self.local_ip + offset=pointer, option_code=50, option_data=self.local_ip ) # Set Server ID to chosen DHCP server IP address. pointer = option_writer( - pointer=pointer, option_code=54, option_data=self.dhcp_server_ip + offset=pointer, option_code=54, option_data=self.dhcp_server_ip ) _BUFF[pointer] = 0xFF diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index 59eb07c..1130c71 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -281,6 +281,20 @@ def test_requesting_with_renew_nak(self, dhcp_mock_5k_with_socket): dhcp_mock_5k_with_socket._handle_dhcp_message() assert dhcp_mock_5k_with_socket._renew is True + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_INIT + + @freeze_time("2022-06-10") + def test_requesting_with_renew_ack(self, dhcp_mock_5k_with_socket): + dhcp_mock_5k_with_socket._sock.available.return_value = 32 + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK + dhcp_mock_5k_with_socket._renew = True + + dhcp_mock_5k_with_socket._handle_dhcp_message() + + assert dhcp_mock_5k_with_socket._renew is False + assert dhcp_mock_5k_with_socket._sock is None assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND @freeze_time("2022-06-10") @@ -293,7 +307,7 @@ def test_requesting_with_renew_no_data(self, dhcp_mock_5k_with_socket): dhcp_mock_5k_with_socket._handle_dhcp_message() assert dhcp_mock_5k_with_socket._renew is True - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_REQUESTING @freeze_time("2022-06-10", auto_tick_seconds=1000) def test_requesting_with_timeout(self, dhcp_mock_5k_with_socket): @@ -306,7 +320,7 @@ def test_requesting_with_timeout(self, dhcp_mock_5k_with_socket): dhcp_mock_5k_with_socket._handle_dhcp_message() assert dhcp_mock_5k_with_socket._renew is True - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_REQUESTING class TestStateMachine: @@ -348,8 +362,8 @@ def test_requesting_state(self, mocker, dhcp_mock_5k_with_socket): 20.0, wiz_dhcp.STATE_BOUND, ), - (60.0, wiz_dhcp.STATE_RENEWING), - (110.0, wiz_dhcp.STATE_REBINDING), + (60.0, wiz_dhcp.STATE_BOUND), + (110.0, wiz_dhcp.STATE_BOUND), (160.0, wiz_dhcp.STATE_INIT), ), ) From 1723d68bf3ba2be0cf57dd8be60a7d793a1fbcc9 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 2 Jan 2023 17:22:50 +0300 Subject: [PATCH 19/80] Added some comments to explain tests. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 9 +- tests/test_dhcp_main_logic.py | 265 ++++++++++++++------ 2 files changed, 198 insertions(+), 76 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index ad7c531..5b1aec6 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -365,7 +365,9 @@ def state_requesting_processed(): if not self._blocking: return self._next_resend = self._next_retry_time_and_retry() - if self._retries > self._max_retries and not self._renew: + if self._retries > self._max_retries: + if self._renew: + return raise TimeoutError( "No response from DHCP server after {} retries.".format( self._max_retries @@ -375,7 +377,10 @@ def state_requesting_processed(): return def _dhcp_state_machine(self, *, blocking: bool = False) -> None: - """I'll get to it""" + """ + A finite state machine to allow the DHCP lease to be managed without blocking + the main program. The initial lease... + """ self._blocking = blocking if self._dhcp_state == STATE_BOUND: diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index 1130c71..a29ef21 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022 Martin Stephens # # SPDX-License-Identifier: MIT -"""Tests to confirm that there are no changes in behaviour to methods and functions. +"""Tests to confirm the behaviour of methods and functions in the finite state machine. These test are not exhaustive, but are a sanity check while making changes to the module.""" import time @@ -14,51 +14,67 @@ @pytest.fixture def dhcp_mock_5k_with_socket(mocker): """Instance of DHCP with mock WIZNET5K interface and a mock socket.""" + # Reset the send / receive buffer for each run test. wiz_dhcp._BUFF = b"" + # Mock the WIZNET5K class to factor its behaviour out of the tests and to control + # responses from methods. mock_wiznet5k = mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k.WIZNET5K", autospec=True ) + # Instantiate DHCP class for testing. dhcp = wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) + # Mock the socket for injecting recv() and available() values and to monitor calls + # to send() dhcp._sock = mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket.socket", autospec=True ) + # Mock the _parse_dhcp_response method to inject message types without supplying + # fake DHCP message packets. mocker.patch.object(dhcp, "_parse_dhcp_response", autospec=True) + # Mock the _send_message_set_next_state to monitor calls to it. mocker.patch.object(dhcp, "_send_message_set_next_state", autospec=True) yield dhcp @pytest.mark.parametrize("blocking", (True, False)) def test_state_machine_blocking_set_correctly(dhcp_mock_5k_with_socket, blocking): + # Set the initial state to the opposite of the attribute. dhcp_mock_5k_with_socket._blocking = not blocking + # Test. dhcp_mock_5k_with_socket._dhcp_state_machine(blocking=blocking) + # Check that the attribute is correct. assert dhcp_mock_5k_with_socket._blocking is blocking def test_state_machine_default_blocking(dhcp_mock_5k_with_socket): + # Default is False so set to True. dhcp_mock_5k_with_socket._blocking = True + # Test. dhcp_mock_5k_with_socket._dhcp_state_machine() + # Check that _blocking is False. assert dhcp_mock_5k_with_socket._blocking is False class TestHandleDhcpMessage: """ - Test that DHCP._handle_dhcp_message responds correctly with good and bad data while in - both blocking and none blocking modes. - """ + Test the _handle_dhcp_message() method.""" @freeze_time("2022-06-10") def test_with_valid_data_on_socket_selecting(self, dhcp_mock_5k_with_socket): - # Mock the methods that will be checked for this test. + # Set up initial values for the test. + # Start FSM in SELECTING state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Receive the expected OFFER message type. dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER - # Set up initial values for the test - wiz_dhcp._BUFF = b"" + # Have some data on the socket. dhcp_mock_5k_with_socket._sock.available.return_value = 24 + # Avoid a timeout before checking the message. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING - # Test + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - # Check response + # Confirm that the message would be parsed. dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() + # Confirm that the correct next FSM state was set. dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @@ -67,22 +83,30 @@ def test_with_valid_data_on_socket_selecting(self, dhcp_mock_5k_with_socket): def test_with_valid_data_on_socket_requesting_not_renew( self, dhcp_mock_5k_with_socket ): - # Mock the methods that will be checked for this test. + # Set up initial values for the test. + # Start FSM in REQUESTING state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Receive the expected OFFER message type. dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK - # Set up initial values for the test + # Have some data on the socket. dhcp_mock_5k_with_socket._sock.available.return_value = 24 + # Avoid a timeout before checking the message. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Test an initial negotiation, not a renewal or rebind. dhcp_mock_5k_with_socket._renew = False + # Store the transaction ID for comparison. initial_transaction_id = dhcp_mock_5k_with_socket._transaction_id - # Test + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - # Check response + # Transaction ID incremented. assert dhcp_mock_5k_with_socket._transaction_id == initial_transaction_id + 1 + # Renew has not changed. assert dhcp_mock_5k_with_socket._renew is False + # The socket has been released. assert dhcp_mock_5k_with_socket._sock is None + # The correct state has been set. assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND - dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() + # No DHCP message to be sent. dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() @freeze_time("2022-06-10") @@ -99,19 +123,26 @@ def test_with_wrong_message_type_on_socket_nonblocking( fsm_state, msg_type, ): - # Mock the methods that will be checked for this test. + # Set up initial values for the test. + # Start FSM in required state. dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = msg_type - # Set up initial values for the test + # Receive the incorrect message type. + dhcp_mock_5k_with_socket._dhcp_state = fsm_state + # Have some data on the socket. dhcp_mock_5k_with_socket._sock.available.return_value = 32 + # Avoid a timeout before checking the message. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_with_socket._dhcp_state = fsm_state + # Test an initial negotiation, not a renewal or rebind. dhcp_mock_5k_with_socket._renew = False - # Test + # Nonblocking mode. + dhcp_mock_5k_with_socket._blocking = False + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - # Check response + # Only one call to recv() (and therefore other methods) because nonblocking. dhcp_mock_5k_with_socket._sock.recv.assert_called_once() dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() + # Confirm that the FSM state has not changed. assert dhcp_mock_5k_with_socket._dhcp_state == fsm_state @freeze_time("2022-06-10") @@ -137,22 +168,29 @@ def test_with_wrong_message_type_on_socket_nonblocking( def test_with_wrong_message_type_on_socket_blocking( self, dhcp_mock_5k_with_socket, fsm_state, msg_type, next_state ): - # Mock the methods that will be checked for this test. + # Set up initial values for the test. + # Start FSM in required state. + dhcp_mock_5k_with_socket._dhcp_state = fsm_state + # Receive the incorrect message types, then a correct one. dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = msg_type - # Set up initial values for the test + # Have some data on the socket. dhcp_mock_5k_with_socket._sock.available.return_value = 32 + # Avoid a timeout before checking the message. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_with_socket._dhcp_state = fsm_state + # Test an initial negotiation, not a renewal or rebind. dhcp_mock_5k_with_socket._renew = False + # Put FSM into blocking mode so that multiple attempts are made. dhcp_mock_5k_with_socket._blocking = True - # Test + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - # Check response - assert dhcp_mock_5k_with_socket._parse_dhcp_response.call_count == 3 + # Confirm call count matches the number of messages received. + assert dhcp_mock_5k_with_socket._parse_dhcp_response.call_count == len(msg_type) + # Confirm correct calls to _send_message_set_next_state are made. if fsm_state == wiz_dhcp.STATE_SELECTING: dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once() elif fsm_state == wiz_dhcp.STATE_REQUESTING: # Not called for STATE_REQUESTING dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() + # Confirm correct final FSM state. assert dhcp_mock_5k_with_socket._dhcp_state == next_state @freeze_time("2022-06-10") @@ -160,18 +198,24 @@ def test_with_no_data_on_socket_blocking( self, dhcp_mock_5k_with_socket, ): - # Mock the methods that will be checked for this test. + # Set up initial values for the test. + # Start FSM in required state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Return a correct message type once data is on the socket. dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER + # No data on the socket, and finally some data. dhcp_mock_5k_with_socket._sock.available.side_effect = [0, 0, 32] - # Set up initial values for the test + # Avoid a timeout before checking the message. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Test an initial negotiation, not a renewal or rebind. dhcp_mock_5k_with_socket._renew = False + # Put FSM into blocking mode so that multiple attempts are made. dhcp_mock_5k_with_socket._blocking = True - # Test + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - # Check response + # Check available() called the correct number of times. assert dhcp_mock_5k_with_socket._sock.available.call_count == 3 + # Confirm that only one message was processed. dhcp_mock_5k_with_socket._sock.recv.assert_called_once() @freeze_time("2022-06-10") @@ -179,37 +223,56 @@ def test_with_no_data_on_socket_nonblocking( self, dhcp_mock_5k_with_socket, ): - # Mock the methods that will be checked for this test. + # Set up initial values for the test. + # Start FSM in required state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Return a correct message type if data is on the socket. dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER + # No data on the socket, and finally some data. dhcp_mock_5k_with_socket._sock.available.side_effect = [0, 0, 32] - # Set up initial values for the test + # Avoid a timeout before checking the message. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Test an initial negotiation, not a renewal or rebind. dhcp_mock_5k_with_socket._renew = False + # Put FSM into nonblocking mode so that a single attempt is made. dhcp_mock_5k_with_socket._blocking = False - # Test + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - # Check response - assert dhcp_mock_5k_with_socket._sock.available.call_count == 1 - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_SELECTING + # Available should only be called once in nonblocking mode. + dhcp_mock_5k_with_socket._sock.available.assert_called_once() + # Check that no data was read from the socket. dhcp_mock_5k_with_socket._sock.recv.assert_not_called() + # Confirm that the FSM state has not changed. + assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_SELECTING @freeze_time("2022-06-10") def test_with_valueerror_nonblocking( self, dhcp_mock_5k_with_socket, ): - # Mock the methods that will be checked for this test. + # Set up initial values for the test. + # Start FSM in required state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Raise exceptions due to bad DHCP messages. + dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = [ + ValueError, + ValueError, + ] + # Have some data on the socket. dhcp_mock_5k_with_socket._sock.available.return_value = 32 - dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = [ValueError] - # Set up initial values for the test + # Avoid a timeout before checking the message. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Test an initial negotiation, not a renewal or rebind. dhcp_mock_5k_with_socket._renew = False + # Put FSM into nonblocking mode so that a single attempt is made. dhcp_mock_5k_with_socket._blocking = False - # Test + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - # Check response + # Available should only be called once in nonblocking mode. + dhcp_mock_5k_with_socket._sock.available.assert_called_once() + # Confirm that _send_message_set_next_state not called. + dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() + # Check that FSM state has not changed. assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_SELECTING @freeze_time("2022-06-10") @@ -217,21 +280,28 @@ def test_with_valueerror_blocking( self, dhcp_mock_5k_with_socket, ): - # Mock the methods that will be checked for this test. - dhcp_mock_5k_with_socket._sock.available.return_value = 32 + # Set up initial values for the test. + # Start FSM in required state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Raise exceptions due to bad DHCP messages, then a good one. dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = [ ValueError, ValueError, wiz_dhcp.DHCP_OFFER, ] - # Set up initial values for the test + # Have some data on the socket. + dhcp_mock_5k_with_socket._sock.available.return_value = 32 + # Avoid a timeout before checking the message. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Test an initial negotiation, not a renewal or rebind. dhcp_mock_5k_with_socket._renew = False + # Put FSM into blocking mode so that multiple attempts are made. dhcp_mock_5k_with_socket._blocking = True - # Test + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - # Check response + # Check available() called three times. + assert dhcp_mock_5k_with_socket._sock.available.call_count == 3 + # Confirm that _send_message_set_next_state was called to change FSM state. dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @@ -241,15 +311,20 @@ def test_timeout_blocking( self, dhcp_mock_5k_with_socket, ): - # Mock the methods that will be checked for this test. + # Set up initial values for the test. + # Start FSM in required state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Never have data on the socket to force a timeout. dhcp_mock_5k_with_socket._sock.available.return_value = 0 - # Set up initial values for the test + # Set an initial value for timeout. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + # Set maximum retries to 3. dhcp_mock_5k_with_socket._max_retries = 3 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Test an initial negotiation, not a renewal or rebind. dhcp_mock_5k_with_socket._renew = False + # Put FSM into blocking mode so that multiple attempts are made. dhcp_mock_5k_with_socket._blocking = True - # Test + # Test that a TimeoutError is raised. with pytest.raises(TimeoutError): dhcp_mock_5k_with_socket._handle_dhcp_message() @@ -258,68 +333,110 @@ def test_timeout_nonblocking( self, dhcp_mock_5k_with_socket, ): - # Mock the methods that will be checked for this test. + # Set up initial values for the test. + # Start FSM in required state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Never have data on the socket to force a timeout. dhcp_mock_5k_with_socket._sock.available.return_value = 0 # Set up initial values for the test dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + # Set maximum retries to 3. dhcp_mock_5k_with_socket._max_retries = 3 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + # Test an initial negotiation, not a renewal or rebind. dhcp_mock_5k_with_socket._renew = False + # Put FSM into nonblocking mode so that a single attempt is made. dhcp_mock_5k_with_socket._blocking = False - # Test + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() + # Confirm that the retries was not incremented, i.e. the loop executed once. assert dhcp_mock_5k_with_socket._retries == 0 @freeze_time("2022-06-10") def test_requesting_with_renew_nak(self, dhcp_mock_5k_with_socket): - dhcp_mock_5k_with_socket._sock.available.return_value = 32 - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + # Set up initial values for the test. + # Start FSM in required state. dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Return a correct message type. dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_NAK + # Have some data on the socket. + dhcp_mock_5k_with_socket._sock.available.return_value = 32 + # Avoid a timeout before checking the message. + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + # Put FSM into nonblocking mode so that a single attempt is made. + dhcp_mock_5k_with_socket._blocking = False + # Test a renewal or rebind. dhcp_mock_5k_with_socket._renew = True - + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - + # Confirm _renew remains True assert dhcp_mock_5k_with_socket._renew is True + # Confirm that a NAK puts the FSM into the INIT state. assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_INIT @freeze_time("2022-06-10") def test_requesting_with_renew_ack(self, dhcp_mock_5k_with_socket): - dhcp_mock_5k_with_socket._sock.available.return_value = 32 - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + # Set up initial values for the test. + # Start FSM in required state. dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Return a correct message type. dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK + # Have some data on the socket. + dhcp_mock_5k_with_socket._sock.available.return_value = 32 + # Avoid a timeout before checking the message. + dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + # Put FSM into nonblocking mode so that a single attempt is made. + dhcp_mock_5k_with_socket._blocking = False + # Test a renewal or rebind. dhcp_mock_5k_with_socket._renew = True - + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - + # Lease renewed so confirm _renew is False. assert dhcp_mock_5k_with_socket._renew is False + # Confirm that socket was released. assert dhcp_mock_5k_with_socket._sock is None + # Confirm that the state is BOUND. assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND @freeze_time("2022-06-10") def test_requesting_with_renew_no_data(self, dhcp_mock_5k_with_socket): + # Set up initial values for the test. + # Start FSM in required state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Never have data on the socket. dhcp_mock_5k_with_socket._sock.available.return_value = 0 + # Initial timeout value. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Put FSM into nonblocking mode so that a single attempt is made. + dhcp_mock_5k_with_socket._blocking = False + # Test a renewal or rebind. dhcp_mock_5k_with_socket._renew = True - + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - + # Confirm that _renew remains True assert dhcp_mock_5k_with_socket._renew is True + # Confirm that state remains as REQUESTING assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_REQUESTING - @freeze_time("2022-06-10", auto_tick_seconds=1000) - def test_requesting_with_timeout(self, dhcp_mock_5k_with_socket): + @freeze_time("2022-06-10", auto_tick_seconds=60) + def test_requesting_with_timeout_renew(self, dhcp_mock_5k_with_socket): + # Set up initial values for the test. + # Start FSM in required state. + dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Never have data on the socket. dhcp_mock_5k_with_socket._sock.available.return_value = 0 + # Initial timeout value. dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + # Set retries to 3 so that it times out on the first pass. dhcp_mock_5k_with_socket._retries = 3 - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Test a renewal or rebind. dhcp_mock_5k_with_socket._renew = True - + # Put FSM into blocking mode so that multiple attempts are made. + dhcp_mock_5k_with_socket._blocking = True + # Test. dhcp_mock_5k_with_socket._handle_dhcp_message() - + # Confirm that _renew remains True assert dhcp_mock_5k_with_socket._renew is True + # Confirm that state remains as REQUESTING assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_REQUESTING From c0cc5391527650b03d8bbe289428598660ab3669 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 3 Jan 2023 14:03:13 +0300 Subject: [PATCH 20/80] Implemented a DHCP packet receiver that only relies on standard socket.socket methods. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 51 ++- tests/test_dhcp_helper_functions.py | 68 ++++ tests/test_dhcp_main_logic.py | 400 ++++++++++---------- 3 files changed, 308 insertions(+), 211 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 5b1aec6..6dffa64 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -91,16 +91,16 @@ class DHCP: to run in a non-blocking mode suitable for CircuitPython. The DHCP client obtains a lease and maintains it. The process of obtaining the initial - lease is best run in a blocking mode, as several messages must be exchanged with the DHCP + lease is run in a blocking mode, as several messages must be exchanged with the DHCP server. Once the lease has been allocated, lease maintenance can be performed in non-blocking mode as nothing needs to be done until it is time to reallocate the lease. Renewing or rebinding is a simpler process which may be repeated periodically until successful. If the lease expires, the client attempts to obtain a new lease in blocking mode when the maintenance routine is run. - In most circumstances, call `DHCP.request_lease` in blocking mode to obtain a - lease, then periodically call `DHCP.maintain_lease` in non-blocking mode so that the - FSM can check whether the lease needs to be renewed, and can then renew it. + In most circumstances, call `DHCP.request_lease` to obtain a lease, then periodically call + `DHCP.maintain_lease` in non-blocking mode so that the FSM can check whether the lease + needs to be renewed, and can then renew it. Since DHCP uses UDP, messages may be lost. The DHCP protocol uses exponential backoff for retrying. Retries occur after 4, 8, and 16 seconds (the final retry is followed by @@ -302,6 +302,38 @@ def _send_message_set_next_state( self._retries = 0 self._dhcp_state = next_state + def _receive_dhcp_response(self) -> int: + """ + Receive data from the socket in response to a DHCP query. + + Reads data from the buffer until a viable minimum packet size has been + received or the operation times out. If a viable packet is received, it is + stored in the global buffer and the number of bytes received is returned. + If the packet is too short, it is discarded and zero is returned. The + maximum packet size is limited by the size of the global buffer. + + :returns int: The number of bytes stored in the global buffer. + """ + # DHCP returns the query plus additional data. The query length is 236 bytes. + minimum_packet_length = 236 + buffer = bytearray(b"") + bytes_read = 0 + while ( + bytes_read <= minimum_packet_length and time.monotonic() < self._next_resend + ): + buffer.extend(self._sock.recv(BUFF_LENGTH - bytes_read)) + bytes_read = len(buffer) + if bytes_read == BUFF_LENGTH: + break + if bytes_read < minimum_packet_length: + bytes_read = 0 + else: + _BUFF[:bytes_read] = buffer + _BUFF[bytes_read:] = bytearray(BUFF_LENGTH - bytes_read) + del buffer + gc.collect() + return bytes_read + def _handle_dhcp_message(self) -> None: """Receive and process DHCP message then update the finite state machine (FSM). @@ -316,7 +348,7 @@ def _handle_dhcp_message(self) -> None: """ global _BUFF # pylint: disable=global-statement - def state_selecting_processed(): + def processing_state_selecting(): """Process a message while the FSM is in SELECTING state.""" if self._dhcp_state == STATE_SELECTING and msg_type == DHCP_OFFER: self._send_message_set_next_state( @@ -326,7 +358,7 @@ def state_selecting_processed(): return True return False - def state_requesting_processed(): + def processing_state_requesting(): """Process a message while the FSM is in REQUESTING state.""" if self._dhcp_state == STATE_REQUESTING: if msg_type == DHCP_NAK: @@ -342,8 +374,7 @@ def state_requesting_processed(): self._lease_time += self._start_time self._increment_transaction_id() self._renew = False - self._sock.close() - self._sock = None + self._socket_release() self._dhcp_state = STATE_BOUND return True return False @@ -358,9 +389,9 @@ def state_requesting_processed(): if self._debug: print(error) else: - if state_selecting_processed(): + if processing_state_selecting(): return - if state_requesting_processed(): + if processing_state_requesting(): return if not self._blocking: return diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 23bf11c..20cfe2c 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -27,6 +27,15 @@ def mock_socket(mocker): ) +@pytest.fixture +def mock_dhcp(mocker, mock_wiznet5k): + dhcp = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp._sock = mocker.patch( + "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket.socket", autospec=True + ) + return dhcp + + class TestDHCPInit: def test_constants(self): """Test all the constants in the DHCP module.""" @@ -547,3 +556,62 @@ def test_send_message_set_next_state_bad_state(self, mock_wiznet5k, next_state): dhcp_client._send_message_set_next_state( next_state=next_state, max_retries=4 ) + + +class TestReceiveResponse: + + minimum_packet_length = 236 + + @freeze_time("2022-10-10") + @pytest.mark.parametrize( + "bytes_on_socket", (wiz_dhcp.BUFF_LENGTH, minimum_packet_length + 1) + ) + def test_receive_response_good_data(self, mock_dhcp, bytes_on_socket): + mock_dhcp._next_resend = time.monotonic() + 15 + mock_dhcp._sock.recv.return_value = bytes([0] * bytes_on_socket) + response = mock_dhcp._receive_dhcp_response() + assert response == bytes_on_socket + assert response > 236 + + @freeze_time("2022-10-10") + @pytest.mark.parametrize( + "bytes_on_socket", ([bytes([0] * minimum_packet_length), bytes([0] * 1)],) + ) + def test_receive_response_short_packet(self, mock_dhcp, bytes_on_socket): + mock_dhcp._next_resend = time.monotonic() + 15 + mock_dhcp._sock.recv.side_effect = bytes_on_socket + assert mock_dhcp._receive_dhcp_response() > 236 + + @freeze_time("2022-10-10", auto_tick_seconds=5) + def test_timeout(self, mock_dhcp): + mock_dhcp._next_resend = time.monotonic() + 15 + mock_dhcp._sock.recv.side_effect = [b"", b"", b"", b"", b"", bytes([0] * 240)] + assert mock_dhcp._receive_dhcp_response() == 0 + + @freeze_time("2022-10-10") + @pytest.mark.parametrize("bytes_returned", ([240], [230, 30])) + def test_buffer_handling(self, mock_dhcp, bytes_returned): + total_bytes = sum(bytes_returned) + mock_dhcp._next_resend = time.monotonic() + 15 + wiz_dhcp._BUFF = bytearray([1] * wiz_dhcp.BUFF_LENGTH) + expected_result = bytearray([2] * total_bytes) + ( + bytes([0] * (wiz_dhcp.BUFF_LENGTH - total_bytes)) + ) + mock_dhcp._sock.recv.side_effect = (bytes([2] * x) for x in bytes_returned) + assert mock_dhcp._receive_dhcp_response() == total_bytes + assert wiz_dhcp._BUFF == expected_result + + @freeze_time("2022-10-10") + def test_buffer_does_not_overrun(self, mocker, mock_dhcp): + mock_dhcp._next_resend = time.monotonic() + 15 + mock_dhcp._sock.recv.return_value = bytes([2] * wiz_dhcp.BUFF_LENGTH) + mock_dhcp._receive_dhcp_response() + mock_dhcp._sock.recv.assert_called_once_with(wiz_dhcp.BUFF_LENGTH) + mock_dhcp._sock.recv.reset_mock() + mock_dhcp._sock.recv.side_effect = [bytes([2] * 200), bytes([2] * 118)] + mock_dhcp._receive_dhcp_response() + assert mock_dhcp._sock.recv.call_count == 2 + assert mock_dhcp._sock.recv.call_args_list == [ + mocker.call(318), + mocker.call(118), + ] diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index a29ef21..f6e6ea1 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -12,7 +12,7 @@ @pytest.fixture -def dhcp_mock_5k_with_socket(mocker): +def mock_dhcp(mocker): """Instance of DHCP with mock WIZNET5K interface and a mock socket.""" # Reset the send / receive buffer for each run test. wiz_dhcp._BUFF = b"" @@ -37,22 +37,22 @@ def dhcp_mock_5k_with_socket(mocker): @pytest.mark.parametrize("blocking", (True, False)) -def test_state_machine_blocking_set_correctly(dhcp_mock_5k_with_socket, blocking): +def test_state_machine_blocking_set_correctly(mock_dhcp, blocking): # Set the initial state to the opposite of the attribute. - dhcp_mock_5k_with_socket._blocking = not blocking + mock_dhcp._blocking = not blocking # Test. - dhcp_mock_5k_with_socket._dhcp_state_machine(blocking=blocking) + mock_dhcp._dhcp_state_machine(blocking=blocking) # Check that the attribute is correct. - assert dhcp_mock_5k_with_socket._blocking is blocking + assert mock_dhcp._blocking is blocking -def test_state_machine_default_blocking(dhcp_mock_5k_with_socket): +def test_state_machine_default_blocking(mock_dhcp): # Default is False so set to True. - dhcp_mock_5k_with_socket._blocking = True + mock_dhcp._blocking = True # Test. - dhcp_mock_5k_with_socket._dhcp_state_machine() + mock_dhcp._dhcp_state_machine() # Check that _blocking is False. - assert dhcp_mock_5k_with_socket._blocking is False + assert mock_dhcp._blocking is False class TestHandleDhcpMessage: @@ -60,54 +60,52 @@ class TestHandleDhcpMessage: Test the _handle_dhcp_message() method.""" @freeze_time("2022-06-10") - def test_with_valid_data_on_socket_selecting(self, dhcp_mock_5k_with_socket): + def test_with_valid_data_on_socket_selecting(self, mock_dhcp): # Set up initial values for the test. # Start FSM in SELECTING state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING # Receive the expected OFFER message type. - dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER + mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER # Have some data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 24 + mock_dhcp._sock.available.return_value = 24 # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Confirm that the message would be parsed. - dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() + mock_dhcp._parse_dhcp_response.assert_called_once() # Confirm that the correct next FSM state was set. - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + mock_dhcp._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @freeze_time("2022-06-10") - def test_with_valid_data_on_socket_requesting_not_renew( - self, dhcp_mock_5k_with_socket - ): + def test_with_valid_data_on_socket_requesting_not_renew(self, mock_dhcp): # Set up initial values for the test. # Start FSM in REQUESTING state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING # Receive the expected OFFER message type. - dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK + mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK # Have some data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 24 + mock_dhcp._sock.available.return_value = 24 # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. - dhcp_mock_5k_with_socket._renew = False + mock_dhcp._renew = False # Store the transaction ID for comparison. - initial_transaction_id = dhcp_mock_5k_with_socket._transaction_id + initial_transaction_id = mock_dhcp._transaction_id # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Transaction ID incremented. - assert dhcp_mock_5k_with_socket._transaction_id == initial_transaction_id + 1 + assert mock_dhcp._transaction_id == initial_transaction_id + 1 # Renew has not changed. - assert dhcp_mock_5k_with_socket._renew is False + assert mock_dhcp._renew is False # The socket has been released. - assert dhcp_mock_5k_with_socket._sock is None + assert mock_dhcp._sock is None # The correct state has been set. - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_BOUND # No DHCP message to be sent. - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() + mock_dhcp._send_message_set_next_state.assert_not_called() @freeze_time("2022-06-10") @pytest.mark.parametrize( @@ -119,31 +117,31 @@ def test_with_valid_data_on_socket_requesting_not_renew( ) def test_with_wrong_message_type_on_socket_nonblocking( self, - dhcp_mock_5k_with_socket, + mock_dhcp, fsm_state, msg_type, ): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = msg_type + mock_dhcp._parse_dhcp_response.return_value = msg_type # Receive the incorrect message type. - dhcp_mock_5k_with_socket._dhcp_state = fsm_state + mock_dhcp._dhcp_state = fsm_state # Have some data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 32 + mock_dhcp._sock.available.return_value = 32 # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. - dhcp_mock_5k_with_socket._renew = False + mock_dhcp._renew = False # Nonblocking mode. - dhcp_mock_5k_with_socket._blocking = False + mock_dhcp._blocking = False # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Only one call to recv() (and therefore other methods) because nonblocking. - dhcp_mock_5k_with_socket._sock.recv.assert_called_once() - dhcp_mock_5k_with_socket._parse_dhcp_response.assert_called_once() - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() + mock_dhcp._sock.recv.assert_called_once() + mock_dhcp._parse_dhcp_response.assert_called_once() + mock_dhcp._send_message_set_next_state.assert_not_called() # Confirm that the FSM state has not changed. - assert dhcp_mock_5k_with_socket._dhcp_state == fsm_state + assert mock_dhcp._dhcp_state == fsm_state @freeze_time("2022-06-10") @pytest.mark.parametrize( @@ -166,311 +164,311 @@ def test_with_wrong_message_type_on_socket_nonblocking( ), ) def test_with_wrong_message_type_on_socket_blocking( - self, dhcp_mock_5k_with_socket, fsm_state, msg_type, next_state + self, mock_dhcp, fsm_state, msg_type, next_state ): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = fsm_state + mock_dhcp._dhcp_state = fsm_state # Receive the incorrect message types, then a correct one. - dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = msg_type + mock_dhcp._parse_dhcp_response.side_effect = msg_type # Have some data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 32 + mock_dhcp._sock.available.return_value = 32 # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. - dhcp_mock_5k_with_socket._renew = False + mock_dhcp._renew = False # Put FSM into blocking mode so that multiple attempts are made. - dhcp_mock_5k_with_socket._blocking = True + mock_dhcp._blocking = True # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Confirm call count matches the number of messages received. - assert dhcp_mock_5k_with_socket._parse_dhcp_response.call_count == len(msg_type) + assert mock_dhcp._parse_dhcp_response.call_count == len(msg_type) # Confirm correct calls to _send_message_set_next_state are made. if fsm_state == wiz_dhcp.STATE_SELECTING: - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once() + mock_dhcp._send_message_set_next_state.assert_called_once() elif fsm_state == wiz_dhcp.STATE_REQUESTING: # Not called for STATE_REQUESTING - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() + mock_dhcp._send_message_set_next_state.assert_not_called() # Confirm correct final FSM state. - assert dhcp_mock_5k_with_socket._dhcp_state == next_state + assert mock_dhcp._dhcp_state == next_state @freeze_time("2022-06-10") def test_with_no_data_on_socket_blocking( self, - dhcp_mock_5k_with_socket, + mock_dhcp, ): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING # Return a correct message type once data is on the socket. - dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER + mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER # No data on the socket, and finally some data. - dhcp_mock_5k_with_socket._sock.available.side_effect = [0, 0, 32] + mock_dhcp._sock.available.side_effect = [0, 0, 32] # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. - dhcp_mock_5k_with_socket._renew = False + mock_dhcp._renew = False # Put FSM into blocking mode so that multiple attempts are made. - dhcp_mock_5k_with_socket._blocking = True + mock_dhcp._blocking = True # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Check available() called the correct number of times. - assert dhcp_mock_5k_with_socket._sock.available.call_count == 3 + assert mock_dhcp._sock.available.call_count == 3 # Confirm that only one message was processed. - dhcp_mock_5k_with_socket._sock.recv.assert_called_once() + mock_dhcp._sock.recv.assert_called_once() @freeze_time("2022-06-10") def test_with_no_data_on_socket_nonblocking( self, - dhcp_mock_5k_with_socket, + mock_dhcp, ): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING # Return a correct message type if data is on the socket. - dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER + mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER # No data on the socket, and finally some data. - dhcp_mock_5k_with_socket._sock.available.side_effect = [0, 0, 32] + mock_dhcp._sock.available.side_effect = [0, 0, 32] # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. - dhcp_mock_5k_with_socket._renew = False + mock_dhcp._renew = False # Put FSM into nonblocking mode so that a single attempt is made. - dhcp_mock_5k_with_socket._blocking = False + mock_dhcp._blocking = False # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Available should only be called once in nonblocking mode. - dhcp_mock_5k_with_socket._sock.available.assert_called_once() + mock_dhcp._sock.available.assert_called_once() # Check that no data was read from the socket. - dhcp_mock_5k_with_socket._sock.recv.assert_not_called() + mock_dhcp._sock.recv.assert_not_called() # Confirm that the FSM state has not changed. - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_SELECTING + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_SELECTING @freeze_time("2022-06-10") def test_with_valueerror_nonblocking( self, - dhcp_mock_5k_with_socket, + mock_dhcp, ): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING # Raise exceptions due to bad DHCP messages. - dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = [ + mock_dhcp._parse_dhcp_response.side_effect = [ ValueError, ValueError, ] # Have some data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 32 + mock_dhcp._sock.available.return_value = 32 # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. - dhcp_mock_5k_with_socket._renew = False + mock_dhcp._renew = False # Put FSM into nonblocking mode so that a single attempt is made. - dhcp_mock_5k_with_socket._blocking = False + mock_dhcp._blocking = False # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Available should only be called once in nonblocking mode. - dhcp_mock_5k_with_socket._sock.available.assert_called_once() + mock_dhcp._sock.available.assert_called_once() # Confirm that _send_message_set_next_state not called. - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_not_called() + mock_dhcp._send_message_set_next_state.assert_not_called() # Check that FSM state has not changed. - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_SELECTING + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_SELECTING @freeze_time("2022-06-10") def test_with_valueerror_blocking( self, - dhcp_mock_5k_with_socket, + mock_dhcp, ): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING # Raise exceptions due to bad DHCP messages, then a good one. - dhcp_mock_5k_with_socket._parse_dhcp_response.side_effect = [ + mock_dhcp._parse_dhcp_response.side_effect = [ ValueError, ValueError, wiz_dhcp.DHCP_OFFER, ] # Have some data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 32 + mock_dhcp._sock.available.return_value = 32 # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. - dhcp_mock_5k_with_socket._renew = False + mock_dhcp._renew = False # Put FSM into blocking mode so that multiple attempts are made. - dhcp_mock_5k_with_socket._blocking = True + mock_dhcp._blocking = True # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Check available() called three times. - assert dhcp_mock_5k_with_socket._sock.available.call_count == 3 + assert mock_dhcp._sock.available.call_count == 3 # Confirm that _send_message_set_next_state was called to change FSM state. - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + mock_dhcp._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @freeze_time("2022-06-10", auto_tick_seconds=1) def test_timeout_blocking( self, - dhcp_mock_5k_with_socket, + mock_dhcp, ): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING # Never have data on the socket to force a timeout. - dhcp_mock_5k_with_socket._sock.available.return_value = 0 + mock_dhcp._sock.available.return_value = 0 # Set an initial value for timeout. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Set maximum retries to 3. - dhcp_mock_5k_with_socket._max_retries = 3 + mock_dhcp._max_retries = 3 # Test an initial negotiation, not a renewal or rebind. - dhcp_mock_5k_with_socket._renew = False + mock_dhcp._renew = False # Put FSM into blocking mode so that multiple attempts are made. - dhcp_mock_5k_with_socket._blocking = True + mock_dhcp._blocking = True # Test that a TimeoutError is raised. with pytest.raises(TimeoutError): - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() @freeze_time("2022-06-10", auto_tick_seconds=1) def test_timeout_nonblocking( self, - dhcp_mock_5k_with_socket, + mock_dhcp, ): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING # Never have data on the socket to force a timeout. - dhcp_mock_5k_with_socket._sock.available.return_value = 0 + mock_dhcp._sock.available.return_value = 0 # Set up initial values for the test - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Set maximum retries to 3. - dhcp_mock_5k_with_socket._max_retries = 3 + mock_dhcp._max_retries = 3 # Test an initial negotiation, not a renewal or rebind. - dhcp_mock_5k_with_socket._renew = False + mock_dhcp._renew = False # Put FSM into nonblocking mode so that a single attempt is made. - dhcp_mock_5k_with_socket._blocking = False + mock_dhcp._blocking = False # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Confirm that the retries was not incremented, i.e. the loop executed once. - assert dhcp_mock_5k_with_socket._retries == 0 + assert mock_dhcp._retries == 0 @freeze_time("2022-06-10") - def test_requesting_with_renew_nak(self, dhcp_mock_5k_with_socket): + def test_requesting_with_renew_nak(self, mock_dhcp): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING # Return a correct message type. - dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_NAK + mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_NAK # Have some data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 32 + mock_dhcp._sock.available.return_value = 32 # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Put FSM into nonblocking mode so that a single attempt is made. - dhcp_mock_5k_with_socket._blocking = False + mock_dhcp._blocking = False # Test a renewal or rebind. - dhcp_mock_5k_with_socket._renew = True + mock_dhcp._renew = True # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Confirm _renew remains True - assert dhcp_mock_5k_with_socket._renew is True + assert mock_dhcp._renew is True # Confirm that a NAK puts the FSM into the INIT state. - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_INIT + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_INIT @freeze_time("2022-06-10") - def test_requesting_with_renew_ack(self, dhcp_mock_5k_with_socket): + def test_requesting_with_renew_ack(self, mock_dhcp): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING # Return a correct message type. - dhcp_mock_5k_with_socket._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK + mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK # Have some data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 32 + mock_dhcp._sock.available.return_value = 32 # Avoid a timeout before checking the message. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Put FSM into nonblocking mode so that a single attempt is made. - dhcp_mock_5k_with_socket._blocking = False + mock_dhcp._blocking = False # Test a renewal or rebind. - dhcp_mock_5k_with_socket._renew = True + mock_dhcp._renew = True # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Lease renewed so confirm _renew is False. - assert dhcp_mock_5k_with_socket._renew is False + assert mock_dhcp._renew is False # Confirm that socket was released. - assert dhcp_mock_5k_with_socket._sock is None + assert mock_dhcp._sock is None # Confirm that the state is BOUND. - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_BOUND + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_BOUND @freeze_time("2022-06-10") - def test_requesting_with_renew_no_data(self, dhcp_mock_5k_with_socket): + def test_requesting_with_renew_no_data(self, mock_dhcp): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING # Never have data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 0 + mock_dhcp._sock.available.return_value = 0 # Initial timeout value. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Put FSM into nonblocking mode so that a single attempt is made. - dhcp_mock_5k_with_socket._blocking = False + mock_dhcp._blocking = False # Test a renewal or rebind. - dhcp_mock_5k_with_socket._renew = True + mock_dhcp._renew = True # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Confirm that _renew remains True - assert dhcp_mock_5k_with_socket._renew is True + assert mock_dhcp._renew is True # Confirm that state remains as REQUESTING - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_REQUESTING + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_REQUESTING @freeze_time("2022-06-10", auto_tick_seconds=60) - def test_requesting_with_timeout_renew(self, dhcp_mock_5k_with_socket): + def test_requesting_with_timeout_renew(self, mock_dhcp): # Set up initial values for the test. # Start FSM in required state. - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING # Never have data on the socket. - dhcp_mock_5k_with_socket._sock.available.return_value = 0 + mock_dhcp._sock.available.return_value = 0 # Initial timeout value. - dhcp_mock_5k_with_socket._next_resend = time.monotonic() + 5 + mock_dhcp._next_resend = time.monotonic() + 5 # Set retries to 3 so that it times out on the first pass. - dhcp_mock_5k_with_socket._retries = 3 + mock_dhcp._retries = 3 # Test a renewal or rebind. - dhcp_mock_5k_with_socket._renew = True + mock_dhcp._renew = True # Put FSM into blocking mode so that multiple attempts are made. - dhcp_mock_5k_with_socket._blocking = True + mock_dhcp._blocking = True # Test. - dhcp_mock_5k_with_socket._handle_dhcp_message() + mock_dhcp._handle_dhcp_message() # Confirm that _renew remains True - assert dhcp_mock_5k_with_socket._renew is True + assert mock_dhcp._renew is True # Confirm that state remains as REQUESTING - assert dhcp_mock_5k_with_socket._dhcp_state == wiz_dhcp.STATE_REQUESTING + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_REQUESTING class TestStateMachine: - def test_init_state(self, mocker, dhcp_mock_5k_with_socket): - mocker.patch.object(dhcp_mock_5k_with_socket, "_dsm_reset") - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_INIT + def test_init_state(self, mocker, mock_dhcp): + mocker.patch.object(mock_dhcp, "_dsm_reset") + mock_dhcp._dhcp_state = wiz_dhcp.STATE_INIT - dhcp_mock_5k_with_socket._dhcp_state_machine() + mock_dhcp._dhcp_state_machine() - dhcp_mock_5k_with_socket._dsm_reset.assert_called_once() - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + mock_dhcp._dsm_reset.assert_called_once() + mock_dhcp._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_SELECTING, max_retries=3 ) - def test_selecting_state(self, mocker, dhcp_mock_5k_with_socket): - mocker.patch.object(dhcp_mock_5k_with_socket, "_handle_dhcp_message") + def test_selecting_state(self, mocker, mock_dhcp): + mocker.patch.object(mock_dhcp, "_handle_dhcp_message") - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_SELECTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - dhcp_mock_5k_with_socket._dhcp_state_machine() + mock_dhcp._dhcp_state_machine() - assert dhcp_mock_5k_with_socket._max_retries == 3 - dhcp_mock_5k_with_socket._handle_dhcp_message.assert_called_once() + assert mock_dhcp._max_retries == 3 + mock_dhcp._handle_dhcp_message.assert_called_once() - def test_requesting_state(self, mocker, dhcp_mock_5k_with_socket): - mocker.patch.object(dhcp_mock_5k_with_socket, "_handle_dhcp_message") + def test_requesting_state(self, mocker, mock_dhcp): + mocker.patch.object(mock_dhcp, "_handle_dhcp_message") - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REQUESTING + mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING - dhcp_mock_5k_with_socket._dhcp_state_machine() + mock_dhcp._dhcp_state_machine() - assert dhcp_mock_5k_with_socket._max_retries == 3 - dhcp_mock_5k_with_socket._handle_dhcp_message.assert_called_once() + assert mock_dhcp._max_retries == 3 + mock_dhcp._handle_dhcp_message.assert_called_once() @pytest.mark.parametrize( "elapsed_time, expected_state", @@ -484,47 +482,47 @@ def test_requesting_state(self, mocker, dhcp_mock_5k_with_socket): (160.0, wiz_dhcp.STATE_INIT), ), ) - def test_bound_state(self, dhcp_mock_5k_with_socket, elapsed_time, expected_state): + def test_bound_state(self, mock_dhcp, elapsed_time, expected_state): with freeze_time("2022-10-12") as frozen_datetime: - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_BOUND - dhcp_mock_5k_with_socket._t1 = time.monotonic() + 50 - dhcp_mock_5k_with_socket._t2 = time.monotonic() + 100 - dhcp_mock_5k_with_socket._lease_time = time.monotonic() + 150 + mock_dhcp._dhcp_state = wiz_dhcp.STATE_BOUND + mock_dhcp._t1 = time.monotonic() + 50 + mock_dhcp._t2 = time.monotonic() + 100 + mock_dhcp._lease_time = time.monotonic() + 150 frozen_datetime.tick(elapsed_time) - dhcp_mock_5k_with_socket._dhcp_state_machine() + mock_dhcp._dhcp_state_machine() - assert dhcp_mock_5k_with_socket._dhcp_state == expected_state + assert mock_dhcp._dhcp_state == expected_state if expected_state == wiz_dhcp.STATE_INIT: - assert dhcp_mock_5k_with_socket._blocking is True + assert mock_dhcp._blocking is True @freeze_time("2022-10-15") - def test_renewing_state(self, mocker, dhcp_mock_5k_with_socket): - mocker.patch.object(dhcp_mock_5k_with_socket, "_socket_setup") - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_RENEWING + def test_renewing_state(self, mocker, mock_dhcp): + mocker.patch.object(mock_dhcp, "_socket_setup") + mock_dhcp._dhcp_state = wiz_dhcp.STATE_RENEWING - dhcp_mock_5k_with_socket._dhcp_state_machine() + mock_dhcp._dhcp_state_machine() - assert dhcp_mock_5k_with_socket._renew is True - assert dhcp_mock_5k_with_socket._start_time == time.monotonic() - dhcp_mock_5k_with_socket._socket_setup.assert_called_once() - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + assert mock_dhcp._renew is True + assert mock_dhcp._start_time == time.monotonic() + mock_dhcp._socket_setup.assert_called_once() + mock_dhcp._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @freeze_time("2022-10-15") - def test_rebinding_state(self, mocker, dhcp_mock_5k_with_socket): - mocker.patch.object(dhcp_mock_5k_with_socket, "_socket_setup") - dhcp_mock_5k_with_socket._dhcp_state = wiz_dhcp.STATE_REBINDING - dhcp_mock_5k_with_socket._dhcp_server_ip = (8, 8, 8, 8) - - dhcp_mock_5k_with_socket._dhcp_state_machine() - - assert dhcp_mock_5k_with_socket.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR - assert dhcp_mock_5k_with_socket._renew is True - assert dhcp_mock_5k_with_socket._start_time == time.monotonic() - dhcp_mock_5k_with_socket._socket_setup.assert_called_once() - dhcp_mock_5k_with_socket._send_message_set_next_state.assert_called_once_with( + def test_rebinding_state(self, mocker, mock_dhcp): + mocker.patch.object(mock_dhcp, "_socket_setup") + mock_dhcp._dhcp_state = wiz_dhcp.STATE_REBINDING + mock_dhcp._dhcp_server_ip = (8, 8, 8, 8) + + mock_dhcp._dhcp_state_machine() + + assert mock_dhcp.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR + assert mock_dhcp._renew is True + assert mock_dhcp._start_time == time.monotonic() + mock_dhcp._socket_setup.assert_called_once() + mock_dhcp._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) From 3f85df9b6424d4dfe1b90fd44c8ec4e71c78ade5 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 3 Jan 2023 14:28:47 +0300 Subject: [PATCH 21/80] Refactored the FSM to use _receive_dhcp_response instead of socket.socket.available. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 6 +-- tests/test_dhcp_main_logic.py | 46 +++++++++------------ 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 6dffa64..fbbc088 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -346,7 +346,6 @@ def _handle_dhcp_message(self) -> None: :raises TimeoutError: If the FSM is in blocking mode and no valid response has been received before the timeout expires. """ - global _BUFF # pylint: disable=global-statement def processing_state_selecting(): """Process a message while the FSM is in SELECTING state.""" @@ -379,10 +378,10 @@ def processing_state_requesting(): return True return False + # Main processing loop while True: while time.monotonic() < self._next_resend: - if self._sock.available(): - _BUFF = self._sock.recv() + if self._receive_dhcp_response(): try: msg_type = self._parse_dhcp_response() except ValueError as error: @@ -580,7 +579,6 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: option_data = _BUFF[pointer:data_end] return data_end, option_type, option_data - global _BUFF # pylint: disable=global-variable-not-assigned # Validate OP if _BUFF[0] != DHCP_BOOT_REPLY: raise ValueError("DHCP message OP is not expected BOOTP Reply.") diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index f6e6ea1..0a8c30a 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -28,6 +28,9 @@ def mock_dhcp(mocker): dhcp._sock = mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket.socket", autospec=True ) + # Mock the _receive_dhcp_response method to select whether a response was received, + # assume a good message. + mocker.patch.object(dhcp, "_receive_dhcp_response", autospec=True, return_value=240) # Mock the _parse_dhcp_response method to inject message types without supplying # fake DHCP message packets. mocker.patch.object(dhcp, "_parse_dhcp_response", autospec=True) @@ -64,12 +67,10 @@ def test_with_valid_data_on_socket_selecting(self, mock_dhcp): # Set up initial values for the test. # Start FSM in SELECTING state. mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - # Receive the expected OFFER message type. - mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER - # Have some data on the socket. - mock_dhcp._sock.available.return_value = 24 # Avoid a timeout before checking the message. mock_dhcp._next_resend = time.monotonic() + 5 + # Receive the expected OFFER message type. + mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER # Test. mock_dhcp._handle_dhcp_message() # Confirm that the message would be parsed. @@ -126,8 +127,6 @@ def test_with_wrong_message_type_on_socket_nonblocking( mock_dhcp._parse_dhcp_response.return_value = msg_type # Receive the incorrect message type. mock_dhcp._dhcp_state = fsm_state - # Have some data on the socket. - mock_dhcp._sock.available.return_value = 32 # Avoid a timeout before checking the message. mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. @@ -136,8 +135,7 @@ def test_with_wrong_message_type_on_socket_nonblocking( mock_dhcp._blocking = False # Test. mock_dhcp._handle_dhcp_message() - # Only one call to recv() (and therefore other methods) because nonblocking. - mock_dhcp._sock.recv.assert_called_once() + # Only one call methods because nonblocking, so no attempt to get another message. mock_dhcp._parse_dhcp_response.assert_called_once() mock_dhcp._send_message_set_next_state.assert_not_called() # Confirm that the FSM state has not changed. @@ -199,22 +197,22 @@ def test_with_no_data_on_socket_blocking( # Set up initial values for the test. # Start FSM in required state. mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING + # Avoid a timeout before checking the message. + mock_dhcp._next_resend = time.monotonic() + 5 # Return a correct message type once data is on the socket. mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER # No data on the socket, and finally some data. - mock_dhcp._sock.available.side_effect = [0, 0, 32] - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 + mock_dhcp._receive_dhcp_response.side_effect = [0, 0, 320] # Test an initial negotiation, not a renewal or rebind. mock_dhcp._renew = False # Put FSM into blocking mode so that multiple attempts are made. mock_dhcp._blocking = True # Test. mock_dhcp._handle_dhcp_message() - # Check available() called the correct number of times. - assert mock_dhcp._sock.available.call_count == 3 + # Check _receive_dhcp_response called the correct number of times. + assert mock_dhcp._receive_dhcp_response.call_count == 3 # Confirm that only one message was processed. - mock_dhcp._sock.recv.assert_called_once() + mock_dhcp._parse_dhcp_response.assert_called_once() @freeze_time("2022-06-10") def test_with_no_data_on_socket_nonblocking( @@ -224,20 +222,20 @@ def test_with_no_data_on_socket_nonblocking( # Set up initial values for the test. # Start FSM in required state. mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING + # Avoid a timeout before checking the message. + mock_dhcp._next_resend = time.monotonic() + 5 # Return a correct message type if data is on the socket. mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER # No data on the socket, and finally some data. - mock_dhcp._sock.available.side_effect = [0, 0, 32] - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 + mock_dhcp._receive_dhcp_response.side_effect = [0, 0, 320] # Test an initial negotiation, not a renewal or rebind. mock_dhcp._renew = False # Put FSM into nonblocking mode so that a single attempt is made. mock_dhcp._blocking = False # Test. mock_dhcp._handle_dhcp_message() - # Available should only be called once in nonblocking mode. - mock_dhcp._sock.available.assert_called_once() + # Receive should only be called once in nonblocking mode. + mock_dhcp._receive_dhcp_response.assert_called_once() # Check that no data was read from the socket. mock_dhcp._sock.recv.assert_not_called() # Confirm that the FSM state has not changed. @@ -256,8 +254,6 @@ def test_with_valueerror_nonblocking( ValueError, ValueError, ] - # Have some data on the socket. - mock_dhcp._sock.available.return_value = 32 # Avoid a timeout before checking the message. mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. @@ -266,8 +262,8 @@ def test_with_valueerror_nonblocking( mock_dhcp._blocking = False # Test. mock_dhcp._handle_dhcp_message() - # Available should only be called once in nonblocking mode. - mock_dhcp._sock.available.assert_called_once() + # Receive should only be called once in nonblocking mode. + mock_dhcp._receive_dhcp_response.assert_called_once() # Confirm that _send_message_set_next_state not called. mock_dhcp._send_message_set_next_state.assert_not_called() # Check that FSM state has not changed. @@ -287,8 +283,6 @@ def test_with_valueerror_blocking( ValueError, wiz_dhcp.DHCP_OFFER, ] - # Have some data on the socket. - mock_dhcp._sock.available.return_value = 32 # Avoid a timeout before checking the message. mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. @@ -298,7 +292,7 @@ def test_with_valueerror_blocking( # Test. mock_dhcp._handle_dhcp_message() # Check available() called three times. - assert mock_dhcp._sock.available.call_count == 3 + assert mock_dhcp._receive_dhcp_response.call_count == 3 # Confirm that _send_message_set_next_state was called to change FSM state. mock_dhcp._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 From 688854edbce5b541013f22d09431b495028e5226 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 3 Jan 2023 17:12:58 +0300 Subject: [PATCH 22/80] Added ip_in_use() to WIZNET5K to allow DHCP to send an ARP and confirm offered IPv4 is not in use. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 7d1baa1..7b54222 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -74,6 +74,16 @@ REG_SNRX_RD = const(0x0028) # Read Size Pointer REG_SNTX_FSR = const(0x0020) # Socket n TX Free Size REG_SNTX_WR = const(0x0024) # TX Write Pointer +# Registers for implementing socketless ARP +_REG_SLCR = const(0x004C) # Socketless Command Register +_REG_SLRTR = const(0x004D) # Socketless Retransmission Time Register +_REG_SLRCR = const(0x004F) # Socketless Retransmission Count Register +_REG_SLPIPR = const(0x0050) # Socketless Peer IP Address Register +_REG_SLPHAR = const(0x0054) # Socketless Peer Hardware Address Register +_REG_SLIR = const(0x005F) # Socketless Peer Interrupt Register +_MASK_ARP_COMMAND = const(0b00000010) # Sets the send ARP command +_MASK_ARP_RECEIVED = const(0b00000010) # Masks the ARP received bit +_MASK_ARP_TIMEOUT = const(0b00000100) # Masks the ARP timeout bit # SNSR Commands SNSR_SOCK_CLOSED = const(0x00) @@ -1155,3 +1165,45 @@ def _read_socket(self, sock: int, address: int) -> Optional[bytearray]: cntl_byte = 0 return self.read(self._ch_base_msb + sock * CH_SIZE + address, cntl_byte) return None + + def ip_in_use(self, ip_addr: bytes) -> bool: + """ + Send an ARP to the IPv4 address supplied and wait for a response. + + A helper function for the DHCP client to confirm that the offered IP address is + not in use before setting up the DHCP parameters. May also be called by the user + before setting a manual IP address, to make sure that it is not already in use. + + :param bytes ip_addr: The 4 byte IPv4 address to send the ARP to. + + :returns bool: True if the ARP received a response (i.e. the address is already in use), + False if the ARP timed out. + """ + if len(ip_addr) != 4: + raise ValueError("An IPv4 address must be 4 bytes.") + if self.ifconfig[0] != b"/x00/x00x/00x/00": + raise ValueError( + "send_arp must be called before setting the Wiznet IP address." + ) + if self.mac_address == b"/x00/x00/x00/x00/x00/x00": + raise ValueError( + "The Wiznet MAC address must be set before calling send_arp." + ) + # Check that no socketless commands are in progress. + while self.read(_REG_SLCR, 0x00): + time.sleep(0.05) + # Set up the ARP parameters. + self.write(_REG_SLPIPR, 0x04, ip_addr) # Set the peer IP address. + self.write(_REG_SLRTR, 0x04, 0x0740) # Set time before retry to 0.2 seconds. + self.write( + _REG_SLRCR, 0x04, 0x05 + ) # Set the retry count to 5, total timeout of 1 second. + self.write(_REG_SLRCR, 0x04, _MASK_ARP_COMMAND) # Send the ARP. + # Wait for the result. + while True: + register = self.read(_REG_SLIR, 0x00) + if register & _MASK_ARP_RECEIVED: + return True + if register & _MASK_ARP_TIMEOUT: + return False + time.sleep(0.05) From ab72630179fdf2fb021bfd0c21e9ae6ca69f538a Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 3 Jan 2023 20:48:25 +0300 Subject: [PATCH 23/80] Added debugging messages to wiznet5k_dhcp. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 249 +++++++------------- 1 file changed, 83 insertions(+), 166 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index fbbc088..fe0a732 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -84,6 +84,12 @@ _BUFF = bytearray(BUFF_LENGTH) +def _debugging_message(message: Union[Exception, str], debugging: bool) -> None: + """Helper function to print debugging messages.""" + if debugging: + print(message) + + class DHCP: """Wiznet5k DHCP Client. @@ -135,6 +141,7 @@ def __init__( :param bool debug: Enable debugging output. """ self._debug = debug + _debugging_message("Initialising DHCP instance.", self._debug) self._response_timeout = response_timeout # Prevent buffer overrun in send_dhcp_message() @@ -177,16 +184,21 @@ def __init__( def request_dhcp_lease(self) -> bool: """Request to renew or acquire a DHCP lease.""" + _debugging_message("Requesting DHCP lease.", self._debug) self._dhcp_state_machine(blocking=True) return self._dhcp_state == STATE_BOUND - def maintain_dhcp_lease(self) -> None: + def maintain_dhcp_lease(self, blocking: bool = False) -> None: """Maintain DHCP lease""" - self._dhcp_state_machine() + _debugging_message( + "Maintaining lease with blocking = {}".format(blocking), self._debug + ) + self._dhcp_state_machine(blocking=blocking) def _dsm_reset(self) -> None: """Close the socket and set attributes to default values used by the state machine INIT state.""" + _debugging_message("Resetting DHCP state machine.", self._debug) self._socket_release() self.dhcp_server_ip = BROADCAST_SERVER_ADDR self._eth.ifconfig = ( @@ -206,6 +218,7 @@ def _dsm_reset(self) -> None: def _socket_release(self) -> None: """Close the socket if it exists.""" + _debugging_message("Releasing socket.", self._debug) if self._sock: self._sock.close() self._sock = None @@ -224,8 +237,10 @@ def _socket_setup(self, timeout: int = 5) -> None: :raises TimeoutError: If the FSM is in blocking mode and a socket cannot be initialised. """ + _debugging_message("Setting up a socket.", self._debug) self._socket_release() stop_time = time.monotonic() + timeout + _debugging_message("Creating new socket instance for DHCP.", self._debug) while not time.monotonic() > stop_time: try: self._sock = socket.socket(type=socket.SOCK_DGRAM) @@ -249,6 +264,7 @@ def _socket_setup(self, timeout: int = 5) -> None: def _increment_transaction_id(self) -> None: """Increment the transaction ID and roll over from 0x7fffffff to 0.""" + _debugging_message("Incrementing transaction ID", self._debug) self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF def _next_retry_time_and_retry(self, *, interval: int = 4) -> int: @@ -266,6 +282,9 @@ def _next_retry_time_and_retry(self, *, interval: int = 4) -> int: :raises ValueError: If the calculated interval is < 1 second. """ + _debugging_message( + "Calculating next retry time and incrementing retries.", self._debug + ) delay = int(2**self._retries * interval + randint(-1, 1) + time.monotonic()) if delay < 1: raise ValueError("Retry delay must be >= 1 second") @@ -288,6 +307,10 @@ def _send_message_set_next_state( :raises ValueError: If the next FSM state does not handle DHCP messages. """ + _debugging_message( + "Setting next FSM state to {} and sending a message.".format(next_state), + self._debug, + ) if next_state not in (STATE_SELECTING, STATE_REQUESTING): raise ValueError("The next state must be SELECTING or REQUESTING.") if next_state == STATE_SELECTING: @@ -314,6 +337,7 @@ def _receive_dhcp_response(self) -> int: :returns int: The number of bytes stored in the global buffer. """ + _debugging_message("Receiving a DHCP response.", self._debug) # DHCP returns the query plus additional data. The query length is 236 bytes. minimum_packet_length = 236 buffer = bytearray(b"") @@ -325,6 +349,7 @@ def _receive_dhcp_response(self) -> int: bytes_read = len(buffer) if bytes_read == BUFF_LENGTH: break + _debugging_message("Received {} bytes".format(bytes_read), self._debug) if bytes_read < minimum_packet_length: bytes_read = 0 else: @@ -350,6 +375,9 @@ def _handle_dhcp_message(self) -> None: def processing_state_selecting(): """Process a message while the FSM is in SELECTING state.""" if self._dhcp_state == STATE_SELECTING and msg_type == DHCP_OFFER: + _debugging_message( + "FSM state is SELECTING with valid OFFER.", self._debug + ) self._send_message_set_next_state( next_state=STATE_REQUESTING, max_retries=3, @@ -360,10 +388,17 @@ def processing_state_selecting(): def processing_state_requesting(): """Process a message while the FSM is in REQUESTING state.""" if self._dhcp_state == STATE_REQUESTING: + _debugging_message("FSM state is REQUESTING.", self._debug) if msg_type == DHCP_NAK: + _debugging_message( + "Message is NAK, setting FSM state to INIT.", self._debug + ) self._dhcp_state = STATE_INIT return True if msg_type == DHCP_ACK: + _debugging_message( + "Message is ACK, setting FSM state to BOUND.", self._debug + ) if self._lease_time == 0: self._lease_time = DEFAULT_LEASE_TIME self._t1 = self._start_time + self._lease_time // 2 @@ -379,20 +414,24 @@ def processing_state_requesting(): return False # Main processing loop + _debugging_message("Processing SELECTING or REQUESTING state.", self._debug) while True: while time.monotonic() < self._next_resend: if self._receive_dhcp_response(): try: msg_type = self._parse_dhcp_response() + _debugging_message( + "Received message type {}".format(msg_type), self._debug + ) except ValueError as error: - if self._debug: - print(error) + _debugging_message(error, self._debug) else: if processing_state_selecting(): return if processing_state_requesting(): return if not self._blocking: + _debugging_message("Nonblocking, exiting loop.", self._debug) return self._next_resend = self._next_retry_time_and_retry() if self._retries > self._max_retries: @@ -404,6 +443,9 @@ def processing_state_requesting(): ) ) if not self._blocking: + _debugging_message( + "Nonblocking, returning from message function.", self._debug + ) return def _dhcp_state_machine(self, *, blocking: bool = False) -> None: @@ -411,18 +453,33 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: A finite state machine to allow the DHCP lease to be managed without blocking the main program. The initial lease... """ - + _debugging_message( + "DHCP FSM called with blocking={}".format(blocking), self._debug + ) + _debugging_message( + "FSM initial state is {}".format(self._dhcp_state), self._debug + ) self._blocking = blocking if self._dhcp_state == STATE_BOUND: now = time.monotonic() if now < self._t1: + _debugging_message("No timers have expired. Exiting FSM.", self._debug) return if now > self._lease_time: + _debugging_message( + "Lease has expired, switching state to INIT.", self._debug + ) self._blocking = True self._dhcp_state = STATE_INIT elif now > self._t2: + _debugging_message( + "T2 has expired, switching state to REBINDING.", self._debug + ) self._dhcp_state = STATE_REBINDING else: + _debugging_message( + "T1 has expired, switching state to RENEWING.", self._debug + ) self._dhcp_state = STATE_RENEWING if self._dhcp_state == STATE_RENEWING: @@ -460,6 +517,10 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._handle_dhcp_message() if self._renew: + _debugging_message( + "Lease has not expired, setting state to BOUND and exiting FSM.", + self._debug, + ) self._dhcp_state = STATE_BOUND def _generate_dhcp_message( @@ -491,7 +552,6 @@ def option_writer( :returns int: Pointer to next option. """ - global _BUFF # pylint: disable=global-variable-not-assigned _BUFF[offset] = option_code data_length = len(option_data) offset += 1 @@ -501,8 +561,8 @@ def option_writer( _BUFF[offset:data_end] = option_data return data_end - global _BUFF # pylint: disable=global-variable-not-assigned - _BUFF[:] = bytearray(b"\x00" * BUFF_LENGTH) + # global _BUFF # pylint: disable=global-variable-not-assigned + _BUFF[:] = bytearray(BUFF_LENGTH) # OP.HTYPE.HLEN.HOPS _BUFF[0:4] = (DHCP_BOOT_REQUEST, DHCP_HTYPE10MB, DHCP_HLENETHERNET, DHCP_HOPS) # Transaction ID (xid) @@ -570,7 +630,6 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: :returns Tuple[int, int, bytes]: Pointer to next option, option type, and option data. """ - global _BUFF # pylint: disable=global-variable-not-assigned option_type = _BUFF[pointer] pointer += 1 data_length = _BUFF[pointer] @@ -581,7 +640,7 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: # Validate OP if _BUFF[0] != DHCP_BOOT_REPLY: - raise ValueError("DHCP message OP is not expected BOOTP Reply.") + raise ValueError("DHCP message is not the expected DHCP Reply.") # Confirm transaction IDs match. xid = _BUFF[4:8] if xid != self._transaction_id.to_bytes(4, "big"): @@ -619,164 +678,22 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: elif data_type == 0: break - if self._debug: - print( - "Msg Type: {}\nSubnet Mask: {}\nDHCP Server IP: {}\nDNS Server IP: {}\ + _debugging_message( + "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, - ) - ) - + 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, + ), + self._debug, + ) gc.collect() if msg_type is None: raise ValueError("No valid message type in response.") return msg_type - - # # pylint: disable=too-many-branches, too-many-statements - # def _dhcp_state_machine(self) -> None: - # """ - # DHCP state machine without wait loops to enable cooperative multitasking. - # 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: 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: 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 = None, None # 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, "big"): - # 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: - # 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 = None, None # 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, "big"): - # 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: - # 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 in ( - # STATE_DHCP_DISCOVER, - # 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 From ebe63b0259c16328fdbd8cbbfa1343bde5f254be Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 4 Jan 2023 20:31:41 +0300 Subject: [PATCH 24/80] Moved _ip_in_use to DHCP and refactored to use a UDP call as W5500 has no ARP tool. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 100 ++++++++++---------- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 40 +++++++- 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 7b54222..e8a8e7a 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -57,6 +57,10 @@ REG_SUBR = const(0x0005) # Subnet Mask Address REG_VERSIONR_W5500 = const(0x0039) # W5500 Silicon Version REG_VERSIONR_W5100S = const(0x0080) # W5100S Silicon Version +_REG_RCR_5100s = const(0x0019) # Retry Count +_REG_RTR_5100s = const(0x0017) # Retry Time +_REG_RCR_5500 = const(0x001B) # Retry Count +_REG_RTR_5500 = const(0x0019) # Retry Time REG_SHAR = const(0x0009) # Source Hardware Address REG_SIPR = const(0x000F) # Source IP Address REG_PHYCFGR = const(0x002E) # W5500 PHY Configuration @@ -74,16 +78,6 @@ REG_SNRX_RD = const(0x0028) # Read Size Pointer REG_SNTX_FSR = const(0x0020) # Socket n TX Free Size REG_SNTX_WR = const(0x0024) # TX Write Pointer -# Registers for implementing socketless ARP -_REG_SLCR = const(0x004C) # Socketless Command Register -_REG_SLRTR = const(0x004D) # Socketless Retransmission Time Register -_REG_SLRCR = const(0x004F) # Socketless Retransmission Count Register -_REG_SLPIPR = const(0x0050) # Socketless Peer IP Address Register -_REG_SLPHAR = const(0x0054) # Socketless Peer Hardware Address Register -_REG_SLIR = const(0x005F) # Socketless Peer Interrupt Register -_MASK_ARP_COMMAND = const(0b00000010) # Sets the send ARP command -_MASK_ARP_RECEIVED = const(0b00000010) # Masks the ARP received bit -_MASK_ARP_TIMEOUT = const(0b00000100) # Masks the ARP timeout bit # SNSR Commands SNSR_SOCK_CLOSED = const(0x00) @@ -1081,29 +1075,35 @@ def _get_tx_free_size(self, sock: int) -> int: return int.from_bytes(val, "big") def _read_snrx_rd(self, sock: int) -> int: + """Read socket n RX Read Data Pointer Register.""" self._pbuff[0] = self._read_socket(sock, REG_SNRX_RD)[0] self._pbuff[1] = self._read_socket(sock, REG_SNRX_RD + 1)[0] return self._pbuff[0] << 8 | self._pbuff[1] def _write_snrx_rd(self, sock: int, data: int) -> None: + """Write socket n RX Read Data Pointer Register.""" self._write_socket(sock, REG_SNRX_RD, data >> 8 & 0xFF) self._write_socket(sock, REG_SNRX_RD + 1, data & 0xFF) def _write_sntx_wr(self, sock: int, data: int) -> None: + """Write the socket write buffer pointer for socket `sock`.""" self._write_socket(sock, REG_SNTX_WR, data >> 8 & 0xFF) self._write_socket(sock, REG_SNTX_WR + 1, data & 0xFF) def _read_sntx_wr(self, sock: int) -> int: + """Read the socket write buffer pointer for socket `sock`.""" self._pbuff[0] = self._read_socket(sock, 0x0024)[0] self._pbuff[1] = self._read_socket(sock, 0x0024 + 1)[0] return self._pbuff[0] << 8 | self._pbuff[1] def _read_sntx_fsr(self, sock: int) -> Optional[bytearray]: + """Read socket n TX Free Size Register""" data = self._read_socket(sock, REG_SNTX_FSR) data += self._read_socket(sock, REG_SNTX_FSR + 1) return data def _read_snrx_rsr(self, sock: int) -> Optional[bytearray]: + """Read socket n Received Size Register""" data = self._read_socket(sock, REG_SNRX_RSR) data += self._read_socket(sock, REG_SNRX_RSR + 1) return data @@ -1166,44 +1166,44 @@ def _read_socket(self, sock: int, address: int) -> Optional[bytearray]: return self.read(self._ch_base_msb + sock * CH_SIZE + address, cntl_byte) return None - def ip_in_use(self, ip_addr: bytes) -> bool: - """ - Send an ARP to the IPv4 address supplied and wait for a response. - - A helper function for the DHCP client to confirm that the offered IP address is - not in use before setting up the DHCP parameters. May also be called by the user - before setting a manual IP address, to make sure that it is not already in use. - - :param bytes ip_addr: The 4 byte IPv4 address to send the ARP to. + @property + def rcr(self) -> int: + """Retry count register.""" + if self._chip_type == "w5500": + rcr_reg = _REG_RCR_5500 + else: + # Assume a W5100s + rcr_reg = _REG_RCR_5100s + return self.read(rcr_reg, 0x00) + + @rcr.setter + def rcr(self, retry_count: int) -> None: + if 0 > retry_count > 255: + raise ValueError("Retries must be from 0 to 255.") + if self._chip_type == "w5500": + rcr_reg = _REG_RCR_5500 + else: + # Assume a W5100s + rcr_reg = _REG_RCR_5100s + self.write(rcr_reg, 0x04, retry_count) - :returns bool: True if the ARP received a response (i.e. the address is already in use), - False if the ARP timed out. - """ - if len(ip_addr) != 4: - raise ValueError("An IPv4 address must be 4 bytes.") - if self.ifconfig[0] != b"/x00/x00x/00x/00": - raise ValueError( - "send_arp must be called before setting the Wiznet IP address." - ) - if self.mac_address == b"/x00/x00/x00/x00/x00/x00": - raise ValueError( - "The Wiznet MAC address must be set before calling send_arp." - ) - # Check that no socketless commands are in progress. - while self.read(_REG_SLCR, 0x00): - time.sleep(0.05) - # Set up the ARP parameters. - self.write(_REG_SLPIPR, 0x04, ip_addr) # Set the peer IP address. - self.write(_REG_SLRTR, 0x04, 0x0740) # Set time before retry to 0.2 seconds. - self.write( - _REG_SLRCR, 0x04, 0x05 - ) # Set the retry count to 5, total timeout of 1 second. - self.write(_REG_SLRCR, 0x04, _MASK_ARP_COMMAND) # Send the ARP. - # Wait for the result. - while True: - register = self.read(_REG_SLIR, 0x00) - if register & _MASK_ARP_RECEIVED: - return True - if register & _MASK_ARP_TIMEOUT: - return False - time.sleep(0.05) + @property + def rtr(self) -> int: + """Retry time register.""" + if self._chip_type == "w5500": + reg = _REG_RTR_5500 + else: + # Assume a W5100s + reg = _REG_RTR_5100s + return self.read(reg, 0x00, 2) + + @rtr.setter + def rtr(self, retry_count: int) -> None: + if 0 > retry_count > 2**16: + raise ValueError("Retry time must be from 0 to {}".format(2**16)) + if self._chip_type == "w5500": + reg = _REG_RTR_5500 + else: + # Assume a W5100s + reg = _REG_RTR_5100s + self.write(reg, 0x04, retry_count) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index fe0a732..022189e 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -550,7 +550,7 @@ def option_writer( :param int option_code: Type of option to add. :param Tuple[int] option_data: The data for the option. - :returns int: Pointer to next option. + :returns int: Pointer to start of next option. """ _BUFF[offset] = option_code data_length = len(option_data) @@ -697,3 +697,41 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: if msg_type is None: raise ValueError("No valid message type in response.") return msg_type + + def _ip_in_use(self) -> bool: + """ + Send an ARP to the IPv4 address supplied and wait for a response. + + A helper function for the DHCP client to confirm that the offered IP address is + not in use before setting up the DHCP parameters. May also be called by the user + before setting a manual IP address, to make sure that it is not already in use. + + According to RFC5227 section 2.1.1 of , we check for ARP Probe or ARPResponse + reception from other devices for 1 second after sending ARPProbe. If there is no + reception for 1 second, the probe is repeated three times in total, and if there + is no reception, it is determined that there is no conflict. + + :param bytes ip_addr: The 4 byte IPv4 address to test for a conflict. + + :returns bool: True if the ARP received a response (i.e. the address is already in use), + False if the ARP timed out. + + :raises RuntimeError: If the Ethernet link is down. + """ + # Check link status + if not self._eth.link_status: + raise RuntimeError("Ethernet link is down") + # Store current RTR and RCR and set RTR and RCR to 1 second and 3 retries for DHCP. + temp_rcr = self._eth.rcr + temp_rtr = self._eth.rtr + # Set current retry timer and retry count to 1 sec and 3 tries to match DHCP standard. + self._eth.rcr = 3 + self._eth.rtr = 100000 # 100us * 10000 = 1 second + # Send a dummy packet to the assigned address on the DHCP socket. + x = self._sock.sendto( + b"CHECK_IP_CONFLICT", (self._eth.pretty_ip(self.local_ip), 5000) + ) + # Reset the RTR and RCR registers. + self._eth.rcr = temp_rcr + self._eth.rtr = temp_rtr + return bool(x) From 8d00f52cf03c32c48e73f7e1516c064c0bd390f6 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sun, 8 Jan 2023 10:26:02 +1100 Subject: [PATCH 25/80] Moved back to WIZNET5K (final location) and renamed to _ip_address_in_use. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 73 ++++++++++++++++++--- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 40 +---------- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index e8a8e7a..a399031 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -982,9 +982,6 @@ def socket_write( """ assert self.link_status, "Ethernet cable disconnected!" assert socket_num <= self.max_sockets, "Provided socket exceeds max_sockets." - status = 0 - ret = 0 - free_size = 0 if len(buffer) > SOCK_SIZE: ret = SOCK_SIZE else: @@ -1021,11 +1018,9 @@ def socket_write( txbuf = buffer[0:size] self.write(dst_addr, 0x00, txbuf) txbuf = buffer[size:ret] - size = ret - size dst_addr = socket_num * SOCK_SIZE + 0x4000 self.write(dst_addr, 0x00, txbuf) else: - txbuf = buffer[:ret] self.write(dst_addr, 0x00, buffer[:ret]) # update sn_tx_wr to the value + data size @@ -1036,9 +1031,7 @@ def socket_write( self._read_sncr(socket_num) # check data was transferred correctly - while ( - self._read_socket(socket_num, REG_SNIR)[0] & SNIR_SEND_OK - ) != SNIR_SEND_OK: + while not self._read_snir(socket_num)[0] & SNIR_SEND_OK: if self.socket_status(socket_num)[0] in ( SNSR_SOCK_CLOSED, SNSR_SOCK_TIME_WAIT, @@ -1048,6 +1041,10 @@ def socket_write( ) or (timeout and time.monotonic() - stamp > timeout): # self.socket_close(socket_num) return 0 + if self._read_snir(socket_num)[0] & SNIR_TIMEOUT: + raise TimeoutError( + "Hardware timeout while sending on socket {}.".format(socket_num) + ) time.sleep(0.01) self._write_snir(socket_num, SNIR_SEND_OK) @@ -1110,8 +1107,15 @@ def _read_snrx_rsr(self, sock: int) -> Optional[bytearray]: def _write_sndipr(self, sock: int, ip_addr: bytearray) -> None: """Write to socket destination IP Address.""" - for octet in range(0, 4): - self._write_socket(sock, REG_SNDIPR + octet, ip_addr[octet]) + for offset in range(4): + self._write_socket(sock, REG_SNDIPR + offset, ip_addr[offset]) + + def _read_sndipr(self, sock) -> bytearray: + """Read socket destination IP address.""" + data = b"" + for offset in range(4): + data += self._read_socket(sock, REG_SIPR + offset) + return bytearray(data) def _write_sndport(self, sock: int, port: int) -> None: """Write to socket destination port.""" @@ -1122,6 +1126,10 @@ def _read_snsr(self, sock: int) -> Optional[bytearray]: """Read Socket n Status Register.""" return self._read_socket(sock, REG_SNSR) + def _read_snir(self, sock: int) -> Optional[bytearray]: + """Read Socket n Interrupt Register.""" + return self._read_socket(sock, REG_SNIR) + def _write_snmr(self, sock: int, protocol: int) -> None: """Write to Socket n Mode Register.""" self._write_socket(sock, REG_SNMR, protocol) @@ -1207,3 +1215,48 @@ def rtr(self, retry_count: int) -> None: # Assume a W5100s reg = _REG_RTR_5100s self.write(reg, 0x04, retry_count) + + def _ip_address_in_use(self, socknum, local_ip) -> bool: + """ + Send an ARP to the IPv4 address supplied and wait for a response. + + A helper function for the DHCP client to confirm that the offered IP address is + not in use before setting up the DHCP parameters. May also be called by the user + before setting a manual IP address, to make sure that it is not already in use. + + According to RFC5227 section 2.1.1 of , we check for ARP Probe or ARPResponse + reception from other devices for 1 second after sending ARPProbe. If there is no + reception for 1 second, the probe is repeated three times in total, and if there + is no reception, it is determined that there is no conflict. + + :param bytes local_ip: The 4 byte IPv4 address to test for a conflict. + :param int socknum: The socket to test. + + :returns bool: True if the he address is already in use), False if not. + + :raises RuntimeError: If the Ethernet link is down or could not connect to the socket. + """ + # Check link status + if not self.link_status: + raise RuntimeError("Ethernet link is down") + # Store current RTR, RCR and destination IPv4 address. + temp_rcr = self.rcr + temp_rtr = self.rtr + temp_ip = self._read_sndipr(socknum) + # Set current retry timer and retry count to 1 sec and 3 tries to match DHCP standard. + self.rcr = 3 + self.rtr = 100000 # 100us * 10000 = 1 second + # Send a dummy packet to the assigned address on the DHCP socket to mimic ARP. + ip_in_use = True + try: + if self.socket_connect(socknum, bytes(local_ip), 5000, conn_mode=0x02) != 1: + raise RuntimeError("Unable to connect to socket {}.".format(socknum)) + self.socket_write(socknum, b"CHECK_IP_CONFLICT") + except TimeoutError: + ip_in_use = False + finally: + # Reset the RTR, RCR and destination IPv4 registers. + self._write_sndipr(socknum, temp_ip) + self.rcr = temp_rcr + self.rtr = temp_rtr + return ip_in_use diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 022189e..b7b2056 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -253,7 +253,9 @@ def _socket_setup(self, timeout: int = 5) -> None: return else: self._sock.settimeout(self._response_timeout) + # Shouldn't call socket.bind on UDP sockets. self._sock.bind((None, 68)) + # Shouldn't call socket.connect on UDP sockets. self._sock.connect((self.dhcp_server_ip, DHCP_SERVER_PORT)) return raise TimeoutError( @@ -697,41 +699,3 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: if msg_type is None: raise ValueError("No valid message type in response.") return msg_type - - def _ip_in_use(self) -> bool: - """ - Send an ARP to the IPv4 address supplied and wait for a response. - - A helper function for the DHCP client to confirm that the offered IP address is - not in use before setting up the DHCP parameters. May also be called by the user - before setting a manual IP address, to make sure that it is not already in use. - - According to RFC5227 section 2.1.1 of , we check for ARP Probe or ARPResponse - reception from other devices for 1 second after sending ARPProbe. If there is no - reception for 1 second, the probe is repeated three times in total, and if there - is no reception, it is determined that there is no conflict. - - :param bytes ip_addr: The 4 byte IPv4 address to test for a conflict. - - :returns bool: True if the ARP received a response (i.e. the address is already in use), - False if the ARP timed out. - - :raises RuntimeError: If the Ethernet link is down. - """ - # Check link status - if not self._eth.link_status: - raise RuntimeError("Ethernet link is down") - # Store current RTR and RCR and set RTR and RCR to 1 second and 3 retries for DHCP. - temp_rcr = self._eth.rcr - temp_rtr = self._eth.rtr - # Set current retry timer and retry count to 1 sec and 3 tries to match DHCP standard. - self._eth.rcr = 3 - self._eth.rtr = 100000 # 100us * 10000 = 1 second - # Send a dummy packet to the assigned address on the DHCP socket. - x = self._sock.sendto( - b"CHECK_IP_CONFLICT", (self._eth.pretty_ip(self.local_ip), 5000) - ) - # Reset the RTR and RCR registers. - self._eth.rcr = temp_rcr - self._eth.rtr = temp_rtr - return bool(x) From 37b5e037a3cb61c1420da33e62b57e447489de46 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sun, 8 Jan 2023 15:40:41 +1100 Subject: [PATCH 26/80] Refactored DHCP._socket_set to _dhcp_connection_setup and removed socket.socket. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 48 +++++++------- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 53 +++++++-------- tests/test_dhcp_helper_functions.py | 73 +++++++++++---------- tests/test_dhcp_main_logic.py | 28 ++++---- 4 files changed, 101 insertions(+), 101 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index a399031..4aae19d 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -663,7 +663,7 @@ def socket_status(self, socket_num: int) -> Optional[bytearray]: :return: Optional[bytearray] """ - return self._read_snsr(socket_num) + return self.read_snsr(socket_num) def socket_connect( self, @@ -714,8 +714,8 @@ def socket_connect( def _send_socket_cmd(self, socket: int, cmd: int) -> None: """Send a socket command to a socket.""" - self._write_sncr(socket, cmd) - while self._read_sncr(socket) != b"\x00": + self.write_sncr(socket, cmd) + while self.read_sncr(socket) != b"\x00": if self._debug: print("waiting for sncr to clear...") @@ -769,7 +769,7 @@ def socket_listen( # Wait until ready status = [SNSR_SOCK_CLOSED] while status[0] not in (SNSR_SOCK_LISTEN, SNSR_SOCK_ESTABLISHED, SNSR_SOCK_UDP): - status = self._read_snsr(socket_num) + status = self.read_snsr(socket_num) if status[0] == SNSR_SOCK_CLOSED: raise RuntimeError("Listening socket closed.") @@ -814,7 +814,7 @@ def socket_open(self, socket_num: int, conn_mode: int = SNMR_TCP) -> int: assert self.link_status, "Ethernet cable disconnected!" if self._debug: print("*** Opening socket %d" % socket_num) - status = self._read_snsr(socket_num)[0] + status = self.read_snsr(socket_num)[0] if status in ( SNSR_SOCK_CLOSED, SNSR_SOCK_TIME_WAIT, @@ -832,20 +832,20 @@ def socket_open(self, socket_num: int, conn_mode: int = SNMR_TCP) -> int: 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: s_port = randint(49152, 65535) while s_port in SRC_PORTS: s_port = randint(49152, 65535) - self._write_sock_port(socket_num, s_port) + self.write_sock_port(socket_num, s_port) SRC_PORTS[socket_num] = s_port # open socket - self._write_sncr(socket_num, CMD_SOCK_OPEN) - self._read_sncr(socket_num) + self.write_sncr(socket_num, CMD_SOCK_OPEN) + self.read_sncr(socket_num) assert ( - self._read_snsr((socket_num))[0] == 0x13 - or self._read_snsr((socket_num))[0] == 0x22 + self.read_snsr((socket_num))[0] == 0x13 + or self.read_snsr((socket_num))[0] == 0x22 ), "Could not open socket in TCP or UDP mode." return 0 return 1 @@ -858,8 +858,8 @@ def socket_close(self, socket_num: int) -> None: """ if self._debug: print("*** Closing socket #%d" % socket_num) - self._write_sncr(socket_num, CMD_SOCK_CLOSE) - self._read_sncr(socket_num) + self.write_sncr(socket_num, CMD_SOCK_CLOSE) + self.read_sncr(socket_num) def socket_disconnect(self, socket_num: int) -> None: """ @@ -869,8 +869,8 @@ def socket_disconnect(self, socket_num: int) -> None: """ if self._debug: print("*** Disconnecting socket #%d" % socket_num) - self._write_sncr(socket_num, CMD_SOCK_DISCON) - self._read_sncr(socket_num) + self.write_sncr(socket_num, CMD_SOCK_DISCON) + self.read_sncr(socket_num) def socket_read( self, socket_num: int, length: int @@ -939,8 +939,8 @@ def socket_read( self._write_snrx_rd(socket_num, ptr) # Notify the W5k of the updated Sn_Rx_RD - self._write_sncr(socket_num, CMD_SOCK_RECV) - self._read_sncr(socket_num) + self.write_sncr(socket_num, CMD_SOCK_RECV) + self.read_sncr(socket_num) return ret, resp def read_udp( @@ -1027,8 +1027,8 @@ def socket_write( ptr = (ptr + ret) & 0xFFFF self._write_sntx_wr(socket_num, ptr) - self._write_sncr(socket_num, CMD_SOCK_SEND) - self._read_sncr(socket_num) + self.write_sncr(socket_num, CMD_SOCK_SEND) + self.read_sncr(socket_num) # check data was transferred correctly while not self._read_snir(socket_num)[0] & SNIR_SEND_OK: @@ -1122,7 +1122,7 @@ def _write_sndport(self, sock: int, port: int) -> None: self._write_socket(sock, REG_SNDPORT, port >> 8) self._write_socket(sock, REG_SNDPORT + 1, port & 0xFF) - def _read_snsr(self, sock: int) -> Optional[bytearray]: + def read_snsr(self, sock: int) -> Optional[bytearray]: """Read Socket n Status Register.""" return self._read_socket(sock, REG_SNSR) @@ -1138,15 +1138,17 @@ def _write_snir(self, sock: int, data: int) -> None: """Write to Socket n Interrupt Register.""" self._write_socket(sock, REG_SNIR, data) - def _write_sock_port(self, sock: int, port: int) -> None: + def write_sock_port(self, sock: int, port: int) -> None: """Write to the socket port number.""" self._write_socket(sock, REG_SNPORT, port >> 8) self._write_socket(sock, REG_SNPORT + 1, port & 0xFF) - def _write_sncr(self, sock: int, data: int) -> None: + def write_sncr(self, sock: int, data: int) -> None: + """Write to socket command register.""" self._write_socket(sock, REG_SNCR, data) - def _read_sncr(self, sock: int) -> Optional[bytearray]: + def read_sncr(self, sock: int) -> Optional[bytearray]: + """Read socket command register.""" return self._read_socket(sock, REG_SNCR) def _read_snmr(self, sock: int) -> Optional[bytearray]: diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index b7b2056..f87ab11 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -152,7 +152,7 @@ def __init__( # Set socket interface socket.set_interface(eth) self._eth = eth - self._sock = None + self._wiz_sock = None # DHCP state machine self._dhcp_state = STATE_INIT @@ -219,11 +219,11 @@ def _dsm_reset(self) -> None: def _socket_release(self) -> None: """Close the socket if it exists.""" _debugging_message("Releasing socket.", self._debug) - if self._sock: - self._sock.close() - self._sock = None + if self._wiz_sock: + self._eth.socket_close(self._wiz_sock) + self._wiz_sock = None - def _socket_setup(self, timeout: int = 5) -> None: + def _dhcp_connection_setup(self, timeout: int = 5) -> None: """Initialise a UDP socket. Attempt to initialise a UDP socket. If the finite state machine (FSM) is in @@ -241,28 +241,19 @@ def _socket_setup(self, timeout: int = 5) -> None: self._socket_release() stop_time = time.monotonic() + timeout _debugging_message("Creating new socket instance for DHCP.", self._debug) - while not time.monotonic() > stop_time: - try: - self._sock = socket.socket(type=socket.SOCK_DGRAM) - except RuntimeError: - if self._debug: - print("DHCP client failed to allocate socket") - if self._blocking: - print("Retrying…") - else: - return - else: - self._sock.settimeout(self._response_timeout) - # Shouldn't call socket.bind on UDP sockets. - self._sock.bind((None, 68)) - # Shouldn't call socket.connect on UDP sockets. - self._sock.connect((self.dhcp_server_ip, DHCP_SERVER_PORT)) - return - raise TimeoutError( - "DHCP client failed to allocate a socket. Retried for {} seconds.".format( - timeout - ) - ) + while not self._wiz_sock and time.monotonic() < stop_time: + self._wiz_sock = self._eth.get_socket() + if self._wiz_sock == 0xFF: + self._wiz_sock = None + while ( + time.monotonic() < stop_time and self._eth.read_snsr(self._wiz_sock) != 0x22 + ): + self._eth.write_sock_port(self._wiz_sock, 68) # Set DHCP client port. + self._eth.write_sncr(self._wiz_sock, 0x01) # Open the socket. + while ( + self._eth.read_sncr(self._wiz_sock) == 0 + ): # Wait for command to complete. + time.sleep(0.0001) def _increment_transaction_id(self) -> None: """Increment the transaction ID and roll over from 0x7fffffff to 0.""" @@ -320,7 +311,7 @@ def _send_message_set_next_state( else: message_type = DHCP_REQUEST self._generate_dhcp_message(message_type=message_type) - self._sock.send(_BUFF) + self._wiz_sock.send(_BUFF) self._retries = 0 self._max_retries = max_retries self._next_resend = self._next_retry_time_and_retry() @@ -347,7 +338,7 @@ def _receive_dhcp_response(self) -> int: while ( bytes_read <= minimum_packet_length and time.monotonic() < self._next_resend ): - buffer.extend(self._sock.recv(BUFF_LENGTH - bytes_read)) + buffer.extend(self._wiz_sock.recv(BUFF_LENGTH - bytes_read)) bytes_read = len(buffer) if bytes_read == BUFF_LENGTH: break @@ -486,7 +477,7 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: if self._dhcp_state == STATE_RENEWING: self._renew = True - self._socket_setup() + self._dhcp_connection_setup() self._start_time = time.monotonic() self._send_message_set_next_state( next_state=STATE_REQUESTING, @@ -496,7 +487,7 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: if self._dhcp_state == STATE_REBINDING: self._renew = True self.dhcp_server_ip = BROADCAST_SERVER_ADDR - self._socket_setup() + self._dhcp_connection_setup() self._start_time = time.monotonic() self._send_message_set_next_state( next_state=STATE_REQUESTING, diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 20cfe2c..77dba92 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -30,7 +30,7 @@ def mock_socket(mocker): @pytest.fixture def mock_dhcp(mocker, mock_wiznet5k): dhcp = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp._sock = mocker.patch( + dhcp._wiz_sock = mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket.socket", autospec=True ) return dhcp @@ -113,7 +113,7 @@ def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mock_socket, mac_addres assert dhcp_client._debug is False assert dhcp_client._mac_address == mac_address mock_socket.set_interface.assert_called_once_with(mock_wiznet5k) - assert dhcp_client._sock is None + assert dhcp_client._wiz_sock is None assert dhcp_client._dhcp_state == wiz_dhcp.STATE_INIT mock_randint.assert_called_once() assert dhcp_client._transaction_id == 0x1234567 @@ -156,7 +156,7 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k, mock_sock """Test the _generate_message function with default values.""" assert len(wiz_dhcp._BUFF) == 318 dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) - dhcp_client._sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) + dhcp_client._wiz_sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) dhcp_client._transaction_id = 0x6FFFFFFF dhcp_client._start_time = time.monotonic() - 23.4 dhcp_client._generate_dhcp_message(message_type=wiz_dhcp.DHCP_DISCOVER) @@ -353,8 +353,8 @@ def test_parsing_failures(self, mock_wiznet5k, mock_socket): # Test for bad OP code, ID mismatch, no server ID, bad Magic Cookie bad_data = dhcp_data.BAD_DATA dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp_client._sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) - dhcp_client._sock.recv.return_value = bad_data + dhcp_client._wiz_sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) + dhcp_client._wiz_sock.recv.return_value = bad_data # Transaction ID mismatch. dhcp_client._transaction_id = 0x42424242 with pytest.raises(ValueError): @@ -381,12 +381,12 @@ def test_socket_reset(self, mock_wiznet5k, mock_socket): dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._dsm_reset() - assert dhcp_client._sock is None + assert dhcp_client._wiz_sock is None - dhcp_client._sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) + dhcp_client._wiz_sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) dhcp_client._dsm_reset() - assert dhcp_client._sock is None + assert dhcp_client._wiz_sock is None @freeze_time("2022-11-10") def test_reset_dsm_parameters(self, mock_wiznet5k): @@ -416,11 +416,11 @@ class TestSocketRelease: def test_socket_set_to_none(self, mock_wiznet5k, mock_socket): dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._socket_release() - assert dhcp_client._sock is None + assert dhcp_client._wiz_sock is None - dhcp_client._sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) + dhcp_client._wiz_sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) dhcp_client._socket_release() - assert dhcp_client._sock is None + assert dhcp_client._wiz_sock is None class TestSmallHelperFunctions: @@ -470,13 +470,13 @@ def test_next_retry_time_optional_attrs(self, mocker, mock_wiznet5k, interval): def test_setup_socket_with_no_error(self, mock_wiznet5k, mock_socket): dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp_client._socket_setup() + dhcp_client._dhcp_connection_setup() mock_socket.socket.assert_called_once() - dhcp_client._sock.settimeout.assert_called_once_with( + dhcp_client._wiz_sock.settimeout.assert_called_once_with( dhcp_client._response_timeout ) - dhcp_client._sock.bind.assert_called_once_with((None, 68)) - dhcp_client._sock.connect.assert_called_once_with( + dhcp_client._wiz_sock.bind.assert_called_once_with((None, 68)) + dhcp_client._wiz_sock.connect.assert_called_once_with( (wiz_dhcp.BROADCAST_SERVER_ADDR, 67) ) @@ -486,9 +486,9 @@ def test_setup_socket_with_error_then_ok_blocking( dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._blocking = True mock_socket.socket.side_effect = [RuntimeError, mocker.Mock()] - assert dhcp_client._socket_setup() is None + assert dhcp_client._dhcp_connection_setup() is None # Function should ignore the error, then set the socket and call settimeout - dhcp_client._sock.settimeout.assert_called_once_with( + dhcp_client._wiz_sock.settimeout.assert_called_once_with( dhcp_client._response_timeout ) @@ -498,9 +498,9 @@ def test_setup_socket_with_error_then_ok_nonblocking( dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._blocking = False mock_socket.socket.side_effect = [RuntimeError, mocker.Mock()] - assert dhcp_client._socket_setup() is None + assert dhcp_client._dhcp_connection_setup() is None # Function should ignore the error, then return, _sock is None - assert dhcp_client._sock is None + assert dhcp_client._wiz_sock is None @freeze_time("2022-10-02", auto_tick_seconds=2) def test_setup_socket_with_error_then_timeout(self, mock_wiznet5k, mock_socket): @@ -509,8 +509,8 @@ def test_setup_socket_with_error_then_timeout(self, mock_wiznet5k, mock_socket): mock_socket.socket.side_effect = [RuntimeError, RuntimeError] # Function should timeout, raise an exception and not set a socket with pytest.raises(TimeoutError): - dhcp_client._socket_setup() - assert dhcp_client._sock is None + dhcp_client._dhcp_connection_setup() + assert dhcp_client._wiz_sock is None @pytest.mark.parametrize( "next_state, msg_type, retries", @@ -527,7 +527,7 @@ def test_send_message_set_next_state_good_data( message = bytearray(b"HelloWorld") wiz_dhcp._BUFF = bytearray(message) dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp_client._sock = mocker.Mock() + dhcp_client._wiz_sock = mocker.Mock() dhcp_client._send_message_set_next_state( next_state=next_state, max_retries=retries ) @@ -537,7 +537,7 @@ def test_send_message_set_next_state_good_data( dhcp_client._generate_dhcp_message.assert_called_once_with( message_type=msg_type ) - dhcp_client._sock.send.assert_called_once_with(message) + dhcp_client._wiz_sock.send.assert_called_once_with(message) dhcp_client._next_retry_time_and_retry.assert_called_once() @pytest.mark.parametrize( @@ -568,7 +568,7 @@ class TestReceiveResponse: ) def test_receive_response_good_data(self, mock_dhcp, bytes_on_socket): mock_dhcp._next_resend = time.monotonic() + 15 - mock_dhcp._sock.recv.return_value = bytes([0] * bytes_on_socket) + mock_dhcp._wiz_sock.recv.return_value = bytes([0] * bytes_on_socket) response = mock_dhcp._receive_dhcp_response() assert response == bytes_on_socket assert response > 236 @@ -579,13 +579,20 @@ def test_receive_response_good_data(self, mock_dhcp, bytes_on_socket): ) def test_receive_response_short_packet(self, mock_dhcp, bytes_on_socket): mock_dhcp._next_resend = time.monotonic() + 15 - mock_dhcp._sock.recv.side_effect = bytes_on_socket + mock_dhcp._wiz_sock.recv.side_effect = bytes_on_socket assert mock_dhcp._receive_dhcp_response() > 236 @freeze_time("2022-10-10", auto_tick_seconds=5) def test_timeout(self, mock_dhcp): mock_dhcp._next_resend = time.monotonic() + 15 - mock_dhcp._sock.recv.side_effect = [b"", b"", b"", b"", b"", bytes([0] * 240)] + mock_dhcp._wiz_sock.recv.side_effect = [ + b"", + b"", + b"", + b"", + b"", + bytes([0] * 240), + ] assert mock_dhcp._receive_dhcp_response() == 0 @freeze_time("2022-10-10") @@ -597,21 +604,21 @@ def test_buffer_handling(self, mock_dhcp, bytes_returned): expected_result = bytearray([2] * total_bytes) + ( bytes([0] * (wiz_dhcp.BUFF_LENGTH - total_bytes)) ) - mock_dhcp._sock.recv.side_effect = (bytes([2] * x) for x in bytes_returned) + mock_dhcp._wiz_sock.recv.side_effect = (bytes([2] * x) for x in bytes_returned) assert mock_dhcp._receive_dhcp_response() == total_bytes assert wiz_dhcp._BUFF == expected_result @freeze_time("2022-10-10") def test_buffer_does_not_overrun(self, mocker, mock_dhcp): mock_dhcp._next_resend = time.monotonic() + 15 - mock_dhcp._sock.recv.return_value = bytes([2] * wiz_dhcp.BUFF_LENGTH) + mock_dhcp._wiz_sock.recv.return_value = bytes([2] * wiz_dhcp.BUFF_LENGTH) mock_dhcp._receive_dhcp_response() - mock_dhcp._sock.recv.assert_called_once_with(wiz_dhcp.BUFF_LENGTH) - mock_dhcp._sock.recv.reset_mock() - mock_dhcp._sock.recv.side_effect = [bytes([2] * 200), bytes([2] * 118)] + mock_dhcp._wiz_sock.recv.assert_called_once_with(wiz_dhcp.BUFF_LENGTH) + mock_dhcp._wiz_sock.recv.reset_mock() + mock_dhcp._wiz_sock.recv.side_effect = [bytes([2] * 200), bytes([2] * 118)] mock_dhcp._receive_dhcp_response() - assert mock_dhcp._sock.recv.call_count == 2 - assert mock_dhcp._sock.recv.call_args_list == [ + assert mock_dhcp._wiz_sock.recv.call_count == 2 + assert mock_dhcp._wiz_sock.recv.call_args_list == [ mocker.call(318), mocker.call(118), ] diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index 0a8c30a..efe726d 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -25,7 +25,7 @@ def mock_dhcp(mocker): dhcp = wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) # Mock the socket for injecting recv() and available() values and to monitor calls # to send() - dhcp._sock = mocker.patch( + dhcp._wiz_sock = mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket.socket", autospec=True ) # Mock the _receive_dhcp_response method to select whether a response was received, @@ -88,7 +88,7 @@ def test_with_valid_data_on_socket_requesting_not_renew(self, mock_dhcp): # Receive the expected OFFER message type. mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK # Have some data on the socket. - mock_dhcp._sock.available.return_value = 24 + mock_dhcp._wiz_sock.available.return_value = 24 # Avoid a timeout before checking the message. mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. @@ -102,7 +102,7 @@ def test_with_valid_data_on_socket_requesting_not_renew(self, mock_dhcp): # Renew has not changed. assert mock_dhcp._renew is False # The socket has been released. - assert mock_dhcp._sock is None + assert mock_dhcp._wiz_sock is None # The correct state has been set. assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_BOUND # No DHCP message to be sent. @@ -170,7 +170,7 @@ def test_with_wrong_message_type_on_socket_blocking( # Receive the incorrect message types, then a correct one. mock_dhcp._parse_dhcp_response.side_effect = msg_type # Have some data on the socket. - mock_dhcp._sock.available.return_value = 32 + mock_dhcp._wiz_sock.available.return_value = 32 # Avoid a timeout before checking the message. mock_dhcp._next_resend = time.monotonic() + 5 # Test an initial negotiation, not a renewal or rebind. @@ -237,7 +237,7 @@ def test_with_no_data_on_socket_nonblocking( # Receive should only be called once in nonblocking mode. mock_dhcp._receive_dhcp_response.assert_called_once() # Check that no data was read from the socket. - mock_dhcp._sock.recv.assert_not_called() + mock_dhcp._wiz_sock.recv.assert_not_called() # Confirm that the FSM state has not changed. assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_SELECTING @@ -307,7 +307,7 @@ def test_timeout_blocking( # Start FSM in required state. mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING # Never have data on the socket to force a timeout. - mock_dhcp._sock.available.return_value = 0 + mock_dhcp._wiz_sock.available.return_value = 0 # Set an initial value for timeout. mock_dhcp._next_resend = time.monotonic() + 5 # Set maximum retries to 3. @@ -329,7 +329,7 @@ def test_timeout_nonblocking( # Start FSM in required state. mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING # Never have data on the socket to force a timeout. - mock_dhcp._sock.available.return_value = 0 + mock_dhcp._wiz_sock.available.return_value = 0 # Set up initial values for the test mock_dhcp._next_resend = time.monotonic() + 5 # Set maximum retries to 3. @@ -351,7 +351,7 @@ def test_requesting_with_renew_nak(self, mock_dhcp): # Return a correct message type. mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_NAK # Have some data on the socket. - mock_dhcp._sock.available.return_value = 32 + mock_dhcp._wiz_sock.available.return_value = 32 # Avoid a timeout before checking the message. mock_dhcp._next_resend = time.monotonic() + 5 # Put FSM into nonblocking mode so that a single attempt is made. @@ -373,7 +373,7 @@ def test_requesting_with_renew_ack(self, mock_dhcp): # Return a correct message type. mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK # Have some data on the socket. - mock_dhcp._sock.available.return_value = 32 + mock_dhcp._wiz_sock.available.return_value = 32 # Avoid a timeout before checking the message. mock_dhcp._next_resend = time.monotonic() + 5 # Put FSM into nonblocking mode so that a single attempt is made. @@ -385,7 +385,7 @@ def test_requesting_with_renew_ack(self, mock_dhcp): # Lease renewed so confirm _renew is False. assert mock_dhcp._renew is False # Confirm that socket was released. - assert mock_dhcp._sock is None + assert mock_dhcp._wiz_sock is None # Confirm that the state is BOUND. assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_BOUND @@ -395,7 +395,7 @@ def test_requesting_with_renew_no_data(self, mock_dhcp): # Start FSM in required state. mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING # Never have data on the socket. - mock_dhcp._sock.available.return_value = 0 + mock_dhcp._wiz_sock.available.return_value = 0 # Initial timeout value. mock_dhcp._next_resend = time.monotonic() + 5 # Put FSM into nonblocking mode so that a single attempt is made. @@ -415,7 +415,7 @@ def test_requesting_with_timeout_renew(self, mock_dhcp): # Start FSM in required state. mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING # Never have data on the socket. - mock_dhcp._sock.available.return_value = 0 + mock_dhcp._wiz_sock.available.return_value = 0 # Initial timeout value. mock_dhcp._next_resend = time.monotonic() + 5 # Set retries to 3 so that it times out on the first pass. @@ -500,7 +500,7 @@ def test_renewing_state(self, mocker, mock_dhcp): assert mock_dhcp._renew is True assert mock_dhcp._start_time == time.monotonic() - mock_dhcp._socket_setup.assert_called_once() + mock_dhcp._dhcp_connection_setup.assert_called_once() mock_dhcp._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @@ -516,7 +516,7 @@ def test_rebinding_state(self, mocker, mock_dhcp): assert mock_dhcp.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR assert mock_dhcp._renew is True assert mock_dhcp._start_time == time.monotonic() - mock_dhcp._socket_setup.assert_called_once() + mock_dhcp._dhcp_connection_setup.assert_called_once() mock_dhcp._send_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) From 29fc1c857d5d75876062d7b877f95b5fd0fd7fa9 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sun, 8 Jan 2023 17:50:23 +1100 Subject: [PATCH 27/80] Refactored DHCP to remove all socket.socket calls. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 41 +++++++++++---------- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 15 ++++---- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 4aae19d..19a4495 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -696,8 +696,8 @@ def socket_connect( raise RuntimeError("Failed to initialize a connection with the socket.") # set socket destination IP and port - self._write_sndipr(socket_num, dest) - self._write_sndport(socket_num, port) + self.write_sndipr(socket_num, dest) + self.write_sndport(socket_num, port) self._send_socket_cmd(socket_num, CMD_SOCK_CONNECT) if conn_mode == SNMR_TCP: @@ -859,7 +859,10 @@ def socket_close(self, socket_num: int) -> None: if self._debug: print("*** Closing socket #%d" % socket_num) self.write_sncr(socket_num, CMD_SOCK_CLOSE) - self.read_sncr(socket_num) + while self.read_sncr(socket_num): + time.sleep(0.0001) + while self.read_snsr(socket_num) != SNSR_SOCK_CLOSED: + time.sleep(0.0001) def socket_disconnect(self, socket_num: int) -> None: """ @@ -872,9 +875,7 @@ def socket_disconnect(self, socket_num: int) -> None: self.write_sncr(socket_num, CMD_SOCK_DISCON) self.read_sncr(socket_num) - def socket_read( - self, socket_num: int, length: int - ) -> Tuple[int, Union[int, bytearray]]: + def socket_read(self, socket_num: int, length: int) -> Tuple[int, bytes]: """ Read data from a TCP socket. @@ -890,24 +891,24 @@ def socket_read( assert socket_num <= self.max_sockets, "Provided socket exceeds max_sockets." # Check if there is data available on the socket + resp = b"" ret = self._get_rx_rcv_size(socket_num) if self._debug: print("Bytes avail. on sock: ", ret) if ret == 0: # no data on socket? - status = self._read_snmr(socket_num) - if status in (SNSR_SOCK_LISTEN, SNSR_SOCK_CLOSED, SNSR_SOCK_CLOSE_WAIT): + if self._read_snmr(socket_num) in ( + SNSR_SOCK_LISTEN, + SNSR_SOCK_CLOSED, + SNSR_SOCK_CLOSE_WAIT, + ): # remote end closed its side of the connection, EOF state - ret = 0 - resp = 0 - else: + raise RuntimeError("Lost connection to peer.") # connection is alive, no data waiting to be read - ret = -1 - resp = -1 + ret = -1 elif ret > length: # set ret to the length of buffer ret = length - if ret > 0: if self._debug: print("\t * Processing {} bytes of data".format(ret)) @@ -940,7 +941,8 @@ def socket_read( # Notify the W5k of the updated Sn_Rx_RD self.write_sncr(socket_num, CMD_SOCK_RECV) - self.read_sncr(socket_num) + while self.read_sncr(socket_num): + time.sleep(0.0001) return ret, resp def read_udp( @@ -1028,7 +1030,8 @@ def socket_write( self._write_sntx_wr(socket_num, ptr) self.write_sncr(socket_num, CMD_SOCK_SEND) - self.read_sncr(socket_num) + while self.read_sncr(socket_num): + time.sleep(0.0001) # check data was transferred correctly while not self._read_snir(socket_num)[0] & SNIR_SEND_OK: @@ -1105,7 +1108,7 @@ def _read_snrx_rsr(self, sock: int) -> Optional[bytearray]: data += self._read_socket(sock, REG_SNRX_RSR + 1) return data - def _write_sndipr(self, sock: int, ip_addr: bytearray) -> None: + def write_sndipr(self, sock: int, ip_addr: bytearray) -> None: """Write to socket destination IP Address.""" for offset in range(4): self._write_socket(sock, REG_SNDIPR + offset, ip_addr[offset]) @@ -1117,7 +1120,7 @@ def _read_sndipr(self, sock) -> bytearray: data += self._read_socket(sock, REG_SIPR + offset) return bytearray(data) - def _write_sndport(self, sock: int, port: int) -> None: + def write_sndport(self, sock: int, port: int) -> None: """Write to socket destination port.""" self._write_socket(sock, REG_SNDPORT, port >> 8) self._write_socket(sock, REG_SNDPORT + 1, port & 0xFF) @@ -1258,7 +1261,7 @@ def _ip_address_in_use(self, socknum, local_ip) -> bool: ip_in_use = False finally: # Reset the RTR, RCR and destination IPv4 registers. - self._write_sndipr(socknum, temp_ip) + self.write_sndipr(socknum, temp_ip) self.rcr = temp_rcr self.rtr = temp_rtr return ip_in_use diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index f87ab11..a44fede 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -28,7 +28,6 @@ import time from random import randint from micropython import const -import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket # DHCP State Machine STATE_INIT = const(0x01) @@ -65,8 +64,8 @@ DHCP_SERVER_PORT = const(67) # DHCP Lease Time, in seconds DEFAULT_LEASE_TIME = const(900) -BROADCAST_SERVER_ADDR = (255, 255, 255, 255) -UNASSIGNED_IP_ADDR = (0, 0, 0, 0) +BROADCAST_SERVER_ADDR = b"/xff/xff/xff/xff" # (255.255.255.255) +UNASSIGNED_IP_ADDR = b"/x00/x00/x00/x00" # (0.0.0.0) # DHCP Response Options MSG_TYPE = 53 @@ -150,7 +149,6 @@ def __init__( self._mac_address = mac_address # Set socket interface - socket.set_interface(eth) self._eth = eth self._wiz_sock = None @@ -237,8 +235,6 @@ def _dhcp_connection_setup(self, timeout: int = 5) -> None: :raises TimeoutError: If the FSM is in blocking mode and a socket cannot be initialised. """ - _debugging_message("Setting up a socket.", self._debug) - self._socket_release() stop_time = time.monotonic() + timeout _debugging_message("Creating new socket instance for DHCP.", self._debug) while not self._wiz_sock and time.monotonic() < stop_time: @@ -254,6 +250,7 @@ def _dhcp_connection_setup(self, timeout: int = 5) -> None: self._eth.read_sncr(self._wiz_sock) == 0 ): # Wait for command to complete. time.sleep(0.0001) + self._eth.write_sndport(self._wiz_sock, DHCP_SERVER_PORT) def _increment_transaction_id(self) -> None: """Increment the transaction ID and roll over from 0x7fffffff to 0.""" @@ -312,6 +309,8 @@ def _send_message_set_next_state( message_type = DHCP_REQUEST self._generate_dhcp_message(message_type=message_type) self._wiz_sock.send(_BUFF) + self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) + self._eth.socket_write(self._wiz_sock, _BUFF) self._retries = 0 self._max_retries = max_retries self._next_resend = self._next_retry_time_and_retry() @@ -338,7 +337,9 @@ def _receive_dhcp_response(self) -> int: while ( bytes_read <= minimum_packet_length and time.monotonic() < self._next_resend ): - buffer.extend(self._wiz_sock.recv(BUFF_LENGTH - bytes_read)) + buffer.extend( + self._eth.socket_read(self._wiz_sock, BUFF_LENGTH - bytes_read)[1] + ) bytes_read = len(buffer) if bytes_read == BUFF_LENGTH: break From 08345f35d1ad1b41889593a673e3472814295131 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 9 Jan 2023 17:02:57 +1100 Subject: [PATCH 28/80] Fixed _dhcp_connection_setup. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 21 ++++----- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 52 ++++++++++++--------- tests/test_dhcp_helper_functions.py | 49 +++++++------------ tests/test_dhcp_main_logic.py | 20 ++++---- 4 files changed, 64 insertions(+), 78 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 19a4495..c8732f1 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -555,7 +555,6 @@ def read( addr: int, callback: int, length: int = 1, - buffer: Optional[WriteableBuffer] = None, ) -> Union[WriteableBuffer, bytearray]: """ Read data from a register address. @@ -563,9 +562,8 @@ def read( :param int addr: Register address to read. :param int callback: Callback reference. :param int length: Number of bytes to read from the register, defaults to 1. - :param Optional[WriteableBuffer] buffer: Buffer to read data into, defaults to None. - :return Union[WriteableBuffer, bytearray]: Data read from the chip. + :return bytes: Data read from the chip. """ with self._device as bus_device: if self._chip_type == "w5500": @@ -578,12 +576,9 @@ def read( bus_device.write(bytes([addr >> 8])) # pylint: disable=no-member bus_device.write(bytes([addr & 0xFF])) # pylint: disable=no-member - if buffer is None: - self._rxbuf = bytearray(length) - bus_device.readinto(self._rxbuf) # pylint: disable=no-member - return self._rxbuf - bus_device.readinto(buffer, end=length) # pylint: disable=no-member - return buffer + self._rxbuf = bytearray(length) + bus_device.readinto(self._rxbuf) # pylint: disable=no-member + return bytes(self._rxbuf) def write( self, addr: int, callback: int, data: Union[int, Sequence[Union[int, bytes]]] @@ -827,7 +822,7 @@ def socket_open(self, socket_num: int, conn_mode: int = SNMR_TCP) -> int: print("* Opening W5k Socket, protocol={}".format(conn_mode)) time.sleep(0.00025) - self._write_snmr(socket_num, conn_mode) + self.write_snmr(socket_num, conn_mode) self._write_snir(socket_num, 0xFF) if self.src_port > 0: @@ -1133,7 +1128,7 @@ def _read_snir(self, sock: int) -> Optional[bytearray]: """Read Socket n Interrupt Register.""" return self._read_socket(sock, REG_SNIR) - def _write_snmr(self, sock: int, protocol: int) -> None: + def write_snmr(self, sock: int, protocol: int) -> None: """Write to Socket n Mode Register.""" self._write_socket(sock, REG_SNMR, protocol) @@ -1169,7 +1164,7 @@ def _write_socket(self, sock: int, address: int, data: int) -> None: ) return None - def _read_socket(self, sock: int, address: int) -> Optional[bytearray]: + def _read_socket(self, sock: int, address: int) -> bytearray: """Read a W5k socket register.""" if self._chip_type == "w5500": cntl_byte = (sock << 5) + 0x08 @@ -1177,7 +1172,7 @@ def _read_socket(self, sock: int, address: int) -> Optional[bytearray]: if self._chip_type == "w5100s": cntl_byte = 0 return self.read(self._ch_base_msb + sock * CH_SIZE + address, cntl_byte) - return None + raise RuntimeError("Invalid Wiznet chip type.") @property def rcr(self) -> int: diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index a44fede..2dd67a4 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -198,6 +198,7 @@ def _dsm_reset(self) -> None: state machine INIT state.""" _debugging_message("Resetting DHCP state machine.", self._debug) self._socket_release() + self._dhcp_connection_setup() self.dhcp_server_ip = BROADCAST_SERVER_ADDR self._eth.ifconfig = ( UNASSIGNED_IP_ADDR, @@ -237,20 +238,22 @@ def _dhcp_connection_setup(self, timeout: int = 5) -> None: """ stop_time = time.monotonic() + timeout _debugging_message("Creating new socket instance for DHCP.", self._debug) - while not self._wiz_sock and time.monotonic() < stop_time: + while self._wiz_sock is None and time.monotonic() < stop_time: self._wiz_sock = self._eth.get_socket() if self._wiz_sock == 0xFF: self._wiz_sock = None - while ( - time.monotonic() < stop_time and self._eth.read_snsr(self._wiz_sock) != 0x22 - ): + while time.monotonic() < stop_time: + self._eth.write_snmr(self._wiz_sock, 0x02) # Set UDP connection self._eth.write_sock_port(self._wiz_sock, 68) # Set DHCP client port. self._eth.write_sncr(self._wiz_sock, 0x01) # Open the socket. while ( self._eth.read_sncr(self._wiz_sock) == 0 ): # Wait for command to complete. - time.sleep(0.0001) - self._eth.write_sndport(self._wiz_sock, DHCP_SERVER_PORT) + time.sleep(0.001) + if self._eth.read_snsr(self._wiz_sock) == bytes([0x22]): + self._eth.write_sndport(self._wiz_sock, DHCP_SERVER_PORT) + return + raise RuntimeError("Unable to initialize UDP socket.") def _increment_transaction_id(self) -> None: """Increment the transaction ID and roll over from 0x7fffffff to 0.""" @@ -281,7 +284,7 @@ def _next_retry_time_and_retry(self, *, interval: int = 4) -> int: self._retries += 1 return delay - def _send_message_set_next_state( + def _prepare_message_set_next_state( self, *, next_state: int, @@ -303,14 +306,6 @@ def _send_message_set_next_state( ) if next_state not in (STATE_SELECTING, STATE_REQUESTING): raise ValueError("The next state must be SELECTING or REQUESTING.") - if next_state == STATE_SELECTING: - message_type = DHCP_DISCOVER - else: - message_type = DHCP_REQUEST - self._generate_dhcp_message(message_type=message_type) - self._wiz_sock.send(_BUFF) - self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) - self._eth.socket_write(self._wiz_sock, _BUFF) self._retries = 0 self._max_retries = max_retries self._next_resend = self._next_retry_time_and_retry() @@ -365,14 +360,14 @@ def _handle_dhcp_message(self) -> None: :raises TimeoutError: If the FSM is in blocking mode and no valid response has been received before the timeout expires. """ - + # pylint: disable=too-many-branches def processing_state_selecting(): """Process a message while the FSM is in SELECTING state.""" if self._dhcp_state == STATE_SELECTING and msg_type == DHCP_OFFER: _debugging_message( "FSM state is SELECTING with valid OFFER.", self._debug ) - self._send_message_set_next_state( + self._prepare_message_set_next_state( next_state=STATE_REQUESTING, max_retries=3, ) @@ -409,8 +404,17 @@ def processing_state_requesting(): # Main processing loop _debugging_message("Processing SELECTING or REQUESTING state.", self._debug) + if self._dhcp_state == STATE_SELECTING: + message_type = DHCP_DISCOVER + elif self._dhcp_state == STATE_REQUESTING: + message_type = DHCP_REQUEST + else: + raise ValueError("Wrong FSM state attempting to send message.") while True: while time.monotonic() < self._next_resend: + self._generate_dhcp_message(message_type=message_type) + self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) + self._eth.socket_write(self._wiz_sock, _BUFF) if self._receive_dhcp_response(): try: msg_type = self._parse_dhcp_response() @@ -480,7 +484,7 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._renew = True self._dhcp_connection_setup() self._start_time = time.monotonic() - self._send_message_set_next_state( + self._prepare_message_set_next_state( next_state=STATE_REQUESTING, max_retries=3, ) @@ -490,14 +494,14 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self.dhcp_server_ip = BROADCAST_SERVER_ADDR self._dhcp_connection_setup() self._start_time = time.monotonic() - self._send_message_set_next_state( + self._prepare_message_set_next_state( next_state=STATE_REQUESTING, max_retries=3, ) if self._dhcp_state == STATE_INIT: self._dsm_reset() - self._send_message_set_next_state( + self._prepare_message_set_next_state( next_state=STATE_SELECTING, max_retries=3, ) @@ -552,13 +556,15 @@ def option_writer( _BUFF[offset] = data_length offset += 1 data_end = offset + data_length - _BUFF[offset:data_end] = option_data + _BUFF[offset:data_end] = bytes(option_data) return data_end # global _BUFF # pylint: disable=global-variable-not-assigned _BUFF[:] = bytearray(BUFF_LENGTH) # OP.HTYPE.HLEN.HOPS - _BUFF[0:4] = (DHCP_BOOT_REQUEST, DHCP_HTYPE10MB, DHCP_HLENETHERNET, DHCP_HOPS) + _BUFF[0:4] = bytes( + [DHCP_BOOT_REQUEST, DHCP_HTYPE10MB, DHCP_HLENETHERNET, DHCP_HOPS] + ) # Transaction ID (xid) _BUFF[4:8] = self._transaction_id.to_bytes(4, "big") # Seconds elapsed @@ -577,10 +583,12 @@ def option_writer( pointer = 240 # Option - DHCP Message Type + print("Option - DHCP Message Type") pointer = option_writer( offset=pointer, option_code=53, option_data=(message_type,) ) # Option - Host Name + print("Option - Host Name") pointer = option_writer( offset=pointer, option_code=12, option_data=self._hostname ) diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 77dba92..133a32e 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -20,19 +20,8 @@ def mock_wiznet5k(mocker): @pytest.fixture -def mock_socket(mocker): - """Mock socket module to allow test data to be read and written by the DHCP module.""" - return mocker.patch( - "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket", autospec=True - ) - - -@pytest.fixture -def mock_dhcp(mocker, mock_wiznet5k): +def mock_dhcp(mock_wiznet5k): dhcp = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp._wiz_sock = mocker.patch( - "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket.socket", autospec=True - ) return dhcp @@ -74,8 +63,8 @@ def test_constants(self): assert wiz_dhcp.DHCP_SERVER_PORT == const(67) # DHCP Lease Time, in seconds assert wiz_dhcp.DEFAULT_LEASE_TIME == const(900) - assert wiz_dhcp.BROADCAST_SERVER_ADDR == (255, 255, 255, 255) - assert wiz_dhcp.UNASSIGNED_IP_ADDR == (0, 0, 0, 0) + assert wiz_dhcp.BROADCAST_SERVER_ADDR == b"/xff/xff/xff/xff" + assert wiz_dhcp.UNASSIGNED_IP_ADDR == b"/x00/x00/x00/x00" # DHCP Response Options assert wiz_dhcp.MSG_TYPE == 53 @@ -100,7 +89,7 @@ def test_constants(self): bytes([1, 2, 4, 6, 7, 8]), ), ) - def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mock_socket, mac_address): + def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mac_address): """Test intial settings from DHCP.__init__.""" # Test with mac address as tuple, list and bytes with default values. mock_randint = mocker.patch( @@ -112,7 +101,6 @@ def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mock_socket, mac_addres assert dhcp_client._response_timeout == 30.0 assert dhcp_client._debug is False assert dhcp_client._mac_address == mac_address - mock_socket.set_interface.assert_called_once_with(mock_wiznet5k) assert dhcp_client._wiz_sock is None assert dhcp_client._dhcp_state == wiz_dhcp.STATE_INIT mock_randint.assert_called_once() @@ -152,11 +140,10 @@ def test_dhcp_setup_other_args(self, mock_wiznet5k): @freeze_time("2022-10-20") class TestSendDHCPMessage: - def test_generate_message_with_default_attributes(self, mock_wiznet5k, mock_socket): + def test_generate_message_with_default_attributes(self, mock_wiznet5k): """Test the _generate_message function with default values.""" assert len(wiz_dhcp._BUFF) == 318 dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) - dhcp_client._wiz_sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) dhcp_client._transaction_id = 0x6FFFFFFF dhcp_client._start_time = time.monotonic() - 23.4 dhcp_client._generate_dhcp_message(message_type=wiz_dhcp.DHCP_DISCOVER) @@ -349,12 +336,11 @@ def test_parse_good_data( assert dhcp_client._t1 == t1 assert dhcp_client._t2 == t2 - def test_parsing_failures(self, mock_wiznet5k, mock_socket): + def test_parsing_failures(self, mock_wiznet5k): # Test for bad OP code, ID mismatch, no server ID, bad Magic Cookie bad_data = dhcp_data.BAD_DATA dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp_client._wiz_sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) - dhcp_client._wiz_sock.recv.return_value = bad_data + dhcp_client._eth._read_socket.return_value = (len(bad_data), bad_data) # Transaction ID mismatch. dhcp_client._transaction_id = 0x42424242 with pytest.raises(ValueError): @@ -377,14 +363,12 @@ def test_parsing_failures(self, mock_wiznet5k, mock_socket): class TestResetDsmReset: - def test_socket_reset(self, mock_wiznet5k, mock_socket): + def test_socket_reset(self, mock_wiznet5k): dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._dsm_reset() assert dhcp_client._wiz_sock is None - dhcp_client._wiz_sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) - dhcp_client._dsm_reset() assert dhcp_client._wiz_sock is None @@ -413,12 +397,12 @@ def test_reset_dsm_parameters(self, mock_wiznet5k): class TestSocketRelease: - def test_socket_set_to_none(self, mock_wiznet5k, mock_socket): + def test_socket_set_to_none(self, mock_wiznet5k): dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._socket_release() assert dhcp_client._wiz_sock is None - dhcp_client._wiz_sock = mock_socket.socket(type=mock_socket.SOCK_DGRAM) + dhcp_client._wiz_sock = 2 dhcp_client._socket_release() assert dhcp_client._wiz_sock is None @@ -468,13 +452,12 @@ def test_next_retry_time_optional_attrs(self, mocker, mock_wiznet5k, interval): 2**retry * interval + now ) - def test_setup_socket_with_no_error(self, mock_wiznet5k, mock_socket): + def test_setup_socket_with_no_error(self, mock_wiznet5k): + mock_wiznet5k.get_socket.return_value = 2 dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._dhcp_connection_setup() - mock_socket.socket.assert_called_once() - dhcp_client._wiz_sock.settimeout.assert_called_once_with( - dhcp_client._response_timeout - ) + mock_wiznet5k.get_socket.assert_called_once() + # mock_wiznet5k.write dhcp_client._wiz_sock.bind.assert_called_once_with((None, 68)) dhcp_client._wiz_sock.connect.assert_called_once_with( (wiz_dhcp.BROADCAST_SERVER_ADDR, 67) @@ -528,7 +511,7 @@ def test_send_message_set_next_state_good_data( wiz_dhcp._BUFF = bytearray(message) dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._wiz_sock = mocker.Mock() - dhcp_client._send_message_set_next_state( + dhcp_client._prepare_message_set_next_state( next_state=next_state, max_retries=retries ) assert dhcp_client._retries == 0 @@ -553,7 +536,7 @@ def test_send_message_set_next_state_bad_state(self, mock_wiznet5k, next_state): # Test with all states that should not call this function. dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) with pytest.raises(ValueError): - dhcp_client._send_message_set_next_state( + dhcp_client._prepare_message_set_next_state( next_state=next_state, max_retries=4 ) diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index efe726d..46013ef 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -76,7 +76,7 @@ def test_with_valid_data_on_socket_selecting(self, mock_dhcp): # Confirm that the message would be parsed. mock_dhcp._parse_dhcp_response.assert_called_once() # Confirm that the correct next FSM state was set. - mock_dhcp._send_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @@ -106,7 +106,7 @@ def test_with_valid_data_on_socket_requesting_not_renew(self, mock_dhcp): # The correct state has been set. assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_BOUND # No DHCP message to be sent. - mock_dhcp._send_message_set_next_state.assert_not_called() + mock_dhcp._prepare_message_set_next_state.assert_not_called() @freeze_time("2022-06-10") @pytest.mark.parametrize( @@ -137,7 +137,7 @@ def test_with_wrong_message_type_on_socket_nonblocking( mock_dhcp._handle_dhcp_message() # Only one call methods because nonblocking, so no attempt to get another message. mock_dhcp._parse_dhcp_response.assert_called_once() - mock_dhcp._send_message_set_next_state.assert_not_called() + mock_dhcp._prepare_message_set_next_state.assert_not_called() # Confirm that the FSM state has not changed. assert mock_dhcp._dhcp_state == fsm_state @@ -183,9 +183,9 @@ def test_with_wrong_message_type_on_socket_blocking( assert mock_dhcp._parse_dhcp_response.call_count == len(msg_type) # Confirm correct calls to _send_message_set_next_state are made. if fsm_state == wiz_dhcp.STATE_SELECTING: - mock_dhcp._send_message_set_next_state.assert_called_once() + mock_dhcp._prepare_message_set_next_state.assert_called_once() elif fsm_state == wiz_dhcp.STATE_REQUESTING: # Not called for STATE_REQUESTING - mock_dhcp._send_message_set_next_state.assert_not_called() + mock_dhcp._prepare_message_set_next_state.assert_not_called() # Confirm correct final FSM state. assert mock_dhcp._dhcp_state == next_state @@ -265,7 +265,7 @@ def test_with_valueerror_nonblocking( # Receive should only be called once in nonblocking mode. mock_dhcp._receive_dhcp_response.assert_called_once() # Confirm that _send_message_set_next_state not called. - mock_dhcp._send_message_set_next_state.assert_not_called() + mock_dhcp._prepare_message_set_next_state.assert_not_called() # Check that FSM state has not changed. assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_SELECTING @@ -294,7 +294,7 @@ def test_with_valueerror_blocking( # Check available() called three times. assert mock_dhcp._receive_dhcp_response.call_count == 3 # Confirm that _send_message_set_next_state was called to change FSM state. - mock_dhcp._send_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @@ -440,7 +440,7 @@ def test_init_state(self, mocker, mock_dhcp): mock_dhcp._dhcp_state_machine() mock_dhcp._dsm_reset.assert_called_once() - mock_dhcp._send_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_SELECTING, max_retries=3 ) @@ -501,7 +501,7 @@ def test_renewing_state(self, mocker, mock_dhcp): assert mock_dhcp._renew is True assert mock_dhcp._start_time == time.monotonic() mock_dhcp._dhcp_connection_setup.assert_called_once() - mock_dhcp._send_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @@ -517,6 +517,6 @@ def test_rebinding_state(self, mocker, mock_dhcp): assert mock_dhcp._renew is True assert mock_dhcp._start_time == time.monotonic() mock_dhcp._dhcp_connection_setup.assert_called_once() - mock_dhcp._send_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_message_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) From 0a8eb0fbde521c5bf04267cede7ee8ad275af18e Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 9 Jan 2023 23:01:16 +1100 Subject: [PATCH 29/80] Fixed sending date. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 18 +++++++++--------- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 18 ++++++++++-------- tests/test_dhcp_helper_functions.py | 4 ++-- tests/test_dhcp_main_logic.py | 20 ++++++++++---------- 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index c8732f1..baba74c 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -977,6 +977,7 @@ def socket_write( :return int: The number of bytes written to the buffer. """ + # pylint: disable=too-many-branches assert self.link_status, "Ethernet cable disconnected!" assert socket_num <= self.max_sockets, "Provided socket exceeds max_sockets." if len(buffer) > SOCK_SIZE: @@ -985,7 +986,7 @@ def socket_write( ret = len(buffer) stamp = time.monotonic() - # if buffer is available, start the transfer + # If buffer is available, start the transfer free_size = self._get_tx_free_size(socket_num) while free_size < ret: free_size = self._get_tx_free_size(socket_num) @@ -1001,7 +1002,6 @@ def socket_write( offset = ptr & SOCK_MASK if self._chip_type == "w5500": dst_addr = offset + (socket_num * SOCK_SIZE + 0x8000) - txbuf = buffer[:ret] cntl_byte = 0x14 + (socket_num << 5) self.write(dst_addr, cntl_byte, txbuf) @@ -1023,10 +1023,9 @@ def socket_write( # update sn_tx_wr to the value + data size ptr = (ptr + ret) & 0xFFFF self._write_sntx_wr(socket_num, ptr) - self.write_sncr(socket_num, CMD_SOCK_SEND) - while self.read_sncr(socket_num): - time.sleep(0.0001) + while self.read_sncr(socket_num) != b"\x00": + time.sleep(0.001) # check data was transferred correctly while not self._read_snir(socket_num)[0] & SNIR_SEND_OK: @@ -1036,14 +1035,15 @@ def socket_write( SNSR_SOCK_FIN_WAIT, SNSR_SOCK_CLOSE_WAIT, SNSR_SOCK_CLOSING, - ) or (timeout and time.monotonic() - stamp > timeout): - # self.socket_close(socket_num) - return 0 + ): + raise RuntimeError("Socket closed before data was sent.") + if timeout and time.monotonic() - stamp > timeout: + raise RuntimeError("Operation timed out. No data sent.") if self._read_snir(socket_num)[0] & SNIR_TIMEOUT: raise TimeoutError( "Hardware timeout while sending on socket {}.".format(socket_num) ) - time.sleep(0.01) + time.sleep(0.001) self._write_snir(socket_num, SNIR_SEND_OK) return ret diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 2dd67a4..697d436 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -64,8 +64,8 @@ DHCP_SERVER_PORT = const(67) # DHCP Lease Time, in seconds DEFAULT_LEASE_TIME = const(900) -BROADCAST_SERVER_ADDR = b"/xff/xff/xff/xff" # (255.255.255.255) -UNASSIGNED_IP_ADDR = b"/x00/x00/x00/x00" # (0.0.0.0) +BROADCAST_SERVER_ADDR = b"\xff\xff\xff\xff" # (255.255.255.255) +UNASSIGNED_IP_ADDR = b"\x00\x00\x00\x00" # (0.0.0.0) # DHCP Response Options MSG_TYPE = 53 @@ -284,7 +284,7 @@ def _next_retry_time_and_retry(self, *, interval: int = 4) -> int: self._retries += 1 return delay - def _prepare_message_set_next_state( + def _prepare_and_set_next_state( self, *, next_state: int, @@ -301,7 +301,7 @@ def _prepare_message_set_next_state( :raises ValueError: If the next FSM state does not handle DHCP messages. """ _debugging_message( - "Setting next FSM state to {} and sending a message.".format(next_state), + "Setting next FSM state to {}.".format(next_state), self._debug, ) if next_state not in (STATE_SELECTING, STATE_REQUESTING): @@ -367,7 +367,7 @@ def processing_state_selecting(): _debugging_message( "FSM state is SELECTING with valid OFFER.", self._debug ) - self._prepare_message_set_next_state( + self._prepare_and_set_next_state( next_state=STATE_REQUESTING, max_retries=3, ) @@ -414,7 +414,9 @@ def processing_state_requesting(): while time.monotonic() < self._next_resend: self._generate_dhcp_message(message_type=message_type) self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) + print("Sending the message.") self._eth.socket_write(self._wiz_sock, _BUFF) + print("Waiting for response…") if self._receive_dhcp_response(): try: msg_type = self._parse_dhcp_response() @@ -484,7 +486,7 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._renew = True self._dhcp_connection_setup() self._start_time = time.monotonic() - self._prepare_message_set_next_state( + self._prepare_and_set_next_state( next_state=STATE_REQUESTING, max_retries=3, ) @@ -494,14 +496,14 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self.dhcp_server_ip = BROADCAST_SERVER_ADDR self._dhcp_connection_setup() self._start_time = time.monotonic() - self._prepare_message_set_next_state( + self._prepare_and_set_next_state( next_state=STATE_REQUESTING, max_retries=3, ) if self._dhcp_state == STATE_INIT: self._dsm_reset() - self._prepare_message_set_next_state( + self._prepare_and_set_next_state( next_state=STATE_SELECTING, max_retries=3, ) diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 133a32e..631a4ed 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -511,7 +511,7 @@ def test_send_message_set_next_state_good_data( wiz_dhcp._BUFF = bytearray(message) dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._wiz_sock = mocker.Mock() - dhcp_client._prepare_message_set_next_state( + dhcp_client._prepare_and_set_next_state( next_state=next_state, max_retries=retries ) assert dhcp_client._retries == 0 @@ -536,7 +536,7 @@ def test_send_message_set_next_state_bad_state(self, mock_wiznet5k, next_state): # Test with all states that should not call this function. dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) with pytest.raises(ValueError): - dhcp_client._prepare_message_set_next_state( + dhcp_client._prepare_and_set_next_state( next_state=next_state, max_retries=4 ) diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py index 46013ef..3f69bfe 100644 --- a/tests/test_dhcp_main_logic.py +++ b/tests/test_dhcp_main_logic.py @@ -76,7 +76,7 @@ def test_with_valid_data_on_socket_selecting(self, mock_dhcp): # Confirm that the message would be parsed. mock_dhcp._parse_dhcp_response.assert_called_once() # Confirm that the correct next FSM state was set. - mock_dhcp._prepare_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_and_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @@ -106,7 +106,7 @@ def test_with_valid_data_on_socket_requesting_not_renew(self, mock_dhcp): # The correct state has been set. assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_BOUND # No DHCP message to be sent. - mock_dhcp._prepare_message_set_next_state.assert_not_called() + mock_dhcp._prepare_and_set_next_state.assert_not_called() @freeze_time("2022-06-10") @pytest.mark.parametrize( @@ -137,7 +137,7 @@ def test_with_wrong_message_type_on_socket_nonblocking( mock_dhcp._handle_dhcp_message() # Only one call methods because nonblocking, so no attempt to get another message. mock_dhcp._parse_dhcp_response.assert_called_once() - mock_dhcp._prepare_message_set_next_state.assert_not_called() + mock_dhcp._prepare_and_set_next_state.assert_not_called() # Confirm that the FSM state has not changed. assert mock_dhcp._dhcp_state == fsm_state @@ -183,9 +183,9 @@ def test_with_wrong_message_type_on_socket_blocking( assert mock_dhcp._parse_dhcp_response.call_count == len(msg_type) # Confirm correct calls to _send_message_set_next_state are made. if fsm_state == wiz_dhcp.STATE_SELECTING: - mock_dhcp._prepare_message_set_next_state.assert_called_once() + mock_dhcp._prepare_and_set_next_state.assert_called_once() elif fsm_state == wiz_dhcp.STATE_REQUESTING: # Not called for STATE_REQUESTING - mock_dhcp._prepare_message_set_next_state.assert_not_called() + mock_dhcp._prepare_and_set_next_state.assert_not_called() # Confirm correct final FSM state. assert mock_dhcp._dhcp_state == next_state @@ -265,7 +265,7 @@ def test_with_valueerror_nonblocking( # Receive should only be called once in nonblocking mode. mock_dhcp._receive_dhcp_response.assert_called_once() # Confirm that _send_message_set_next_state not called. - mock_dhcp._prepare_message_set_next_state.assert_not_called() + mock_dhcp._prepare_and_set_next_state.assert_not_called() # Check that FSM state has not changed. assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_SELECTING @@ -294,7 +294,7 @@ def test_with_valueerror_blocking( # Check available() called three times. assert mock_dhcp._receive_dhcp_response.call_count == 3 # Confirm that _send_message_set_next_state was called to change FSM state. - mock_dhcp._prepare_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_and_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @@ -440,7 +440,7 @@ def test_init_state(self, mocker, mock_dhcp): mock_dhcp._dhcp_state_machine() mock_dhcp._dsm_reset.assert_called_once() - mock_dhcp._prepare_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_and_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_SELECTING, max_retries=3 ) @@ -501,7 +501,7 @@ def test_renewing_state(self, mocker, mock_dhcp): assert mock_dhcp._renew is True assert mock_dhcp._start_time == time.monotonic() mock_dhcp._dhcp_connection_setup.assert_called_once() - mock_dhcp._prepare_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_and_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) @@ -517,6 +517,6 @@ def test_rebinding_state(self, mocker, mock_dhcp): assert mock_dhcp._renew is True assert mock_dhcp._start_time == time.monotonic() mock_dhcp._dhcp_connection_setup.assert_called_once() - mock_dhcp._prepare_message_set_next_state.assert_called_once_with( + mock_dhcp._prepare_and_set_next_state.assert_called_once_with( next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 ) From 53176d88089cb9e5cea8ebb2f0982caf27a068e2 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 9 Jan 2023 23:16:40 +1100 Subject: [PATCH 30/80] Moved sending message outside receiving loop. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 1 - adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index baba74c..85b7f12 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -1044,7 +1044,6 @@ def socket_write( "Hardware timeout while sending on socket {}.".format(socket_num) ) time.sleep(0.001) - self._write_snir(socket_num, SNIR_SEND_OK) return ret diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 697d436..ac9dacc 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -409,13 +409,14 @@ def processing_state_requesting(): elif self._dhcp_state == STATE_REQUESTING: message_type = DHCP_REQUEST else: - raise ValueError("Wrong FSM state attempting to send message.") + raise ValueError( + "FSM should only send messages in SELECTING or REQUESTING states." + ) while True: + self._generate_dhcp_message(message_type=message_type) + self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) + self._eth.socket_write(self._wiz_sock, _BUFF) while time.monotonic() < self._next_resend: - self._generate_dhcp_message(message_type=message_type) - self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) - print("Sending the message.") - self._eth.socket_write(self._wiz_sock, _BUFF) print("Waiting for response…") if self._receive_dhcp_response(): try: From fce4a569f78e9f561f956439384e64a0c26806ba Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 9 Jan 2023 23:41:33 +1100 Subject: [PATCH 31/80] Fixed DHCP resend timing to match DHCP standard. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index ac9dacc..8c8e013 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -278,11 +278,13 @@ def _next_retry_time_and_retry(self, *, interval: int = 4) -> int: _debugging_message( "Calculating next retry time and incrementing retries.", self._debug ) - delay = int(2**self._retries * interval + randint(-1, 1) + time.monotonic()) + delay = 2**self._retries * interval + randint(-1, 1) + print("delay = {}".format(delay)) + delay += time.monotonic() if delay < 1: raise ValueError("Retry delay must be >= 1 second") self._retries += 1 - return delay + return int(delay) def _prepare_and_set_next_state( self, @@ -306,9 +308,7 @@ def _prepare_and_set_next_state( ) if next_state not in (STATE_SELECTING, STATE_REQUESTING): raise ValueError("The next state must be SELECTING or REQUESTING.") - self._retries = 0 self._max_retries = max_retries - self._next_resend = self._next_retry_time_and_retry() self._retries = 0 self._dhcp_state = next_state @@ -360,7 +360,7 @@ def _handle_dhcp_message(self) -> None: :raises TimeoutError: If the FSM is in blocking mode and no valid response has been received before the timeout expires. """ - # pylint: disable=too-many-branches + # pylint: disable=too-many-branches, too-many-statements def processing_state_selecting(): """Process a message while the FSM is in SELECTING state.""" if self._dhcp_state == STATE_SELECTING and msg_type == DHCP_OFFER: @@ -416,7 +416,9 @@ def processing_state_requesting(): self._generate_dhcp_message(message_type=message_type) self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) self._eth.socket_write(self._wiz_sock, _BUFF) + self._next_resend = self._next_retry_time_and_retry() while time.monotonic() < self._next_resend: + start_time = time.monotonic() print("Waiting for response…") if self._receive_dhcp_response(): try: @@ -434,7 +436,9 @@ def processing_state_requesting(): if not self._blocking: _debugging_message("Nonblocking, exiting loop.", self._debug) return - self._next_resend = self._next_retry_time_and_retry() + print("++++ Attempt {}".format(self._retries)) + print(time.monotonic() - start_time) + # self._next_resend = self._next_retry_time_and_retry() if self._retries > self._max_retries: if self._renew: return From 8bd5bbd910809b5be4635821a9fd02d0a0faef89 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 10 Jan 2023 08:52:56 +1100 Subject: [PATCH 32/80] Refactored to use a for loop in _handle_dhcp_message and local variables for the retry timeouts. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 74 ++++++++------------- tests/test_dhcp_helper_functions.py | 6 +- 2 files changed, 32 insertions(+), 48 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 8c8e013..1d42a6d 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -156,7 +156,6 @@ def __init__( self._dhcp_state = STATE_INIT self._transaction_id = randint(1, 0x7FFFFFFF) self._start_time = 0 - self._next_resend = 0 self._retries = 0 self._max_retries = 0 self._blocking = False @@ -260,31 +259,30 @@ def _increment_transaction_id(self) -> None: _debugging_message("Incrementing transaction ID", self._debug) self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF - def _next_retry_time_and_retry(self, *, interval: int = 4) -> int: + def _next_retry_time(self, *, attempt: int, interval: int = 4) -> float: """Calculate a retry stop time. The interval is calculated as an exponential fallback with a random variation to prevent DHCP packet collisions. This timeout is intended to be compared with - time.monotonic(). Uses self._retries as the exponent, and increments this value - each time it is called. + time.monotonic(). + :param int attempt: The current attempt, used as the exponent for calculating the + timeout. :param int interval: The base retry interval in seconds. Defaults to 4 as per the - DHCP standard for Ethernet connections. + DHCP standard for Ethernet connections. Minimum value 2, defaults to 4. - :returns int: The timeout in time.monotonic() seconds. + :returns float: The timeout in time.monotonic() seconds. - :raises ValueError: If the calculated interval is < 1 second. + :raises ValueError: If the interval is not > 1 second as this could return a zero or + negative delay. """ _debugging_message( "Calculating next retry time and incrementing retries.", self._debug ) - delay = 2**self._retries * interval + randint(-1, 1) - print("delay = {}".format(delay)) - delay += time.monotonic() - if delay < 1: - raise ValueError("Retry delay must be >= 1 second") - self._retries += 1 - return int(delay) + if interval <= 1: + raise ValueError("Retry interval must be > 1 second.") + delay = 2**attempt * interval + randint(-1, 1) + time.monotonic() + return delay def _prepare_and_set_next_state( self, @@ -312,7 +310,7 @@ def _prepare_and_set_next_state( self._retries = 0 self._dhcp_state = next_state - def _receive_dhcp_response(self) -> int: + def _receive_dhcp_response(self, timeout) -> int: """ Receive data from the socket in response to a DHCP query. @@ -329,9 +327,7 @@ def _receive_dhcp_response(self) -> int: minimum_packet_length = 236 buffer = bytearray(b"") bytes_read = 0 - while ( - bytes_read <= minimum_packet_length and time.monotonic() < self._next_resend - ): + while bytes_read <= minimum_packet_length and time.monotonic() < timeout: buffer.extend( self._eth.socket_read(self._wiz_sock, BUFF_LENGTH - bytes_read)[1] ) @@ -412,15 +408,13 @@ def processing_state_requesting(): raise ValueError( "FSM should only send messages in SELECTING or REQUESTING states." ) - while True: - self._generate_dhcp_message(message_type=message_type) + for attempt in range(4): # Initial attempt plus 3 retries. + message_length = self._generate_dhcp_message(message_type=message_type) self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) - self._eth.socket_write(self._wiz_sock, _BUFF) - self._next_resend = self._next_retry_time_and_retry() - while time.monotonic() < self._next_resend: - start_time = time.monotonic() - print("Waiting for response…") - if self._receive_dhcp_response(): + self._eth.socket_write(self._wiz_sock, _BUFF[:message_length]) + next_resend = self._next_retry_time(attempt=attempt) + while time.monotonic() < next_resend: + if self._receive_dhcp_response(next_resend): try: msg_type = self._parse_dhcp_response() _debugging_message( @@ -436,22 +430,11 @@ def processing_state_requesting(): if not self._blocking: _debugging_message("Nonblocking, exiting loop.", self._debug) return - print("++++ Attempt {}".format(self._retries)) - print(time.monotonic() - start_time) - # self._next_resend = self._next_retry_time_and_retry() - if self._retries > self._max_retries: - if self._renew: - return - raise TimeoutError( - "No response from DHCP server after {} retries.".format( - self._max_retries - ) - ) - if not self._blocking: - _debugging_message( - "Nonblocking, returning from message function.", self._debug - ) - return + if self._renew: + return + raise TimeoutError( + "No response from DHCP server after {} retries.".format(self._max_retries) + ) def _dhcp_state_machine(self, *, blocking: bool = False) -> None: """ @@ -534,7 +517,7 @@ def _generate_dhcp_message( message_type: int, broadcast: bool = False, renew: bool = False, - ) -> None: + ) -> int: """ Assemble a DHCP message. The content will vary depending on which type of message is being sent and whether the lease is new or being renewed. @@ -543,6 +526,8 @@ def _generate_dhcp_message( :param bool broadcast: Used to set the flag requiring a broadcast reply from the DHCP server. Defaults to False which matches the DHCP standard. :param bool renew: Set True for renewing and rebinding operations, defaults to False. + + :returns int: The length of the DHCP message. """ def option_writer( @@ -590,12 +575,10 @@ def option_writer( pointer = 240 # Option - DHCP Message Type - print("Option - DHCP Message Type") pointer = option_writer( offset=pointer, option_code=53, option_data=(message_type,) ) # Option - Host Name - print("Option - Host Name") pointer = option_writer( offset=pointer, option_code=12, option_data=self._hostname ) @@ -613,6 +596,7 @@ def option_writer( offset=pointer, option_code=54, option_data=self.dhcp_server_ip ) _BUFF[pointer] = 0xFF + return pointer + 1 def _parse_dhcp_response( self, diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 631a4ed..e2b9c93 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -431,7 +431,7 @@ def test_next_retry_time_default_attrs(self, mocker, mock_wiznet5k, rand_int): now = time.monotonic() for retry in range(3): dhcp_client._retries = retry - assert dhcp_client._next_retry_time_and_retry() == int( + assert dhcp_client._next_retry_time() == int( 2**retry * 4 + rand_int + now ) assert dhcp_client._retries == retry + 1 @@ -448,7 +448,7 @@ def test_next_retry_time_optional_attrs(self, mocker, mock_wiznet5k, interval): now = time.monotonic() for retry in range(3): dhcp_client._retries = retry - assert dhcp_client._next_retry_time_and_retry(interval=interval) == int( + assert dhcp_client._next_retry_time(interval=interval) == int( 2**retry * interval + now ) @@ -521,7 +521,7 @@ def test_send_message_set_next_state_good_data( message_type=msg_type ) dhcp_client._wiz_sock.send.assert_called_once_with(message) - dhcp_client._next_retry_time_and_retry.assert_called_once() + dhcp_client._next_retry_time.assert_called_once() @pytest.mark.parametrize( "next_state", From 1e03545fa9db4529a9816a4738ecbc0bd06b0675 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 10 Jan 2023 13:52:41 +1100 Subject: [PATCH 33/80] Refactored to remove _prepare_and_set_next_state --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 60 ++------- tests/test_dhcp_helper_functions.py | 135 +++++++++----------- 2 files changed, 71 insertions(+), 124 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 1d42a6d..a664d11 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -156,8 +156,6 @@ def __init__( self._dhcp_state = STATE_INIT self._transaction_id = randint(1, 0x7FFFFFFF) self._start_time = 0 - self._retries = 0 - self._max_retries = 0 self._blocking = False self._renew = False @@ -210,7 +208,6 @@ def _dsm_reset(self) -> None: self.subnet_mask = UNASSIGNED_IP_ADDR self.dns_server_ip = UNASSIGNED_IP_ADDR self._renew = False - self._retries = 0 self._increment_transaction_id() self._start_time = int(time.monotonic()) @@ -221,7 +218,7 @@ def _socket_release(self) -> None: self._eth.socket_close(self._wiz_sock) self._wiz_sock = None - def _dhcp_connection_setup(self, timeout: int = 5) -> None: + def _dhcp_connection_setup(self, timeout: float = 5.0) -> None: """Initialise a UDP socket. Attempt to initialise a UDP socket. If the finite state machine (FSM) is in @@ -246,12 +243,13 @@ def _dhcp_connection_setup(self, timeout: int = 5) -> None: self._eth.write_sock_port(self._wiz_sock, 68) # Set DHCP client port. self._eth.write_sncr(self._wiz_sock, 0x01) # Open the socket. while ( - self._eth.read_sncr(self._wiz_sock) == 0 + self._eth.read_sncr(self._wiz_sock) != 0 ): # Wait for command to complete. time.sleep(0.001) if self._eth.read_snsr(self._wiz_sock) == bytes([0x22]): - self._eth.write_sndport(self._wiz_sock, DHCP_SERVER_PORT) + self._eth.write_sndport(2, DHCP_SERVER_PORT) return + self._wiz_sock = None raise RuntimeError("Unable to initialize UDP socket.") def _increment_transaction_id(self) -> None: @@ -284,32 +282,6 @@ def _next_retry_time(self, *, attempt: int, interval: int = 4) -> float: delay = 2**attempt * interval + randint(-1, 1) + time.monotonic() return delay - def _prepare_and_set_next_state( - self, - *, - next_state: int, - max_retries: int, - ) -> None: - """Generate a DHCP message and update the finite state machine state (FSM). - - Creates and sends a DHCP message, resets retry parameters and updates the - FSM state. - - :param int next_state: The state that the FSM will be set to. - :param int max_retries: Maximum attempts to resend the DHCP message. - - :raises ValueError: If the next FSM state does not handle DHCP messages. - """ - _debugging_message( - "Setting next FSM state to {}.".format(next_state), - self._debug, - ) - if next_state not in (STATE_SELECTING, STATE_REQUESTING): - raise ValueError("The next state must be SELECTING or REQUESTING.") - self._max_retries = max_retries - self._retries = 0 - self._dhcp_state = next_state - def _receive_dhcp_response(self, timeout) -> int: """ Receive data from the socket in response to a DHCP query. @@ -363,10 +335,7 @@ def processing_state_selecting(): _debugging_message( "FSM state is SELECTING with valid OFFER.", self._debug ) - self._prepare_and_set_next_state( - next_state=STATE_REQUESTING, - max_retries=3, - ) + self._dhcp_state = STATE_REQUESTING return True return False @@ -433,7 +402,7 @@ def processing_state_requesting(): if self._renew: return raise TimeoutError( - "No response from DHCP server after {} retries.".format(self._max_retries) + "No response from DHCP server after {} retries.".format(attempt) ) def _dhcp_state_machine(self, *, blocking: bool = False) -> None: @@ -474,34 +443,23 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._renew = True self._dhcp_connection_setup() self._start_time = time.monotonic() - self._prepare_and_set_next_state( - next_state=STATE_REQUESTING, - max_retries=3, - ) + self._dhcp_state = STATE_REQUESTING if self._dhcp_state == STATE_REBINDING: self._renew = True self.dhcp_server_ip = BROADCAST_SERVER_ADDR self._dhcp_connection_setup() self._start_time = time.monotonic() - self._prepare_and_set_next_state( - next_state=STATE_REQUESTING, - max_retries=3, - ) + self._dhcp_state = STATE_REQUESTING if self._dhcp_state == STATE_INIT: self._dsm_reset() - self._prepare_and_set_next_state( - next_state=STATE_SELECTING, - max_retries=3, - ) + self._dhcp_state = STATE_SELECTING if self._dhcp_state == STATE_SELECTING: - self._max_retries = 3 self._handle_dhcp_message() if self._dhcp_state == STATE_REQUESTING: - self._max_retries = 3 self._handle_dhcp_message() if self._renew: diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index e2b9c93..ec0ea57 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -63,8 +63,8 @@ def test_constants(self): assert wiz_dhcp.DHCP_SERVER_PORT == const(67) # DHCP Lease Time, in seconds assert wiz_dhcp.DEFAULT_LEASE_TIME == const(900) - assert wiz_dhcp.BROADCAST_SERVER_ADDR == b"/xff/xff/xff/xff" - assert wiz_dhcp.UNASSIGNED_IP_ADDR == b"/x00/x00/x00/x00" + assert wiz_dhcp.BROADCAST_SERVER_ADDR == b"\xff\xff\xff\xff" + assert wiz_dhcp.UNASSIGNED_IP_ADDR == b"\x00\x00\x00\x00" # DHCP Response Options assert wiz_dhcp.MSG_TYPE == 53 @@ -362,38 +362,36 @@ def test_parsing_failures(self, mock_wiznet5k): dhcp_client._parse_dhcp_response() -class TestResetDsmReset: - def test_socket_reset(self, mock_wiznet5k): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - - dhcp_client._dsm_reset() - assert dhcp_client._wiz_sock is None - - dhcp_client._dsm_reset() - assert dhcp_client._wiz_sock is None - - @freeze_time("2022-11-10") - def test_reset_dsm_parameters(self, mock_wiznet5k): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp_client.dhcp_server_ip = (1, 2, 3, 4) - dhcp_client.local_ip = (2, 3, 4, 5) - dhcp_client.subnet_mask = (3, 4, 5, 6) - dhcp_client.dns_server_ip = (7, 8, 8, 10) - dhcp_client._renew = True - dhcp_client._retries = 4 - dhcp_client._transaction_id = 3 - dhcp_client._start_time = None - - dhcp_client._dsm_reset() - - assert dhcp_client.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR - assert dhcp_client.local_ip == wiz_dhcp.UNASSIGNED_IP_ADDR - assert dhcp_client.subnet_mask == wiz_dhcp.UNASSIGNED_IP_ADDR - assert dhcp_client.dns_server_ip == wiz_dhcp.UNASSIGNED_IP_ADDR - assert dhcp_client._renew is False - assert dhcp_client._retries == 0 - assert dhcp_client._transaction_id == 4 - assert dhcp_client._start_time == time.monotonic() +@freeze_time("2022-11-10") +def test_dsm_reset(mocker, mock_wiznet5k): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + mocker.patch.object(dhcp_client, "_dhcp_connection_setup", autospec=True) + mocker.patch.object(dhcp_client, "_socket_release", autospec=True) + dhcp_client.dhcp_server_ip = (1, 2, 3, 4) + dhcp_client.local_ip = (2, 3, 4, 5) + dhcp_client.subnet_mask = (3, 4, 5, 6) + dhcp_client.dns_server_ip = (7, 8, 8, 10) + dhcp_client._renew = True + dhcp_client._retries = 4 + dhcp_client._transaction_id = 3 + dhcp_client._start_time = None + + dhcp_client._dsm_reset() + dhcp_client._dhcp_connection_setup.assert_called_once() + dhcp_client._socket_release.assert_called_once() + assert mock_wiznet5k.ifconfig == ( + wiz_dhcp.UNASSIGNED_IP_ADDR, + wiz_dhcp.UNASSIGNED_IP_ADDR, + wiz_dhcp.UNASSIGNED_IP_ADDR, + wiz_dhcp.UNASSIGNED_IP_ADDR, + ) + assert dhcp_client.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR + assert dhcp_client.local_ip == wiz_dhcp.UNASSIGNED_IP_ADDR + assert dhcp_client.subnet_mask == wiz_dhcp.UNASSIGNED_IP_ADDR + assert dhcp_client.dns_server_ip == wiz_dhcp.UNASSIGNED_IP_ADDR + assert dhcp_client._renew is False + assert dhcp_client._transaction_id == 4 + assert dhcp_client._start_time == time.monotonic() class TestSocketRelease: @@ -430,11 +428,9 @@ def test_next_retry_time_default_attrs(self, mocker, mock_wiznet5k, rand_int): ) now = time.monotonic() for retry in range(3): - dhcp_client._retries = retry - assert dhcp_client._next_retry_time() == int( + assert dhcp_client._next_retry_time(attempt=retry) == int( 2**retry * 4 + rand_int + now ) - assert dhcp_client._retries == retry + 1 @freeze_time("2022-10-10") @pytest.mark.parametrize("interval", (2, 7, 10)) @@ -447,51 +443,44 @@ def test_next_retry_time_optional_attrs(self, mocker, mock_wiznet5k, interval): ) now = time.monotonic() for retry in range(3): - dhcp_client._retries = retry - assert dhcp_client._next_retry_time(interval=interval) == int( - 2**retry * interval + now - ) - - def test_setup_socket_with_no_error(self, mock_wiznet5k): - mock_wiznet5k.get_socket.return_value = 2 + assert dhcp_client._next_retry_time( + attempt=retry, interval=interval + ) == int(2**retry * interval + now) + + @freeze_time("2022-7-6") + def test_setup_socket_with_no_error(self, mocker, mock_wiznet5k): + mocker.patch.object(mock_wiznet5k, "get_socket", return_value=2) + mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=0) + mocker.patch.object(mock_wiznet5k, "read_snsr", return_value=b"\x22") dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._dhcp_connection_setup() mock_wiznet5k.get_socket.assert_called_once() - # mock_wiznet5k.write - dhcp_client._wiz_sock.bind.assert_called_once_with((None, 68)) - dhcp_client._wiz_sock.connect.assert_called_once_with( - (wiz_dhcp.BROADCAST_SERVER_ADDR, 67) + mock_wiznet5k.write_snmr.assert_called_once_with(2, 0x02) + mock_wiznet5k.write_sock_port(2, 68) + mock_wiznet5k.write_sncr(2, 0x01) + mock_wiznet5k.read_sncr.assert_called_with(2) + mock_wiznet5k.write_sndport.assert_called_once_with( + 2, wiz_dhcp.DHCP_SERVER_PORT ) + assert dhcp_client._wiz_sock == 2 - def test_setup_socket_with_error_then_ok_blocking( - self, mocker, mock_wiznet5k, mock_socket - ): + @freeze_time("2022-7-6", auto_tick_seconds=2) + def test_setup_socket_with_timeout_on_get_socket(self, mocker, mock_wiznet5k): + mocker.patch.object(mock_wiznet5k, "get_socket", return_value=0xFF) + mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=0) + mocker.patch.object(mock_wiznet5k, "read_snsr", return_value=b"\x22") dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp_client._blocking = True - mock_socket.socket.side_effect = [RuntimeError, mocker.Mock()] - assert dhcp_client._dhcp_connection_setup() is None - # Function should ignore the error, then set the socket and call settimeout - dhcp_client._wiz_sock.settimeout.assert_called_once_with( - dhcp_client._response_timeout - ) - - def test_setup_socket_with_error_then_ok_nonblocking( - self, mocker, mock_wiznet5k, mock_socket - ): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp_client._blocking = False - mock_socket.socket.side_effect = [RuntimeError, mocker.Mock()] - assert dhcp_client._dhcp_connection_setup() is None - # Function should ignore the error, then return, _sock is None + with pytest.raises(RuntimeError): + dhcp_client._dhcp_connection_setup() assert dhcp_client._wiz_sock is None - @freeze_time("2022-10-02", auto_tick_seconds=2) - def test_setup_socket_with_error_then_timeout(self, mock_wiznet5k, mock_socket): + @freeze_time("2022-7-6", auto_tick_seconds=2) + def test_setup_socket_with_timeout_on_socket_is_udp(self, mocker, mock_wiznet5k): + mocker.patch.object(mock_wiznet5k, "get_socket", return_value=2) + mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=0) + mocker.patch.object(mock_wiznet5k, "read_snsr", return_value=b"\x21") dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp_client._blocking = True - mock_socket.socket.side_effect = [RuntimeError, RuntimeError] - # Function should timeout, raise an exception and not set a socket - with pytest.raises(TimeoutError): + with pytest.raises(RuntimeError): dhcp_client._dhcp_connection_setup() assert dhcp_client._wiz_sock is None From eb536c0bd41b52cfc2bc80b1a43d6a52780324c8 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 10 Jan 2023 14:18:45 +1100 Subject: [PATCH 34/80] Refactored to processing message states into a single function - _process_messaging_states --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 82 ++++++++++----------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index a664d11..09ab9a7 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -316,6 +316,44 @@ def _receive_dhcp_response(self, timeout) -> int: gc.collect() return bytes_read + def _process_messaging_states(self, *, message_type: int): + """ + Process a message while the FSM is in SELECTING or REQUESTING state. + + Check the message and update the FSM state if it is a valid type. + + :param int message_type: The type of message received from the DHCP server. + + :returns bool: True if the message was valid for the current state + """ + if self._dhcp_state == STATE_SELECTING and message_type == DHCP_OFFER: + _debugging_message("FSM state is SELECTING with valid OFFER.", self._debug) + self._dhcp_state = STATE_REQUESTING + return True + if self._dhcp_state == STATE_REQUESTING: + _debugging_message("FSM state is REQUESTING.", self._debug) + if message_type == DHCP_NAK: + _debugging_message( + "Message is NAK, setting FSM state to INIT.", self._debug + ) + self._dhcp_state = STATE_INIT + return True + if message_type == DHCP_ACK: + _debugging_message( + "Message is ACK, setting FSM state to BOUND.", self._debug + ) + if self._lease_time == 0: + self._lease_time = DEFAULT_LEASE_TIME + self._t1 = self._start_time + self._lease_time // 2 + self._t2 = self._start_time + self._lease_time - self._lease_time // 8 + self._lease_time += self._start_time + self._increment_transaction_id() + self._renew = False + self._socket_release() + self._dhcp_state = STATE_BOUND + return True + return False + def _handle_dhcp_message(self) -> None: """Receive and process DHCP message then update the finite state machine (FSM). @@ -328,46 +366,6 @@ def _handle_dhcp_message(self) -> None: :raises TimeoutError: If the FSM is in blocking mode and no valid response has been received before the timeout expires. """ - # pylint: disable=too-many-branches, too-many-statements - def processing_state_selecting(): - """Process a message while the FSM is in SELECTING state.""" - if self._dhcp_state == STATE_SELECTING and msg_type == DHCP_OFFER: - _debugging_message( - "FSM state is SELECTING with valid OFFER.", self._debug - ) - self._dhcp_state = STATE_REQUESTING - return True - return False - - def processing_state_requesting(): - """Process a message while the FSM is in REQUESTING state.""" - if self._dhcp_state == STATE_REQUESTING: - _debugging_message("FSM state is REQUESTING.", self._debug) - if msg_type == DHCP_NAK: - _debugging_message( - "Message is NAK, setting FSM state to INIT.", self._debug - ) - self._dhcp_state = STATE_INIT - return True - if msg_type == DHCP_ACK: - _debugging_message( - "Message is ACK, setting FSM state to BOUND.", self._debug - ) - if self._lease_time == 0: - self._lease_time = DEFAULT_LEASE_TIME - self._t1 = self._start_time + self._lease_time // 2 - self._t2 = ( - self._start_time + self._lease_time - self._lease_time // 8 - ) - self._lease_time += self._start_time - self._increment_transaction_id() - self._renew = False - self._socket_release() - self._dhcp_state = STATE_BOUND - return True - return False - - # Main processing loop _debugging_message("Processing SELECTING or REQUESTING state.", self._debug) if self._dhcp_state == STATE_SELECTING: message_type = DHCP_DISCOVER @@ -392,9 +390,7 @@ def processing_state_requesting(): except ValueError as error: _debugging_message(error, self._debug) else: - if processing_state_selecting(): - return - if processing_state_requesting(): + if self._process_messaging_states(message_type=message_type): return if not self._blocking: _debugging_message("Nonblocking, exiting loop.", self._debug) From 9bec64e4b3a69e015d230b3b3b177448119139c7 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 10 Jan 2023 18:10:11 +1100 Subject: [PATCH 35/80] Wrote tests for _handle_dhcp_message --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 35 +-- tests/test_dhcp_helper_functions.py | 222 +++++++++++++++++--- 2 files changed, 207 insertions(+), 50 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 09ab9a7..89ad7c0 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -282,7 +282,7 @@ def _next_retry_time(self, *, attempt: int, interval: int = 4) -> float: delay = 2**attempt * interval + randint(-1, 1) + time.monotonic() return delay - def _receive_dhcp_response(self, timeout) -> int: + def _receive_dhcp_response(self, timeout: float) -> int: """ Receive data from the socket in response to a DHCP query. @@ -324,7 +324,7 @@ def _process_messaging_states(self, *, message_type: int): :param int message_type: The type of message received from the DHCP server. - :returns bool: True if the message was valid for the current state + :returns bool: True if the message was valid for the current state. """ if self._dhcp_state == STATE_SELECTING and message_type == DHCP_OFFER: _debugging_message("FSM state is SELECTING with valid OFFER.", self._debug) @@ -355,11 +355,12 @@ def _process_messaging_states(self, *, message_type: int): return False def _handle_dhcp_message(self) -> None: - """Receive and process DHCP message then update the finite state machine (FSM). + """Send, receive and process DHCP message. Update the finite state machine (FSM). - Wait for a response from the DHCP server, resending on an exponential fallback - schedule if no response is received. Process the response, sending messages, - setting attributes, and setting next FSM state according to the current state. + Send a message and wait for a response from the DHCP server, resending on an + exponential fallback schedule if no response is received. Process the response, + sending messages, setting attributes, and setting next FSM state according to the + current state. Only called when the FSM is in SELECTING or REQUESTING states. @@ -368,35 +369,35 @@ def _handle_dhcp_message(self) -> None: """ _debugging_message("Processing SELECTING or REQUESTING state.", self._debug) if self._dhcp_state == STATE_SELECTING: - message_type = DHCP_DISCOVER + msg_type_out = DHCP_DISCOVER elif self._dhcp_state == STATE_REQUESTING: - message_type = DHCP_REQUEST + msg_type_out = DHCP_REQUEST else: raise ValueError( - "FSM should only send messages in SELECTING or REQUESTING states." + "FSM should only send messages while in SELECTING or REQUESTING states." ) for attempt in range(4): # Initial attempt plus 3 retries. - message_length = self._generate_dhcp_message(message_type=message_type) + message_length = self._generate_dhcp_message(message_type=msg_type_out) self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) self._eth.socket_write(self._wiz_sock, _BUFF[:message_length]) next_resend = self._next_retry_time(attempt=attempt) while time.monotonic() < next_resend: if self._receive_dhcp_response(next_resend): try: - msg_type = self._parse_dhcp_response() + msg_type_in = self._parse_dhcp_response() _debugging_message( - "Received message type {}".format(msg_type), self._debug + "Received message type {}".format(msg_type_in), self._debug ) except ValueError as error: _debugging_message(error, self._debug) else: - if self._process_messaging_states(message_type=message_type): + if self._process_messaging_states(message_type=msg_type_in): return - if not self._blocking: - _debugging_message("Nonblocking, exiting loop.", self._debug) + if not self._blocking or self._renew: + _debugging_message( + "Nonblocking or renewing, exiting loop.", self._debug + ) return - if self._renew: - return raise TimeoutError( "No response from DHCP server after {} retries.".format(attempt) ) diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index ec0ea57..7ed4766 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -484,50 +484,206 @@ def test_setup_socket_with_timeout_on_socket_is_udp(self, mocker, mock_wiznet5k) dhcp_client._dhcp_connection_setup() assert dhcp_client._wiz_sock is None + +class TestHandleDhcpMessage: @pytest.mark.parametrize( - "next_state, msg_type, retries", + "fsm_state, msg_in", ( - (wiz_dhcp.STATE_SELECTING, wiz_dhcp.DHCP_DISCOVER, 2), - (wiz_dhcp.STATE_REQUESTING, wiz_dhcp.DHCP_REQUEST, 3), + (wiz_dhcp.STATE_SELECTING, wiz_dhcp.DHCP_DISCOVER), + (wiz_dhcp.STATE_REQUESTING, wiz_dhcp.DHCP_REQUEST), ), ) - def test_send_message_set_next_state_good_data( - self, mocker, mock_wiznet5k, next_state, msg_type, retries + @freeze_time("2022-5-5") + def test_good_data(self, mocker, mock_wiznet5k, fsm_state, msg_in): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + # Mock out methods to allow _handle_dhcp_message to run. + mocker.patch.object( + dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 + ) + mocker.patch.object(dhcp_client, "_process_messaging_states", autospec=True) + mocker.patch.object( + dhcp_client, "_receive_dhcp_response", autospec=True, return_value=300 + ) + mocker.patch.object( + dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x00 + ) + mocker.patch.object( + dhcp_client, + "_next_retry_time", + autospec=True, + return_value=time.monotonic() + 5, + ) + # Set initial FSM state. + dhcp_client._wiz_sock = 3 + dhcp_client._dhcp_state = fsm_state + dhcp_client._blocking = True + dhcp_client._renew = False + # Test. + dhcp_client._handle_dhcp_message() + # Confirm that the msg_type sent matches the FSM state. + dhcp_client._generate_dhcp_message.assert_called_once_with(message_type=msg_in) + dhcp_client._eth.write_sndipr.assert_called_once_with( + 3, dhcp_client.dhcp_server_ip + ) + dhcp_client._eth.socket_write.assert_called_once_with(3, wiz_dhcp._BUFF[:300]) + dhcp_client._next_retry_time.assert_called_once_with(attempt=0) + dhcp_client._receive_dhcp_response.assert_called_once_with(time.monotonic() + 5) + # If the initial message was good, receive is only called once. + dhcp_client._parse_dhcp_response.assert_called_once() + # Called once if the message was valid. + dhcp_client._process_messaging_states.assert_called_once_with(message_type=0x00) + + @freeze_time("2022-5-5", auto_tick_seconds=1) + def test_timeout_blocking_no_response(self, mocker, mock_wiznet5k): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + # Mock out methods to allow _handle_dhcp_message to run. + mocker.patch.object( + dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 + ) + mocker.patch.object(dhcp_client, "_process_messaging_states", autospec=True) + # No message bytes returned, so the handler should loop. + mocker.patch.object( + dhcp_client, "_receive_dhcp_response", autospec=True, return_value=0 + ) + mocker.patch.object( + dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x00 + ) + mocker.patch.object( + dhcp_client, + "_next_retry_time", + autospec=True, + return_value=time.monotonic() + 5, + ) + # Set initial FSM state. + dhcp_client._wiz_sock = 3 + dhcp_client._dhcp_state = wiz_dhcp.STATE_REQUESTING + dhcp_client._blocking = True + dhcp_client._renew = False + # Test that a TimeoutError is raised. + with pytest.raises(TimeoutError): + dhcp_client._handle_dhcp_message() + # Confirm that _receive_dhcp_response is called repeatedly. + assert dhcp_client._receive_dhcp_response.call_count == 4 + # Check that message parsing and processing not called. + dhcp_client._parse_dhcp_response.assert_not_called() + dhcp_client._process_messaging_states.assert_not_called() + + @freeze_time("2022-5-5", auto_tick_seconds=1) + def test_timeout_blocking_bad_message(self, mocker, mock_wiznet5k): + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + # Mock out methods to allow _handle_dhcp_message to run. + mocker.patch.object( + dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 + ) + # Return False to model a bad message type, which should loop. + mocker.patch.object( + dhcp_client, "_process_messaging_states", autospec=True, return_value=False + ) + mocker.patch.object( + dhcp_client, "_receive_dhcp_response", autospec=True, return_value=300 + ) + mocker.patch.object( + dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x00 + ) + mocker.patch.object( + dhcp_client, + "_next_retry_time", + autospec=True, + return_value=time.monotonic() + 5, + ) + # Set initial FSM state. + dhcp_client._wiz_sock = 3 + dhcp_client._dhcp_state = wiz_dhcp.STATE_REQUESTING + dhcp_client._blocking = True + dhcp_client._renew = False + # Test that a TimeoutError is raised. + with pytest.raises(TimeoutError): + dhcp_client._handle_dhcp_message() + # Confirm that processing methods are called repeatedly. + assert dhcp_client._receive_dhcp_response.call_count == 4 + assert dhcp_client._parse_dhcp_response.call_count == 4 + assert dhcp_client._process_messaging_states.call_count == 4 + + @freeze_time("2022-5-5") + @pytest.mark.parametrize( + "renew, blocking", ((True, False), (True, True), (False, False)) + ) + def test_no_response_non_blocking_renewing( + self, mocker, mock_wiznet5k, renew, blocking ): - mocker.patch.object(wiz_dhcp.DHCP, "_generate_dhcp_message") - mocker.patch.object(wiz_dhcp.DHCP, "_next_retry_time_and_retry") - message = bytearray(b"HelloWorld") - wiz_dhcp._BUFF = bytearray(message) dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - dhcp_client._wiz_sock = mocker.Mock() - dhcp_client._prepare_and_set_next_state( - next_state=next_state, max_retries=retries + # Mock out methods to allow _handle_dhcp_message to run. + mocker.patch.object( + dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 ) - assert dhcp_client._retries == 0 - assert dhcp_client._max_retries == retries - assert dhcp_client._dhcp_state == next_state - dhcp_client._generate_dhcp_message.assert_called_once_with( - message_type=msg_type + mocker.patch.object(dhcp_client, "_process_messaging_states", autospec=True) + # No message bytes returned, so the handler do nothing and return. + mocker.patch.object( + dhcp_client, "_receive_dhcp_response", autospec=True, return_value=0 ) - dhcp_client._wiz_sock.send.assert_called_once_with(message) - dhcp_client._next_retry_time.assert_called_once() - + mocker.patch.object( + dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x00 + ) + mocker.patch.object( + dhcp_client, + "_next_retry_time", + autospec=True, + return_value=time.monotonic() + 5, + ) + # Set initial FSM state. + dhcp_client._wiz_sock = 3 + dhcp_client._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Combinations of renew and blocking that will not loop. + dhcp_client._blocking = blocking + dhcp_client._renew = renew + # Test. + dhcp_client._handle_dhcp_message() + dhcp_client._next_retry_time.assert_called_once_with(attempt=0) + dhcp_client._receive_dhcp_response.assert_called_once_with(time.monotonic() + 5) + # No bytes returned so don't call parse or process message. + dhcp_client._parse_dhcp_response.assert_not_called() + dhcp_client._process_messaging_states.assert_not_called() + + @freeze_time("2022-5-5") @pytest.mark.parametrize( - "next_state", - ( - wiz_dhcp.STATE_INIT, - wiz_dhcp.STATE_REBINDING, - wiz_dhcp.STATE_RENEWING, - wiz_dhcp.STATE_BOUND, - ), + "renew, blocking", ((True, False), (True, True), (False, False)) ) - def test_send_message_set_next_state_bad_state(self, mock_wiznet5k, next_state): - # Test with all states that should not call this function. + def test_bad_message_non_blocking_renewing( + self, mocker, mock_wiznet5k, renew, blocking + ): dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) - with pytest.raises(ValueError): - dhcp_client._prepare_and_set_next_state( - next_state=next_state, max_retries=4 - ) + # Mock out methods to allow _handle_dhcp_message to run. + mocker.patch.object( + dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 + ) + # Bad message so check that the handler does not loop. + mocker.patch.object(dhcp_client, "_process_messaging_states", autospec=False) + mocker.patch.object( + dhcp_client, "_receive_dhcp_response", autospec=True, return_value=300 + ) + mocker.patch.object( + dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x00 + ) + mocker.patch.object( + dhcp_client, + "_next_retry_time", + autospec=True, + return_value=time.monotonic() + 5, + ) + # Set initial FSM state. + dhcp_client._wiz_sock = 3 + dhcp_client._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Combinations of renew and blocking that will not loop. + dhcp_client._blocking = blocking + dhcp_client._renew = renew + # Test. + dhcp_client._handle_dhcp_message() + dhcp_client._next_retry_time.assert_called_once_with(attempt=0) + dhcp_client._receive_dhcp_response.assert_called_once_with(time.monotonic() + 5) + # Bad message returned so call parse and process message. + dhcp_client._parse_dhcp_response.assert_called_once() + # Called once if the message was valid. + dhcp_client._process_messaging_states.assert_called_once() class TestReceiveResponse: From 29aae43597a1394baee9f8b16d7e264cd73abb6e Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 10 Jan 2023 23:06:43 +1100 Subject: [PATCH 36/80] Completed tests for DHCP helper methods. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 12 ++-- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 2 +- tests/test_dhcp_helper_functions.py | 68 ++++++++++++--------- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 85b7f12..41eddb2 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -940,18 +940,16 @@ def socket_read(self, socket_num: int, length: int) -> Tuple[int, bytes]: time.sleep(0.0001) return ret, resp - def read_udp( - self, socket_num: int, length: int - ) -> Union[int, Tuple[int, Union[int, bytearray]]]: + def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: """ Read UDP socket's current message bytes. :param int socket_num: The socket to read data from. :param int length: The number of bytes to read from the socket. - :return Union[int, Tuple[int, Union[int, bytearray]]]: If the read was successful then - the first item of the tuple is the length of the data and the second is the data. - If the read was unsuccessful then -1 is returned. + :return Tuple[int, bytes]: If the read was successful then the first + item of the tuple is the length of the data and the second is the data. + If the read was unsuccessful then (0, b"") is returned. """ if self.udp_datasize[socket_num] > 0: if self.udp_datasize[socket_num] <= length: @@ -962,7 +960,7 @@ def read_udp( self.socket_read(socket_num, self.udp_datasize[socket_num] - length) self.udp_datasize[socket_num] = 0 return ret, resp - return -1 + return 0, b"" def socket_write( self, socket_num: int, buffer: bytearray, timeout: float = 0 diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 89ad7c0..8b7ce51 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -301,7 +301,7 @@ def _receive_dhcp_response(self, timeout: float) -> int: bytes_read = 0 while bytes_read <= minimum_packet_length and time.monotonic() < timeout: buffer.extend( - self._eth.socket_read(self._wiz_sock, BUFF_LENGTH - bytes_read)[1] + self._eth.read_udp(self._wiz_sock, BUFF_LENGTH - bytes_read)[1] ) bytes_read = len(buffer) if bytes_read == BUFF_LENGTH: diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 7ed4766..7f33229 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -695,33 +695,34 @@ class TestReceiveResponse: "bytes_on_socket", (wiz_dhcp.BUFF_LENGTH, minimum_packet_length + 1) ) def test_receive_response_good_data(self, mock_dhcp, bytes_on_socket): - mock_dhcp._next_resend = time.monotonic() + 15 - mock_dhcp._wiz_sock.recv.return_value = bytes([0] * bytes_on_socket) - response = mock_dhcp._receive_dhcp_response() + mock_dhcp._eth.read_udp.return_value = ( + bytes_on_socket, + bytes([0] * bytes_on_socket), + ) + response = mock_dhcp._receive_dhcp_response(time.monotonic() + 15) assert response == bytes_on_socket assert response > 236 @freeze_time("2022-10-10") - @pytest.mark.parametrize( - "bytes_on_socket", ([bytes([0] * minimum_packet_length), bytes([0] * 1)],) - ) - def test_receive_response_short_packet(self, mock_dhcp, bytes_on_socket): - mock_dhcp._next_resend = time.monotonic() + 15 - mock_dhcp._wiz_sock.recv.side_effect = bytes_on_socket - assert mock_dhcp._receive_dhcp_response() > 236 + def test_receive_response_short_packet(self, mock_dhcp): + mock_dhcp._eth.read_udp.side_effect = [ + (236, bytes([0] * 236)), + (1, bytes([0] * 1)), + ] + assert mock_dhcp._receive_dhcp_response(time.monotonic() + 15) > 236 @freeze_time("2022-10-10", auto_tick_seconds=5) def test_timeout(self, mock_dhcp): mock_dhcp._next_resend = time.monotonic() + 15 - mock_dhcp._wiz_sock.recv.side_effect = [ - b"", - b"", - b"", - b"", - b"", + mock_dhcp._eth.read_udp.side_effect = [ + (0, b""), + (0, b""), + (0, b""), + (0, b""), + (0, b""), bytes([0] * 240), ] - assert mock_dhcp._receive_dhcp_response() == 0 + assert mock_dhcp._receive_dhcp_response(time.monotonic() + 15) == 0 @freeze_time("2022-10-10") @pytest.mark.parametrize("bytes_returned", ([240], [230, 30])) @@ -732,21 +733,30 @@ def test_buffer_handling(self, mock_dhcp, bytes_returned): expected_result = bytearray([2] * total_bytes) + ( bytes([0] * (wiz_dhcp.BUFF_LENGTH - total_bytes)) ) - mock_dhcp._wiz_sock.recv.side_effect = (bytes([2] * x) for x in bytes_returned) - assert mock_dhcp._receive_dhcp_response() == total_bytes + mock_dhcp._eth.read_udp.side_effect = ( + (x, bytes([2] * x)) for x in bytes_returned + ) + assert mock_dhcp._receive_dhcp_response(time.monotonic() + 15) == total_bytes assert wiz_dhcp._BUFF == expected_result @freeze_time("2022-10-10") def test_buffer_does_not_overrun(self, mocker, mock_dhcp): + mock_dhcp._wiz_sock = 1 mock_dhcp._next_resend = time.monotonic() + 15 - mock_dhcp._wiz_sock.recv.return_value = bytes([2] * wiz_dhcp.BUFF_LENGTH) - mock_dhcp._receive_dhcp_response() - mock_dhcp._wiz_sock.recv.assert_called_once_with(wiz_dhcp.BUFF_LENGTH) - mock_dhcp._wiz_sock.recv.reset_mock() - mock_dhcp._wiz_sock.recv.side_effect = [bytes([2] * 200), bytes([2] * 118)] - mock_dhcp._receive_dhcp_response() - assert mock_dhcp._wiz_sock.recv.call_count == 2 - assert mock_dhcp._wiz_sock.recv.call_args_list == [ - mocker.call(318), - mocker.call(118), + mock_dhcp._eth.read_udp.return_value = ( + wiz_dhcp.BUFF_LENGTH, + bytes([2] * wiz_dhcp.BUFF_LENGTH), + ) + mock_dhcp._receive_dhcp_response(time.monotonic() + 10) + mock_dhcp._eth.read_udp.assert_called_once_with(1, wiz_dhcp.BUFF_LENGTH) + mock_dhcp._eth.read_udp.reset_mock() + mock_dhcp._eth.read_udp.side_effect = [ + (200, bytes([2] * 200)), + (118, bytes([2] * 118)), + ] + mock_dhcp._receive_dhcp_response(time.monotonic() + 10) + assert mock_dhcp._eth.read_udp.call_count == 2 + assert mock_dhcp._eth.read_udp.call_args_list == [ + mocker.call(1, 318), + mocker.call(1, 118), ] From 24f1d4f48b3489fcf438838fe54ead744ae4f8ac Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 11 Jan 2023 08:31:10 +1100 Subject: [PATCH 37/80] Refactored _handle_DHCP to return 0, a message type or raise a ValueError exception instead of changing FSM state. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 23 ++++++++++---------- tests/test_dhcp_helper_functions.py | 24 ++++++++------------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 8b7ce51..3a12ab2 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -354,16 +354,18 @@ def _process_messaging_states(self, *, message_type: int): return True return False - def _handle_dhcp_message(self) -> None: + def _handle_dhcp_message(self) -> int: """Send, receive and process DHCP message. Update the finite state machine (FSM). Send a message and wait for a response from the DHCP server, resending on an - exponential fallback schedule if no response is received. Process the response, - sending messages, setting attributes, and setting next FSM state according to the - current state. - + exponential fallback schedule matching the DHCP standard if no response is received. Only called when the FSM is in SELECTING or REQUESTING states. + :returns int: The DHCP message type, or 0 if no message received in non-blocking + or renewing states. + + :raises ValueError: If the function is not called from SELECTING or BLOCKING FSM + states. :raises TimeoutError: If the FSM is in blocking mode and no valid response has been received before the timeout expires. """ @@ -374,7 +376,7 @@ def _handle_dhcp_message(self) -> None: msg_type_out = DHCP_REQUEST else: raise ValueError( - "FSM should only send messages while in SELECTING or REQUESTING states." + "FSM can only send messages while in SELECTING or REQUESTING states." ) for attempt in range(4): # Initial attempt plus 3 retries. message_length = self._generate_dhcp_message(message_type=msg_type_out) @@ -388,16 +390,15 @@ def _handle_dhcp_message(self) -> None: _debugging_message( "Received message type {}".format(msg_type_in), self._debug ) + return msg_type_in except ValueError as error: _debugging_message(error, self._debug) - else: - if self._process_messaging_states(message_type=msg_type_in): - return if not self._blocking or self._renew: _debugging_message( - "Nonblocking or renewing, exiting loop.", self._debug + "No message, nonblocking or renewing, exiting loop.", + self._debug, ) - return + return 0 # Did not receive a response in a single attempt. raise TimeoutError( "No response from DHCP server after {} retries.".format(attempt) ) diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 7f33229..a869422 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -504,8 +504,9 @@ def test_good_data(self, mocker, mock_wiznet5k, fsm_state, msg_in): mocker.patch.object( dhcp_client, "_receive_dhcp_response", autospec=True, return_value=300 ) + # Non zero value is a good message for _handle_dhcp_message. mocker.patch.object( - dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x00 + dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x01 ) mocker.patch.object( dhcp_client, @@ -519,7 +520,7 @@ def test_good_data(self, mocker, mock_wiznet5k, fsm_state, msg_in): dhcp_client._blocking = True dhcp_client._renew = False # Test. - dhcp_client._handle_dhcp_message() + assert dhcp_client._handle_dhcp_message() == 1 # Confirm that the msg_type sent matches the FSM state. dhcp_client._generate_dhcp_message.assert_called_once_with(message_type=msg_in) dhcp_client._eth.write_sndipr.assert_called_once_with( @@ -530,8 +531,6 @@ def test_good_data(self, mocker, mock_wiznet5k, fsm_state, msg_in): dhcp_client._receive_dhcp_response.assert_called_once_with(time.monotonic() + 5) # If the initial message was good, receive is only called once. dhcp_client._parse_dhcp_response.assert_called_once() - # Called once if the message was valid. - dhcp_client._process_messaging_states.assert_called_once_with(message_type=0x00) @freeze_time("2022-5-5", auto_tick_seconds=1) def test_timeout_blocking_no_response(self, mocker, mock_wiznet5k): @@ -546,7 +545,7 @@ def test_timeout_blocking_no_response(self, mocker, mock_wiznet5k): dhcp_client, "_receive_dhcp_response", autospec=True, return_value=0 ) mocker.patch.object( - dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x00 + dhcp_client, "_parse_dhcp_response", autospec=True, side_effect=[ValueError] ) mocker.patch.object( dhcp_client, @@ -564,9 +563,8 @@ def test_timeout_blocking_no_response(self, mocker, mock_wiznet5k): dhcp_client._handle_dhcp_message() # Confirm that _receive_dhcp_response is called repeatedly. assert dhcp_client._receive_dhcp_response.call_count == 4 - # Check that message parsing and processing not called. + # Check that message parsing not called. dhcp_client._parse_dhcp_response.assert_not_called() - dhcp_client._process_messaging_states.assert_not_called() @freeze_time("2022-5-5", auto_tick_seconds=1) def test_timeout_blocking_bad_message(self, mocker, mock_wiznet5k): @@ -583,7 +581,7 @@ def test_timeout_blocking_bad_message(self, mocker, mock_wiznet5k): dhcp_client, "_receive_dhcp_response", autospec=True, return_value=300 ) mocker.patch.object( - dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x00 + dhcp_client, "_parse_dhcp_response", autospec=True, side_effect=ValueError ) mocker.patch.object( dhcp_client, @@ -602,7 +600,6 @@ def test_timeout_blocking_bad_message(self, mocker, mock_wiznet5k): # Confirm that processing methods are called repeatedly. assert dhcp_client._receive_dhcp_response.call_count == 4 assert dhcp_client._parse_dhcp_response.call_count == 4 - assert dhcp_client._process_messaging_states.call_count == 4 @freeze_time("2022-5-5") @pytest.mark.parametrize( @@ -637,12 +634,11 @@ def test_no_response_non_blocking_renewing( dhcp_client._blocking = blocking dhcp_client._renew = renew # Test. - dhcp_client._handle_dhcp_message() + assert dhcp_client._handle_dhcp_message() == 0 dhcp_client._next_retry_time.assert_called_once_with(attempt=0) dhcp_client._receive_dhcp_response.assert_called_once_with(time.monotonic() + 5) # No bytes returned so don't call parse or process message. dhcp_client._parse_dhcp_response.assert_not_called() - dhcp_client._process_messaging_states.assert_not_called() @freeze_time("2022-5-5") @pytest.mark.parametrize( @@ -662,7 +658,7 @@ def test_bad_message_non_blocking_renewing( dhcp_client, "_receive_dhcp_response", autospec=True, return_value=300 ) mocker.patch.object( - dhcp_client, "_parse_dhcp_response", autospec=True, return_value=0x00 + dhcp_client, "_parse_dhcp_response", autospec=True, side_effect=ValueError ) mocker.patch.object( dhcp_client, @@ -677,13 +673,11 @@ def test_bad_message_non_blocking_renewing( dhcp_client._blocking = blocking dhcp_client._renew = renew # Test. - dhcp_client._handle_dhcp_message() + assert dhcp_client._handle_dhcp_message() == 0 dhcp_client._next_retry_time.assert_called_once_with(attempt=0) dhcp_client._receive_dhcp_response.assert_called_once_with(time.monotonic() + 5) # Bad message returned so call parse and process message. dhcp_client._parse_dhcp_response.assert_called_once() - # Called once if the message was valid. - dhcp_client._process_messaging_states.assert_called_once() class TestReceiveResponse: From dd1548e3c1f9df4e3848cd27e95f04d92812b8c9 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 11 Jan 2023 15:54:54 +1100 Subject: [PATCH 38/80] Added tests for _process_messaging_states. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 107 ++++++++++---------- tests/test_dhcp_helper_functions.py | 82 +++++++++++++++ 2 files changed, 135 insertions(+), 54 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 3a12ab2..b899d8b 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -155,7 +155,7 @@ def __init__( # DHCP state machine self._dhcp_state = STATE_INIT self._transaction_id = randint(1, 0x7FFFFFFF) - self._start_time = 0 + self._start_time = 0.0 self._blocking = False self._renew = False @@ -209,7 +209,7 @@ def _dsm_reset(self) -> None: self.dns_server_ip = UNASSIGNED_IP_ADDR self._renew = False self._increment_transaction_id() - self._start_time = int(time.monotonic()) + self._start_time = time.monotonic() def _socket_release(self) -> None: """Close the socket if it exists.""" @@ -329,16 +329,14 @@ def _process_messaging_states(self, *, message_type: int): if self._dhcp_state == STATE_SELECTING and message_type == DHCP_OFFER: _debugging_message("FSM state is SELECTING with valid OFFER.", self._debug) self._dhcp_state = STATE_REQUESTING - return True - if self._dhcp_state == STATE_REQUESTING: + elif self._dhcp_state == STATE_REQUESTING: _debugging_message("FSM state is REQUESTING.", self._debug) if message_type == DHCP_NAK: _debugging_message( "Message is NAK, setting FSM state to INIT.", self._debug ) self._dhcp_state = STATE_INIT - return True - if message_type == DHCP_ACK: + elif message_type == DHCP_ACK: _debugging_message( "Message is ACK, setting FSM state to BOUND.", self._debug ) @@ -349,10 +347,7 @@ def _process_messaging_states(self, *, message_type: int): self._lease_time += self._start_time self._increment_transaction_id() self._renew = False - self._socket_release() self._dhcp_state = STATE_BOUND - return True - return False def _handle_dhcp_message(self) -> int: """Send, receive and process DHCP message. Update the finite state machine (FSM). @@ -415,57 +410,61 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: "FSM initial state is {}".format(self._dhcp_state), self._debug ) self._blocking = blocking - if self._dhcp_state == STATE_BOUND: - now = time.monotonic() - if now < self._t1: - _debugging_message("No timers have expired. Exiting FSM.", self._debug) - return - if now > self._lease_time: - _debugging_message( - "Lease has expired, switching state to INIT.", self._debug - ) - self._blocking = True - self._dhcp_state = STATE_INIT - elif now > self._t2: - _debugging_message( - "T2 has expired, switching state to REBINDING.", self._debug - ) - self._dhcp_state = STATE_REBINDING - else: - _debugging_message( - "T1 has expired, switching state to RENEWING.", self._debug - ) - self._dhcp_state = STATE_RENEWING + while True: + if self._dhcp_state == STATE_BOUND: + now = time.monotonic() + if now < self._t1: + _debugging_message( + "No timers have expired. Exiting FSM.", self._debug + ) + self._socket_release() + return + if now > self._lease_time: + _debugging_message( + "Lease has expired, switching state to INIT.", self._debug + ) + self._blocking = True + self._dhcp_state = STATE_INIT + elif now > self._t2: + _debugging_message( + "T2 has expired, switching state to REBINDING.", self._debug + ) + self._dhcp_state = STATE_REBINDING + else: + _debugging_message( + "T1 has expired, switching state to RENEWING.", self._debug + ) + self._dhcp_state = STATE_RENEWING - if self._dhcp_state == STATE_RENEWING: - self._renew = True - self._dhcp_connection_setup() - self._start_time = time.monotonic() - self._dhcp_state = STATE_REQUESTING + if self._dhcp_state == STATE_RENEWING: + self._renew = True + self._dhcp_connection_setup() + self._start_time = time.monotonic() + self._dhcp_state = STATE_REQUESTING - if self._dhcp_state == STATE_REBINDING: - self._renew = True - self.dhcp_server_ip = BROADCAST_SERVER_ADDR - self._dhcp_connection_setup() - self._start_time = time.monotonic() - self._dhcp_state = STATE_REQUESTING + if self._dhcp_state == STATE_REBINDING: + self._renew = True + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + self._dhcp_connection_setup() + self._start_time = time.monotonic() + self._dhcp_state = STATE_REQUESTING - if self._dhcp_state == STATE_INIT: - self._dsm_reset() - self._dhcp_state = STATE_SELECTING + if self._dhcp_state == STATE_INIT: + self._dsm_reset() + self._dhcp_state = STATE_SELECTING - if self._dhcp_state == STATE_SELECTING: - self._handle_dhcp_message() + if self._dhcp_state == STATE_SELECTING: + self._process_messaging_states(message_type=self._handle_dhcp_message()) - if self._dhcp_state == STATE_REQUESTING: - self._handle_dhcp_message() + if self._dhcp_state == STATE_REQUESTING: + self._process_messaging_states(message_type=self._handle_dhcp_message()) - if self._renew: - _debugging_message( - "Lease has not expired, setting state to BOUND and exiting FSM.", - self._debug, - ) - self._dhcp_state = STATE_BOUND + if self._renew: + _debugging_message( + "Lease has not expired, resetting state to BOUND and exiting FSM.", + self._debug, + ) + self._dhcp_state = STATE_BOUND def _generate_dhcp_message( self, diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index a869422..4a133e4 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -754,3 +754,85 @@ def test_buffer_does_not_overrun(self, mocker, mock_dhcp): mocker.call(1, 318), mocker.call(1, 118), ] + + +class TestProcessMessagingStates: + @pytest.mark.parametrize( + "state, bad_messages", + ( + ( + wiz_dhcp.STATE_SELECTING, + ( + 0, + wiz_dhcp.DHCP_ACK, + wiz_dhcp.DHCP_REQUEST, + wiz_dhcp.DHCP_DECLINE, + wiz_dhcp.DHCP_DISCOVER, + wiz_dhcp.DHCP_NAK, + wiz_dhcp.DHCP_INFORM, + wiz_dhcp.DHCP_RELEASE, + ), + ), + ( + wiz_dhcp.STATE_REQUESTING, + ( + 0, + wiz_dhcp.DHCP_OFFER, + wiz_dhcp.DHCP_REQUEST, + wiz_dhcp.DHCP_DECLINE, + wiz_dhcp.DHCP_DISCOVER, + wiz_dhcp.DHCP_INFORM, + wiz_dhcp.DHCP_RELEASE, + ), + ), + ), + ) + def test_called_with_bad_or_no_message(self, mock_dhcp, state, bad_messages): + # Setup with the current state. + mock_dhcp._dhcp_state = state + # Test against 0 (no message) and all bad message types. + for message_type in bad_messages: + # Test. + mock_dhcp._process_messaging_states(message_type=message_type) + # Confirm that a 0 message does not change state. + assert mock_dhcp._dhcp_state == state + + def test_called_from_selecting_good_message(self, mock_dhcp): + # Setup with the required state. + mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING + # Test. + mock_dhcp._process_messaging_states(message_type=wiz_dhcp.DHCP_OFFER) + # Confirm correct new state. + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_REQUESTING + + @freeze_time("2022-3-4") + @pytest.mark.parametrize("lease_time", (0, 8000)) + def test_called_from_requesting_with_ack(self, mock_dhcp, lease_time): + # Setup with the required state. + mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Set the lease_time (zero forces a default to be used). + mock_dhcp._lease_time = lease_time + # Set renew to True to confirm that an ACK sets it to False. + mock_dhcp._renew = True + # Set a start time. + mock_dhcp._start_time = time.monotonic() + # Test. + mock_dhcp._process_messaging_states(message_type=wiz_dhcp.DHCP_ACK) + # Confirm timers are correctly set. + if lease_time == 0: + lease_time = wiz_dhcp.DEFAULT_LEASE_TIME + assert mock_dhcp._t1 == time.monotonic() + lease_time // 2 + assert mock_dhcp._t2 == time.monotonic() + lease_time - lease_time // 8 + assert mock_dhcp._lease_time == time.monotonic() + lease_time + # Check that renew is forced to False + assert mock_dhcp._renew is False + # FSM state should be bound. + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_BOUND + + def test_called_from_requesting_with_nak(self, mock_dhcp): + # Setup with the required state. + mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING + # Test. + mock_dhcp._process_messaging_states(message_type=wiz_dhcp.DHCP_NAK) + # FSM state should be init after receiving a NAK response. + assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_INIT From 052f037e21bd77f8338e2c66ee92f415dbb064c9 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 11 Jan 2023 19:07:42 +1100 Subject: [PATCH 39/80] Deleted obsolete tests for FSM --- tests/test_dhcp_main_logic.py | 522 ---------------------------------- 1 file changed, 522 deletions(-) delete mode 100644 tests/test_dhcp_main_logic.py diff --git a/tests/test_dhcp_main_logic.py b/tests/test_dhcp_main_logic.py deleted file mode 100644 index 3f69bfe..0000000 --- a/tests/test_dhcp_main_logic.py +++ /dev/null @@ -1,522 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Martin Stephens -# -# SPDX-License-Identifier: MIT -"""Tests to confirm the behaviour of methods and functions in the finite state machine. -These test are not exhaustive, but are a sanity check while making changes to the module.""" -import time - -# pylint: disable=no-self-use, redefined-outer-name, protected-access, invalid-name, too-many-arguments -import pytest -from freezegun import freeze_time -import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as wiz_dhcp - - -@pytest.fixture -def mock_dhcp(mocker): - """Instance of DHCP with mock WIZNET5K interface and a mock socket.""" - # Reset the send / receive buffer for each run test. - wiz_dhcp._BUFF = b"" - # Mock the WIZNET5K class to factor its behaviour out of the tests and to control - # responses from methods. - mock_wiznet5k = mocker.patch( - "adafruit_wiznet5k.adafruit_wiznet5k.WIZNET5K", autospec=True - ) - # Instantiate DHCP class for testing. - dhcp = wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) - # Mock the socket for injecting recv() and available() values and to monitor calls - # to send() - dhcp._wiz_sock = mocker.patch( - "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket.socket", autospec=True - ) - # Mock the _receive_dhcp_response method to select whether a response was received, - # assume a good message. - mocker.patch.object(dhcp, "_receive_dhcp_response", autospec=True, return_value=240) - # Mock the _parse_dhcp_response method to inject message types without supplying - # fake DHCP message packets. - mocker.patch.object(dhcp, "_parse_dhcp_response", autospec=True) - # Mock the _send_message_set_next_state to monitor calls to it. - mocker.patch.object(dhcp, "_send_message_set_next_state", autospec=True) - yield dhcp - - -@pytest.mark.parametrize("blocking", (True, False)) -def test_state_machine_blocking_set_correctly(mock_dhcp, blocking): - # Set the initial state to the opposite of the attribute. - mock_dhcp._blocking = not blocking - # Test. - mock_dhcp._dhcp_state_machine(blocking=blocking) - # Check that the attribute is correct. - assert mock_dhcp._blocking is blocking - - -def test_state_machine_default_blocking(mock_dhcp): - # Default is False so set to True. - mock_dhcp._blocking = True - # Test. - mock_dhcp._dhcp_state_machine() - # Check that _blocking is False. - assert mock_dhcp._blocking is False - - -class TestHandleDhcpMessage: - """ - Test the _handle_dhcp_message() method.""" - - @freeze_time("2022-06-10") - def test_with_valid_data_on_socket_selecting(self, mock_dhcp): - # Set up initial values for the test. - # Start FSM in SELECTING state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Receive the expected OFFER message type. - mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER - # Test. - mock_dhcp._handle_dhcp_message() - # Confirm that the message would be parsed. - mock_dhcp._parse_dhcp_response.assert_called_once() - # Confirm that the correct next FSM state was set. - mock_dhcp._prepare_and_set_next_state.assert_called_once_with( - next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 - ) - - @freeze_time("2022-06-10") - def test_with_valid_data_on_socket_requesting_not_renew(self, mock_dhcp): - # Set up initial values for the test. - # Start FSM in REQUESTING state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING - # Receive the expected OFFER message type. - mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK - # Have some data on the socket. - mock_dhcp._wiz_sock.available.return_value = 24 - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Test an initial negotiation, not a renewal or rebind. - mock_dhcp._renew = False - # Store the transaction ID for comparison. - initial_transaction_id = mock_dhcp._transaction_id - # Test. - mock_dhcp._handle_dhcp_message() - # Transaction ID incremented. - assert mock_dhcp._transaction_id == initial_transaction_id + 1 - # Renew has not changed. - assert mock_dhcp._renew is False - # The socket has been released. - assert mock_dhcp._wiz_sock is None - # The correct state has been set. - assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_BOUND - # No DHCP message to be sent. - mock_dhcp._prepare_and_set_next_state.assert_not_called() - - @freeze_time("2022-06-10") - @pytest.mark.parametrize( - "fsm_state, msg_type", - ( - (wiz_dhcp.STATE_SELECTING, wiz_dhcp.DHCP_ACK), - (wiz_dhcp.STATE_REQUESTING, wiz_dhcp.DHCP_OFFER), - ), - ) - def test_with_wrong_message_type_on_socket_nonblocking( - self, - mock_dhcp, - fsm_state, - msg_type, - ): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._parse_dhcp_response.return_value = msg_type - # Receive the incorrect message type. - mock_dhcp._dhcp_state = fsm_state - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Test an initial negotiation, not a renewal or rebind. - mock_dhcp._renew = False - # Nonblocking mode. - mock_dhcp._blocking = False - # Test. - mock_dhcp._handle_dhcp_message() - # Only one call methods because nonblocking, so no attempt to get another message. - mock_dhcp._parse_dhcp_response.assert_called_once() - mock_dhcp._prepare_and_set_next_state.assert_not_called() - # Confirm that the FSM state has not changed. - assert mock_dhcp._dhcp_state == fsm_state - - @freeze_time("2022-06-10") - @pytest.mark.parametrize( - "fsm_state, msg_type, next_state", - ( - ( - wiz_dhcp.STATE_SELECTING, - [wiz_dhcp.DHCP_ACK, wiz_dhcp.DHCP_ACK, wiz_dhcp.DHCP_OFFER], - wiz_dhcp.STATE_REQUESTING, - ), - ( - wiz_dhcp.STATE_REQUESTING, - [ - wiz_dhcp.DHCP_OFFER, - wiz_dhcp.DHCP_OFFER, - wiz_dhcp.DHCP_ACK, - ], - wiz_dhcp.STATE_BOUND, - ), - ), - ) - def test_with_wrong_message_type_on_socket_blocking( - self, mock_dhcp, fsm_state, msg_type, next_state - ): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = fsm_state - # Receive the incorrect message types, then a correct one. - mock_dhcp._parse_dhcp_response.side_effect = msg_type - # Have some data on the socket. - mock_dhcp._wiz_sock.available.return_value = 32 - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Test an initial negotiation, not a renewal or rebind. - mock_dhcp._renew = False - # Put FSM into blocking mode so that multiple attempts are made. - mock_dhcp._blocking = True - # Test. - mock_dhcp._handle_dhcp_message() - # Confirm call count matches the number of messages received. - assert mock_dhcp._parse_dhcp_response.call_count == len(msg_type) - # Confirm correct calls to _send_message_set_next_state are made. - if fsm_state == wiz_dhcp.STATE_SELECTING: - mock_dhcp._prepare_and_set_next_state.assert_called_once() - elif fsm_state == wiz_dhcp.STATE_REQUESTING: # Not called for STATE_REQUESTING - mock_dhcp._prepare_and_set_next_state.assert_not_called() - # Confirm correct final FSM state. - assert mock_dhcp._dhcp_state == next_state - - @freeze_time("2022-06-10") - def test_with_no_data_on_socket_blocking( - self, - mock_dhcp, - ): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Return a correct message type once data is on the socket. - mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER - # No data on the socket, and finally some data. - mock_dhcp._receive_dhcp_response.side_effect = [0, 0, 320] - # Test an initial negotiation, not a renewal or rebind. - mock_dhcp._renew = False - # Put FSM into blocking mode so that multiple attempts are made. - mock_dhcp._blocking = True - # Test. - mock_dhcp._handle_dhcp_message() - # Check _receive_dhcp_response called the correct number of times. - assert mock_dhcp._receive_dhcp_response.call_count == 3 - # Confirm that only one message was processed. - mock_dhcp._parse_dhcp_response.assert_called_once() - - @freeze_time("2022-06-10") - def test_with_no_data_on_socket_nonblocking( - self, - mock_dhcp, - ): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Return a correct message type if data is on the socket. - mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_OFFER - # No data on the socket, and finally some data. - mock_dhcp._receive_dhcp_response.side_effect = [0, 0, 320] - # Test an initial negotiation, not a renewal or rebind. - mock_dhcp._renew = False - # Put FSM into nonblocking mode so that a single attempt is made. - mock_dhcp._blocking = False - # Test. - mock_dhcp._handle_dhcp_message() - # Receive should only be called once in nonblocking mode. - mock_dhcp._receive_dhcp_response.assert_called_once() - # Check that no data was read from the socket. - mock_dhcp._wiz_sock.recv.assert_not_called() - # Confirm that the FSM state has not changed. - assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_SELECTING - - @freeze_time("2022-06-10") - def test_with_valueerror_nonblocking( - self, - mock_dhcp, - ): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - # Raise exceptions due to bad DHCP messages. - mock_dhcp._parse_dhcp_response.side_effect = [ - ValueError, - ValueError, - ] - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Test an initial negotiation, not a renewal or rebind. - mock_dhcp._renew = False - # Put FSM into nonblocking mode so that a single attempt is made. - mock_dhcp._blocking = False - # Test. - mock_dhcp._handle_dhcp_message() - # Receive should only be called once in nonblocking mode. - mock_dhcp._receive_dhcp_response.assert_called_once() - # Confirm that _send_message_set_next_state not called. - mock_dhcp._prepare_and_set_next_state.assert_not_called() - # Check that FSM state has not changed. - assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_SELECTING - - @freeze_time("2022-06-10") - def test_with_valueerror_blocking( - self, - mock_dhcp, - ): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - # Raise exceptions due to bad DHCP messages, then a good one. - mock_dhcp._parse_dhcp_response.side_effect = [ - ValueError, - ValueError, - wiz_dhcp.DHCP_OFFER, - ] - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Test an initial negotiation, not a renewal or rebind. - mock_dhcp._renew = False - # Put FSM into blocking mode so that multiple attempts are made. - mock_dhcp._blocking = True - # Test. - mock_dhcp._handle_dhcp_message() - # Check available() called three times. - assert mock_dhcp._receive_dhcp_response.call_count == 3 - # Confirm that _send_message_set_next_state was called to change FSM state. - mock_dhcp._prepare_and_set_next_state.assert_called_once_with( - next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 - ) - - @freeze_time("2022-06-10", auto_tick_seconds=1) - def test_timeout_blocking( - self, - mock_dhcp, - ): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - # Never have data on the socket to force a timeout. - mock_dhcp._wiz_sock.available.return_value = 0 - # Set an initial value for timeout. - mock_dhcp._next_resend = time.monotonic() + 5 - # Set maximum retries to 3. - mock_dhcp._max_retries = 3 - # Test an initial negotiation, not a renewal or rebind. - mock_dhcp._renew = False - # Put FSM into blocking mode so that multiple attempts are made. - mock_dhcp._blocking = True - # Test that a TimeoutError is raised. - with pytest.raises(TimeoutError): - mock_dhcp._handle_dhcp_message() - - @freeze_time("2022-06-10", auto_tick_seconds=1) - def test_timeout_nonblocking( - self, - mock_dhcp, - ): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - # Never have data on the socket to force a timeout. - mock_dhcp._wiz_sock.available.return_value = 0 - # Set up initial values for the test - mock_dhcp._next_resend = time.monotonic() + 5 - # Set maximum retries to 3. - mock_dhcp._max_retries = 3 - # Test an initial negotiation, not a renewal or rebind. - mock_dhcp._renew = False - # Put FSM into nonblocking mode so that a single attempt is made. - mock_dhcp._blocking = False - # Test. - mock_dhcp._handle_dhcp_message() - # Confirm that the retries was not incremented, i.e. the loop executed once. - assert mock_dhcp._retries == 0 - - @freeze_time("2022-06-10") - def test_requesting_with_renew_nak(self, mock_dhcp): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING - # Return a correct message type. - mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_NAK - # Have some data on the socket. - mock_dhcp._wiz_sock.available.return_value = 32 - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Put FSM into nonblocking mode so that a single attempt is made. - mock_dhcp._blocking = False - # Test a renewal or rebind. - mock_dhcp._renew = True - # Test. - mock_dhcp._handle_dhcp_message() - # Confirm _renew remains True - assert mock_dhcp._renew is True - # Confirm that a NAK puts the FSM into the INIT state. - assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_INIT - - @freeze_time("2022-06-10") - def test_requesting_with_renew_ack(self, mock_dhcp): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING - # Return a correct message type. - mock_dhcp._parse_dhcp_response.return_value = wiz_dhcp.DHCP_ACK - # Have some data on the socket. - mock_dhcp._wiz_sock.available.return_value = 32 - # Avoid a timeout before checking the message. - mock_dhcp._next_resend = time.monotonic() + 5 - # Put FSM into nonblocking mode so that a single attempt is made. - mock_dhcp._blocking = False - # Test a renewal or rebind. - mock_dhcp._renew = True - # Test. - mock_dhcp._handle_dhcp_message() - # Lease renewed so confirm _renew is False. - assert mock_dhcp._renew is False - # Confirm that socket was released. - assert mock_dhcp._wiz_sock is None - # Confirm that the state is BOUND. - assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_BOUND - - @freeze_time("2022-06-10") - def test_requesting_with_renew_no_data(self, mock_dhcp): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING - # Never have data on the socket. - mock_dhcp._wiz_sock.available.return_value = 0 - # Initial timeout value. - mock_dhcp._next_resend = time.monotonic() + 5 - # Put FSM into nonblocking mode so that a single attempt is made. - mock_dhcp._blocking = False - # Test a renewal or rebind. - mock_dhcp._renew = True - # Test. - mock_dhcp._handle_dhcp_message() - # Confirm that _renew remains True - assert mock_dhcp._renew is True - # Confirm that state remains as REQUESTING - assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_REQUESTING - - @freeze_time("2022-06-10", auto_tick_seconds=60) - def test_requesting_with_timeout_renew(self, mock_dhcp): - # Set up initial values for the test. - # Start FSM in required state. - mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING - # Never have data on the socket. - mock_dhcp._wiz_sock.available.return_value = 0 - # Initial timeout value. - mock_dhcp._next_resend = time.monotonic() + 5 - # Set retries to 3 so that it times out on the first pass. - mock_dhcp._retries = 3 - # Test a renewal or rebind. - mock_dhcp._renew = True - # Put FSM into blocking mode so that multiple attempts are made. - mock_dhcp._blocking = True - # Test. - mock_dhcp._handle_dhcp_message() - # Confirm that _renew remains True - assert mock_dhcp._renew is True - # Confirm that state remains as REQUESTING - assert mock_dhcp._dhcp_state == wiz_dhcp.STATE_REQUESTING - - -class TestStateMachine: - def test_init_state(self, mocker, mock_dhcp): - mocker.patch.object(mock_dhcp, "_dsm_reset") - mock_dhcp._dhcp_state = wiz_dhcp.STATE_INIT - - mock_dhcp._dhcp_state_machine() - - mock_dhcp._dsm_reset.assert_called_once() - mock_dhcp._prepare_and_set_next_state.assert_called_once_with( - next_state=wiz_dhcp.STATE_SELECTING, max_retries=3 - ) - - def test_selecting_state(self, mocker, mock_dhcp): - mocker.patch.object(mock_dhcp, "_handle_dhcp_message") - - mock_dhcp._dhcp_state = wiz_dhcp.STATE_SELECTING - - mock_dhcp._dhcp_state_machine() - - assert mock_dhcp._max_retries == 3 - mock_dhcp._handle_dhcp_message.assert_called_once() - - def test_requesting_state(self, mocker, mock_dhcp): - mocker.patch.object(mock_dhcp, "_handle_dhcp_message") - - mock_dhcp._dhcp_state = wiz_dhcp.STATE_REQUESTING - - mock_dhcp._dhcp_state_machine() - - assert mock_dhcp._max_retries == 3 - mock_dhcp._handle_dhcp_message.assert_called_once() - - @pytest.mark.parametrize( - "elapsed_time, expected_state", - ( - ( - 20.0, - wiz_dhcp.STATE_BOUND, - ), - (60.0, wiz_dhcp.STATE_BOUND), - (110.0, wiz_dhcp.STATE_BOUND), - (160.0, wiz_dhcp.STATE_INIT), - ), - ) - def test_bound_state(self, mock_dhcp, elapsed_time, expected_state): - with freeze_time("2022-10-12") as frozen_datetime: - mock_dhcp._dhcp_state = wiz_dhcp.STATE_BOUND - mock_dhcp._t1 = time.monotonic() + 50 - mock_dhcp._t2 = time.monotonic() + 100 - mock_dhcp._lease_time = time.monotonic() + 150 - - frozen_datetime.tick(elapsed_time) - - mock_dhcp._dhcp_state_machine() - - assert mock_dhcp._dhcp_state == expected_state - if expected_state == wiz_dhcp.STATE_INIT: - assert mock_dhcp._blocking is True - - @freeze_time("2022-10-15") - def test_renewing_state(self, mocker, mock_dhcp): - mocker.patch.object(mock_dhcp, "_socket_setup") - mock_dhcp._dhcp_state = wiz_dhcp.STATE_RENEWING - - mock_dhcp._dhcp_state_machine() - - assert mock_dhcp._renew is True - assert mock_dhcp._start_time == time.monotonic() - mock_dhcp._dhcp_connection_setup.assert_called_once() - mock_dhcp._prepare_and_set_next_state.assert_called_once_with( - next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 - ) - - @freeze_time("2022-10-15") - def test_rebinding_state(self, mocker, mock_dhcp): - mocker.patch.object(mock_dhcp, "_socket_setup") - mock_dhcp._dhcp_state = wiz_dhcp.STATE_REBINDING - mock_dhcp._dhcp_server_ip = (8, 8, 8, 8) - - mock_dhcp._dhcp_state_machine() - - assert mock_dhcp.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR - assert mock_dhcp._renew is True - assert mock_dhcp._start_time == time.monotonic() - mock_dhcp._dhcp_connection_setup.assert_called_once() - mock_dhcp._prepare_and_set_next_state.assert_called_once_with( - next_state=wiz_dhcp.STATE_REQUESTING, max_retries=3 - ) From 43b35e3e0e733ac37fa4a54a0268b7a82c91e3c7 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 12 Jan 2023 13:51:52 +1100 Subject: [PATCH 40/80] Added a hexdump to _debugging_message --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index b899d8b..a2d3ced 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -86,7 +86,19 @@ def _debugging_message(message: Union[Exception, str], debugging: bool) -> None: """Helper function to print debugging messages.""" if debugging: + if isinstance(message, (bytes, bytearray)): + temp = "" + for index, value in enumerate(message): + if not index % 16: + temp += "\n" + elif not index % 8: + temp += " " + else: + temp += " " + temp += "{:02x}".format(value) + message = temp print(message) + gc.collect() class DHCP: From bbc9887a134ac5227e3bb3ac1b62b1c8112b2c6d Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 12 Jan 2023 14:09:28 +1100 Subject: [PATCH 41/80] Added debugging messages --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index a2d3ced..22faeeb 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -83,7 +83,9 @@ _BUFF = bytearray(BUFF_LENGTH) -def _debugging_message(message: Union[Exception, str], debugging: bool) -> None: +def _debugging_message( + message: Union[Exception, str, bytes, bytearray], debugging: bool +) -> None: """Helper function to print debugging messages.""" if debugging: if isinstance(message, (bytes, bytearray)): @@ -326,6 +328,7 @@ def _receive_dhcp_response(self, timeout: float) -> int: _BUFF[bytes_read:] = bytearray(BUFF_LENGTH - bytes_read) del buffer gc.collect() + _debugging_message(_BUFF, self._debug) return bytes_read def _process_messaging_states(self, *, message_type: int): @@ -402,7 +405,7 @@ def _handle_dhcp_message(self) -> int: _debugging_message(error, self._debug) if not self._blocking or self._renew: _debugging_message( - "No message, nonblocking or renewing, exiting loop.", + "No message, FSM is nonblocking or renewing, exiting loop.", self._debug, ) return 0 # Did not receive a response in a single attempt. @@ -416,7 +419,7 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: the main program. The initial lease... """ _debugging_message( - "DHCP FSM called with blocking={}".format(blocking), self._debug + "DHCP FSM called with blocking = {}".format(blocking), self._debug ) _debugging_message( "FSM initial state is {}".format(self._dhcp_state), self._debug @@ -449,12 +452,14 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._dhcp_state = STATE_RENEWING if self._dhcp_state == STATE_RENEWING: + _debugging_message("FSM state is RENEWING.", self._debug) self._renew = True self._dhcp_connection_setup() self._start_time = time.monotonic() self._dhcp_state = STATE_REQUESTING if self._dhcp_state == STATE_REBINDING: + _debugging_message("FSM state is REBINDING.", self._debug) self._renew = True self.dhcp_server_ip = BROADCAST_SERVER_ADDR self._dhcp_connection_setup() @@ -462,13 +467,16 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._dhcp_state = STATE_REQUESTING if self._dhcp_state == STATE_INIT: + _debugging_message("FSM state is INIT.", self._debug) self._dsm_reset() self._dhcp_state = STATE_SELECTING if self._dhcp_state == STATE_SELECTING: + _debugging_message("FSM state is SELECTING.", self._debug) self._process_messaging_states(message_type=self._handle_dhcp_message()) if self._dhcp_state == STATE_REQUESTING: + _debugging_message("FSM state is REQUESTING.", self._debug) self._process_messaging_states(message_type=self._handle_dhcp_message()) if self._renew: @@ -518,6 +526,9 @@ def option_writer( _BUFF[offset:data_end] = bytes(option_data) return data_end + _debugging_message( + "Generating DHCP message tyoe {}".format(message_type), self._debug + ) # global _BUFF # pylint: disable=global-variable-not-assigned _BUFF[:] = bytearray(BUFF_LENGTH) # OP.HTYPE.HLEN.HOPS @@ -563,6 +574,7 @@ def option_writer( offset=pointer, option_code=54, option_data=self.dhcp_server_ip ) _BUFF[pointer] = 0xFF + _debugging_message(_BUFF, self._debug) return pointer + 1 def _parse_dhcp_response( @@ -598,6 +610,7 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: option_data = _BUFF[pointer:data_end] return data_end, option_type, option_data + _debugging_message("Parsing DHCP message.", self._debug) # Validate OP if _BUFF[0] != DHCP_BOOT_REPLY: raise ValueError("DHCP message is not the expected DHCP Reply.") @@ -653,7 +666,6 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: ), self._debug, ) - gc.collect() if msg_type is None: raise ValueError("No valid message type in response.") return msg_type From 85f8f766dab7c4a6668df2c2463940c27c349b12 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 12 Jan 2023 15:55:02 +1100 Subject: [PATCH 42/80] Fine tuned debugging messages and fixed a bug the prevented a command being recognized. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 22faeeb..c9c1fd8 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -247,7 +247,7 @@ def _dhcp_connection_setup(self, timeout: float = 5.0) -> None: initialised. """ stop_time = time.monotonic() + timeout - _debugging_message("Creating new socket instance for DHCP.", self._debug) + _debugging_message("Setting up connection for DHCP.", self._debug) while self._wiz_sock is None and time.monotonic() < stop_time: self._wiz_sock = self._eth.get_socket() if self._wiz_sock == 0xFF: @@ -257,11 +257,12 @@ def _dhcp_connection_setup(self, timeout: float = 5.0) -> None: self._eth.write_sock_port(self._wiz_sock, 68) # Set DHCP client port. self._eth.write_sncr(self._wiz_sock, 0x01) # Open the socket. while ( - self._eth.read_sncr(self._wiz_sock) != 0 + self._eth.read_sncr(self._wiz_sock) != b"\x00" ): # Wait for command to complete. time.sleep(0.001) - if self._eth.read_snsr(self._wiz_sock) == bytes([0x22]): + if self._eth.read_snsr(self._wiz_sock) == b"\x22": self._eth.write_sndport(2, DHCP_SERVER_PORT) + _debugging_message("+ Connection OK, port set.", self._debug) return self._wiz_sock = None raise RuntimeError("Unable to initialize UDP socket.") @@ -328,7 +329,7 @@ def _receive_dhcp_response(self, timeout: float) -> int: _BUFF[bytes_read:] = bytearray(BUFF_LENGTH - bytes_read) del buffer gc.collect() - _debugging_message(_BUFF, self._debug) + _debugging_message(_BUFF[:bytes_read], self._debug) return bytes_read def _process_messaging_states(self, *, message_type: int): @@ -527,7 +528,7 @@ def option_writer( return data_end _debugging_message( - "Generating DHCP message tyoe {}".format(message_type), self._debug + "Generating DHCP message type {}".format(message_type), self._debug ) # global _BUFF # pylint: disable=global-variable-not-assigned _BUFF[:] = bytearray(BUFF_LENGTH) @@ -574,8 +575,9 @@ def option_writer( offset=pointer, option_code=54, option_data=self.dhcp_server_ip ) _BUFF[pointer] = 0xFF - _debugging_message(_BUFF, self._debug) - return pointer + 1 + pointer += 1 + _debugging_message(_BUFF[:pointer], self._debug) + return pointer def _parse_dhcp_response( self, From 9aa49ea3db9aa3bb4025b004fc8957f5f2150cc6 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 12 Jan 2023 17:49:44 +1100 Subject: [PATCH 43/80] Fixed a bug where the server port was not set. Improved message generation. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 26 +++++++++++++++--- tests/dhcp_dummy_data.py | 29 +++++++++++++++------ tests/test_dhcp_helper_functions.py | 9 ++++--- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index c9c1fd8..2f3d8c0 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -393,6 +393,7 @@ def _handle_dhcp_message(self) -> int: message_length = self._generate_dhcp_message(message_type=msg_type_out) self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) self._eth.socket_write(self._wiz_sock, _BUFF[:message_length]) + self._eth.write_sndport(self._wiz_sock, DHCP_SERVER_PORT) next_resend = self._next_retry_time(attempt=attempt) while time.monotonic() < next_resend: if self._receive_dhcp_response(next_resend): @@ -543,6 +544,8 @@ def option_writer( # Flags (only bit 0 is used, all other bits must be 0) if broadcast: _BUFF[10] = 0b10000000 + else: + _BUFF[10] = 0b00000000 if renew: _BUFF[12:16] = bytes(self.local_ip) # chaddr @@ -561,11 +564,23 @@ def option_writer( pointer = option_writer( offset=pointer, option_code=12, option_data=self._hostname ) + + # Option - Client ID + pointer = option_writer( + offset=pointer, + option_code=61, + option_data=tuple(b"\x01" + bytes(self._mac_address)), + ) + + # Request subnet mask, router and DNS server. + pointer = option_writer(offset=pointer, option_code=55, option_data=(1, 3, 6)) + + # Request a 90 day lease. + pointer = option_writer( + offset=pointer, option_code=51, option_data=b"\x00\x76\xa7\x00" + ) + if message_type == DHCP_REQUEST: - # Request subnet mask, router and DNS server. - pointer = option_writer( - offset=pointer, option_code=55, option_data=(1, 3, 6) - ) # Set Requested IP Address to offered IP address. pointer = option_writer( offset=pointer, option_code=50, option_data=self.local_ip @@ -574,8 +589,11 @@ def option_writer( pointer = option_writer( offset=pointer, option_code=54, option_data=self.dhcp_server_ip ) + _BUFF[pointer] = 0xFF pointer += 1 + if pointer > BUFF_LENGTH: + raise ValueError("DHCP message too long.") _debugging_message(_BUFF[:pointer], self._debug) return pointer diff --git a/tests/dhcp_dummy_data.py b/tests/dhcp_dummy_data.py index 5959988..014b3fc 100644 --- a/tests/dhcp_dummy_data.py +++ b/tests/dhcp_dummy_data.py @@ -25,7 +25,10 @@ def _build_message(message_body: bytearray, message_options: bytearray) -> bytea b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" b"\x08\t\x00\x00\x00\x00\x00\x00\x00\x00" ) -options = bytearray(b"c\x82Sc5\x01\x01\x0c\x12WIZnet040506070809\xff") +options = bytearray( + b"c\x82Sc5\x01\x01\x0c\x12WIZnet040506070809=\x07\x01" + b"\x04\x05\x06\x07\x08\t7\x03\x01\x03\x063\x04\x00v\xa7\x00\xff" +) DHCP_SEND_01 = _build_message(message, options) message = bytearray( @@ -33,21 +36,30 @@ def _build_message(message_body: bytearray, message_options: bytearray) -> bytea b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" b"\x08\t" ) -options = bytearray(b"c\x82Sc5\x01\x01\x0c\x12WIZnet040506070809\xff") +options = bytearray( + b"c\x82Sc5\x01\x01\x0c\x12WIZnet040506070809=\x07\x01" + b"\x04\x05\x06\x07\x08\t7\x03\x01\x03\x063\x04\x00v\xa7\x00\xff" +) DHCP_SEND_02 = _build_message(message, options) message = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00#\x80\x00\xc0\xa8\x03\x04" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18#.9DO" ) -options = bytearray(b"c\x82Sc5\x01\x01\x0c\x04bert\xff") +options = bytearray( + b"c\x82Sc5\x01\x01\x0c\x04bert=\x07\x01\x18#.9DO7" + b"\x03\x01\x03\x063\x04\x00v\xa7\x00\xff" +) DHCP_SEND_03 = _build_message(message, options) message = bytearray( b"\x01\x01\x06\x00o\xff\xff\xff\x00#\x80\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c" ) -options = bytearray(b"c\x82Sc5\x01\x01\x0c\x05clash\xff") +options = bytearray( + b"c\x82Sc5\x01\x01\x0c\x05clash=\x07\x01\xffa$e*c7" + b"\x03\x01\x03\x063\x04\x00v\xa7\x00\xff" +) DHCP_SEND_04 = _build_message(message, options) # DHCP REQUEST messages. @@ -57,8 +69,8 @@ def _build_message(message_body: bytearray, message_options: bytearray) -> bytea ) options = bytearray( - b"c\x82Sc5\x01\x03\x0c\nhelicopter7\x03\x01\x03\x062" - b"\x04\n\n\n+6\x04\x91B-\x16\xff\x00\x00\x00" + b"c\x82Sc5\x01\x03\x0c\nhelicopter=\x07\x01\xffa$e*c7" + b"\x03\x01\x03\x063\x04\x00v\xa7\x002\x04\n\n\n+6\x04\x91B-\x16\xff" ) DHCP_SEND_05 = _build_message(message, options) @@ -69,8 +81,9 @@ def _build_message(message_body: bytearray, message_options: bytearray) -> bytea ) options = bytearray( - b"c\x82Sc5\x01\x03\x0c\x12WIZnet4B3FA604C8657\x03\x01\x03" - b"\x062\x04def\x046\x04\xf5\xa6\x05\x0b\xff" + b"c\x82Sc5\x01\x03\x0c\x12WIZnet4B3FA604C865=\x07\x01K?\xa6" + b"\x04\xc8e7\x03\x01\x03\x063\x04\x00v\xa7\x002\x04def\x046" + b"\x04\xf5\xa6\x05\x0b\xff" ) DHCP_SEND_06 = _build_message(message, options) diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 4a133e4..64e611b 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -450,7 +450,7 @@ def test_next_retry_time_optional_attrs(self, mocker, mock_wiznet5k, interval): @freeze_time("2022-7-6") def test_setup_socket_with_no_error(self, mocker, mock_wiznet5k): mocker.patch.object(mock_wiznet5k, "get_socket", return_value=2) - mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=0) + mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=b"\x00") mocker.patch.object(mock_wiznet5k, "read_snsr", return_value=b"\x22") dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) dhcp_client._dhcp_connection_setup() @@ -467,7 +467,7 @@ def test_setup_socket_with_no_error(self, mocker, mock_wiznet5k): @freeze_time("2022-7-6", auto_tick_seconds=2) def test_setup_socket_with_timeout_on_get_socket(self, mocker, mock_wiznet5k): mocker.patch.object(mock_wiznet5k, "get_socket", return_value=0xFF) - mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=0) + mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=b"\x00") mocker.patch.object(mock_wiznet5k, "read_snsr", return_value=b"\x22") dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) with pytest.raises(RuntimeError): @@ -477,7 +477,7 @@ def test_setup_socket_with_timeout_on_get_socket(self, mocker, mock_wiznet5k): @freeze_time("2022-7-6", auto_tick_seconds=2) def test_setup_socket_with_timeout_on_socket_is_udp(self, mocker, mock_wiznet5k): mocker.patch.object(mock_wiznet5k, "get_socket", return_value=2) - mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=0) + mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=b"\x00") mocker.patch.object(mock_wiznet5k, "read_snsr", return_value=b"\x21") dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) with pytest.raises(RuntimeError): @@ -526,6 +526,9 @@ def test_good_data(self, mocker, mock_wiznet5k, fsm_state, msg_in): dhcp_client._eth.write_sndipr.assert_called_once_with( 3, dhcp_client.dhcp_server_ip ) + dhcp_client._eth.write_sndport.assert_called_once_with( + dhcp_client._wiz_sock, wiz_dhcp.DHCP_SERVER_PORT + ) dhcp_client._eth.socket_write.assert_called_once_with(3, wiz_dhcp._BUFF[:300]) dhcp_client._next_retry_time.assert_called_once_with(attempt=0) dhcp_client._receive_dhcp_response.assert_called_once_with(time.monotonic() + 5) From 4829ec78028d7b3b099a63d845191ad56767f9a6 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 12 Jan 2023 17:58:44 +1100 Subject: [PATCH 44/80] Really fixed the server port bug. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 2f3d8c0..5f6b291 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -392,8 +392,8 @@ def _handle_dhcp_message(self) -> int: for attempt in range(4): # Initial attempt plus 3 retries. message_length = self._generate_dhcp_message(message_type=msg_type_out) self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) - self._eth.socket_write(self._wiz_sock, _BUFF[:message_length]) self._eth.write_sndport(self._wiz_sock, DHCP_SERVER_PORT) + self._eth.socket_write(self._wiz_sock, _BUFF[:message_length]) next_resend = self._next_retry_time(attempt=attempt) while time.monotonic() < next_resend: if self._receive_dhcp_response(next_resend): From b983145fa9d2a3a067a61c378cdaee919ea3701d Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 17 Jan 2023 15:21:58 +1000 Subject: [PATCH 45/80] Pause to integrate changes from other pull requests. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 2 ++ adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 41eddb2..63567ea 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -952,6 +952,7 @@ def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: If the read was unsuccessful then (0, b"") is returned. """ if self.udp_datasize[socket_num] > 0: + print("+ bytes on socket.") if self.udp_datasize[socket_num] <= length: ret, resp = self.socket_read(socket_num, self.udp_datasize[socket_num]) else: @@ -960,6 +961,7 @@ def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: self.socket_read(socket_num, self.udp_datasize[socket_num] - length) self.udp_datasize[socket_num] = 0 return ret, resp + print("+ No bytes on socket.") return 0, b"" def socket_write( diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 5f6b291..d5f9f14 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -314,11 +314,14 @@ def _receive_dhcp_response(self, timeout: float) -> int: minimum_packet_length = 236 buffer = bytearray(b"") bytes_read = 0 + _debugging_message("+ Beginning to receive…", self._debug) while bytes_read <= minimum_packet_length and time.monotonic() < timeout: - buffer.extend( - self._eth.read_udp(self._wiz_sock, BUFF_LENGTH - bytes_read)[1] - ) + x = self._eth.read_udp(self._wiz_sock, BUFF_LENGTH - bytes_read)[1] + buffer.extend(x) bytes_read = len(buffer) + _debugging_message("+ Bytes read so far {}".format(bytes_read), self._debug) + _debugging_message(x, self._debug) + if bytes_read == BUFF_LENGTH: break _debugging_message("Received {} bytes".format(bytes_read), self._debug) From 836bb29389335a06d4f26408f6371da316540e1a Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 21 Jan 2023 13:39:30 +1100 Subject: [PATCH 46/80] Updated tests to match changes merged in from main. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 18 +++---- tests/test_dhcp.py | 30 +++++------ tests/test_dhcp_helper_functions.py | 55 +-------------------- 3 files changed, 23 insertions(+), 80 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index c5c2e3c..8ff89d6 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -68,15 +68,15 @@ _UNASSIGNED_IP_ADDR = b"\x00\x00\x00\x00" # (0.0.0.0) # DHCP Response Options -_MSG_TYPE = 53 -_SUBNET_MASK = 1 -_ROUTERS_ON_SUBNET = 3 -_DNS_SERVERS = 6 -_DHCP_SERVER_ID = 54 -_T1_VAL = 58 -_T2_VAL = 59 -_LEASE_TIME = 51 -_OPT_END = 255 +_MSG_TYPE = const(53) +_SUBNET_MASK = const(1) +_ROUTERS_ON_SUBNET = const(3) +_DNS_SERVERS = const(6) +_DHCP_SERVER_ID = const(54) +_T1_VAL = const(58) +_T2_VAL = const(59) +_LEASE_TIME = const(51) +_OPT_END = const(255) # Packet buffer _BUFF_LENGTH = 318 diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py index a7bb478..62f0680 100644 --- a/tests/test_dhcp.py +++ b/tests/test_dhcp.py @@ -25,18 +25,6 @@ def wrench(mocker): class TestDHCPInit: def test_constants(self): - # DHCP State Machine - assert wiz_dhcp._STATE_DHCP_START == const(0x00) - assert wiz_dhcp._STATE_DHCP_DISCOVER == const(0x01) - assert wiz_dhcp._STATE_DHCP_REQUEST == const(0x02) - assert wiz_dhcp._STATE_DHCP_LEASED == const(0x03) - assert wiz_dhcp._STATE_DHCP_REREQUEST == const(0x04) - assert wiz_dhcp._STATE_DHCP_RELEASE == const(0x05) - assert wiz_dhcp._STATE_DHCP_WAIT == const(0x06) - assert wiz_dhcp._STATE_DHCP_DISCONN == const(0x07) - - # DHCP wait time between attempts - assert wiz_dhcp._DHCP_WAIT_TIME == const(60) # DHCP Message Types assert wiz_dhcp._DHCP_DISCOVER == const(1) @@ -51,13 +39,10 @@ def test_constants(self): # DHCP Message OP Codes assert wiz_dhcp._DHCP_BOOT_REQUEST == const(0x01) assert wiz_dhcp._DHCP_BOOT_REPLY == const(0x02) - assert wiz_dhcp._DHCP_HTYPE10MB == const(0x01) assert wiz_dhcp._DHCP_HTYPE100MB == const(0x02) - assert wiz_dhcp._DHCP_HLENETHERNET == const(0x06) assert wiz_dhcp._DHCP_HOPS == const(0x00) - assert wiz_dhcp._MAGIC_COOKIE == b"c\x82Sc" assert wiz_dhcp._MAX_DHCP_OPT == const(0x10) @@ -67,6 +52,14 @@ def test_constants(self): assert wiz_dhcp._DEFAULT_LEASE_TIME == const(900) assert wiz_dhcp._BROADCAST_SERVER_ADDR == (255, 255, 255, 255) + # DHCP State Machine + assert wiz_dhcp._STATE_INIT == const(0x01) + assert wiz_dhcp._STATE_SELECTING == const(0x02) + assert wiz_dhcp._STATE_REQUESTING == const(0x03) + assert wiz_dhcp._STATE_BOUND == const(0x04) + assert wiz_dhcp._STATE_RENEWING == const(0x05) + assert wiz_dhcp._STATE_REBINDING == const(0x06) + # DHCP Response Options assert wiz_dhcp._MSG_TYPE == 53 assert wiz_dhcp._SUBNET_MASK == 1 @@ -79,6 +72,7 @@ def test_constants(self): assert wiz_dhcp._OPT_END == 255 # Packet buffer + assert wiz_dhcp._BUFF_LENGTH == 318 assert wiz_dhcp._BUFF == bytearray(318) @pytest.mark.parametrize( @@ -219,7 +213,7 @@ def test_send_with_defaults(self, wiznet, wrench): ( (4, 5, 6, 7, 8, 9), None, - wiz_dhcp._STATE_DHCP_DISCOVER, + wiz_dhcp._DHCP_DISCOVER, 23.4, False, 0, @@ -229,7 +223,7 @@ def test_send_with_defaults(self, wiznet, wrench): ( (24, 35, 46, 57, 68, 79), "bert.co.uk", - wiz_dhcp._STATE_DHCP_REQUEST, + wiz_dhcp._DHCP_REQUEST, 35.5, False, (192, 168, 3, 4), @@ -239,7 +233,7 @@ def test_send_with_defaults(self, wiznet, wrench): ( (255, 97, 36, 101, 42, 99), "clash.net", - wiz_dhcp._STATE_DHCP_REQUEST, + wiz_dhcp._DHCP_REQUEST, 35.5, True, (10, 10, 10, 43), diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 27acb01..6f72d2d 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -8,7 +8,8 @@ # pylint: disable=no-self-use, redefined-outer-name, protected-access, invalid-name, too-many-arguments import pytest from freezegun import freeze_time -from micropython import const + +# from micropython import const import dhcp_dummy_data as dhcp_data import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as wiz_dhcp @@ -28,58 +29,6 @@ def mock_dhcp(mock_wiznet5k): class TestDHCPInit: def test_constants(self): """Test all the constants in the DHCP module.""" - # DHCP State Machine - assert wiz_dhcp._STATE_INIT == const(0x01) - assert wiz_dhcp._STATE_SELECTING == const(0x02) - assert wiz_dhcp._STATE_REQUESTING == const(0x03) - assert wiz_dhcp._STATE_BOUND == const(0x04) - assert wiz_dhcp._STATE_RENEWING == const(0x05) - assert wiz_dhcp._STATE_REBINDING == const(0x06) - - # DHCP Message Types - assert wiz_dhcp.DHCP_DISCOVER == const(1) - assert wiz_dhcp.DHCP_OFFER == const(2) - assert wiz_dhcp.DHCP_REQUEST == const(3) - assert wiz_dhcp.DHCP_DECLINE == const(4) - assert wiz_dhcp.DHCP_ACK == const(5) - assert wiz_dhcp.DHCP_NAK == const(6) - assert wiz_dhcp.DHCP_RELEASE == const(7) - assert wiz_dhcp.DHCP_INFORM == const(8) - - # DHCP Message OP Codes - assert wiz_dhcp.DHCP_BOOT_REQUEST == const(0x01) - assert wiz_dhcp.DHCP_BOOT_REPLY == const(0x02) - - assert wiz_dhcp.DHCP_HTYPE10MB == const(0x01) - assert wiz_dhcp.DHCP_HTYPE100MB == const(0x02) - - assert wiz_dhcp.DHCP_HLENETHERNET == const(0x06) - assert wiz_dhcp.DHCP_HOPS == const(0x00) - - assert wiz_dhcp.MAGIC_COOKIE == b"c\x82Sc" - assert wiz_dhcp.MAX_DHCP_OPT == const(0x10) - - # Default DHCP Server port - assert wiz_dhcp.DHCP_SERVER_PORT == const(67) - # DHCP Lease Time, in seconds - assert wiz_dhcp.DEFAULT_LEASE_TIME == const(900) - assert wiz_dhcp.BROADCAST_SERVER_ADDR == b"\xff\xff\xff\xff" - assert wiz_dhcp._UNASSIGNED_IP_ADDR == b"\x00\x00\x00\x00" - - # DHCP Response Options - assert wiz_dhcp.MSG_TYPE == 53 - assert wiz_dhcp.SUBNET_MASK == 1 - assert wiz_dhcp.ROUTERS_ON_SUBNET == 3 - assert wiz_dhcp.DNS_SERVERS == 6 - assert wiz_dhcp.DHCP_SERVER_ID == 54 - assert wiz_dhcp.T1_VAL == 58 - assert wiz_dhcp.T2_VAL == 59 - assert wiz_dhcp.LEASE_TIME == 51 - assert wiz_dhcp.OPT_END == 255 - - # Packet buffer - assert wiz_dhcp._BUFF_LENGTH == 318 - assert wiz_dhcp._BUFF == bytearray(318) @pytest.mark.parametrize( "mac_address", From e6d3ea348966af9db001198c121f6a3785964673 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 21 Jan 2023 13:51:59 +1100 Subject: [PATCH 47/80] Updated tests to match changes merged in from main. --- tests/test_dhcp.py | 506 ---------------------------- tests/test_dhcp_helper_functions.py | 58 ++-- 2 files changed, 29 insertions(+), 535 deletions(-) delete mode 100644 tests/test_dhcp.py diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py deleted file mode 100644 index 62f0680..0000000 --- a/tests/test_dhcp.py +++ /dev/null @@ -1,506 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Martin Stephens -# -# SPDX-License-Identifier: MIT -"""Tests to confirm that there are no changes in behaviour to public methods and functions.""" -# pylint: disable=no-self-use, redefined-outer-name, protected-access, invalid-name, too-many-arguments -import pytest -from micropython import const -import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as wiz_dhcp - -# -DEFAULT_DEBUG_ON = True - - -@pytest.fixture -def wiznet(mocker): - return mocker.patch("adafruit_wiznet5k.adafruit_wiznet5k.WIZNET5K", autospec=True) - - -@pytest.fixture -def wrench(mocker): - return mocker.patch( - "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.socket", autospec=True - ) - - -class TestDHCPInit: - def test_constants(self): - - # DHCP Message Types - assert wiz_dhcp._DHCP_DISCOVER == const(1) - assert wiz_dhcp._DHCP_OFFER == const(2) - assert wiz_dhcp._DHCP_REQUEST == const(3) - assert wiz_dhcp._DHCP_DECLINE == const(4) - assert wiz_dhcp._DHCP_ACK == const(5) - assert wiz_dhcp._DHCP_NAK == const(6) - assert wiz_dhcp._DHCP_RELEASE == const(7) - assert wiz_dhcp._DHCP_INFORM == const(8) - - # DHCP Message OP Codes - assert wiz_dhcp._DHCP_BOOT_REQUEST == const(0x01) - assert wiz_dhcp._DHCP_BOOT_REPLY == const(0x02) - assert wiz_dhcp._DHCP_HTYPE10MB == const(0x01) - assert wiz_dhcp._DHCP_HTYPE100MB == const(0x02) - assert wiz_dhcp._DHCP_HLENETHERNET == const(0x06) - assert wiz_dhcp._DHCP_HOPS == const(0x00) - assert wiz_dhcp._MAGIC_COOKIE == b"c\x82Sc" - assert wiz_dhcp._MAX_DHCP_OPT == const(0x10) - - # Default DHCP Server port - assert wiz_dhcp._DHCP_SERVER_PORT == const(67) - # DHCP Lease Time, in seconds - assert wiz_dhcp._DEFAULT_LEASE_TIME == const(900) - assert wiz_dhcp._BROADCAST_SERVER_ADDR == (255, 255, 255, 255) - - # DHCP State Machine - assert wiz_dhcp._STATE_INIT == const(0x01) - assert wiz_dhcp._STATE_SELECTING == const(0x02) - assert wiz_dhcp._STATE_REQUESTING == const(0x03) - assert wiz_dhcp._STATE_BOUND == const(0x04) - assert wiz_dhcp._STATE_RENEWING == const(0x05) - assert wiz_dhcp._STATE_REBINDING == const(0x06) - - # DHCP Response Options - assert wiz_dhcp._MSG_TYPE == 53 - assert wiz_dhcp._SUBNET_MASK == 1 - assert wiz_dhcp._ROUTERS_ON_SUBNET == 3 - assert wiz_dhcp._DNS_SERVERS == 6 - assert wiz_dhcp._DHCP_SERVER_ID == 54 - assert wiz_dhcp._T1_VAL == 58 - assert wiz_dhcp._T2_VAL == 59 - assert wiz_dhcp._LEASE_TIME == 51 - assert wiz_dhcp._OPT_END == 255 - - # Packet buffer - assert wiz_dhcp._BUFF_LENGTH == 318 - assert wiz_dhcp._BUFF == bytearray(318) - - @pytest.mark.parametrize( - "mac_address", - ( - [1, 2, 3, 4, 5, 6], - (7, 8, 9, 10, 11, 12), - bytes([1, 2, 4, 6, 7, 8]), - ), - ) - def test_dhcp_setup_default(self, mocker, wiznet, wrench, mac_address): - # Test with mac address as tuple, list and bytes with default values. - mock_randint = mocker.patch( - "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", autospec=True - ) - mock_randint.return_value = 0x1234567 - dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address) - assert dhcp_client._eth == wiznet - assert dhcp_client._response_timeout == 30.0 - assert dhcp_client._debug is False - assert dhcp_client._mac_address == mac_address - wrench.set_interface.assert_called_once_with(wiznet) - assert dhcp_client._sock is None - assert dhcp_client._dhcp_state == wiz_dhcp._STATE_DHCP_START - assert dhcp_client._initial_xid == 0 - mock_randint.assert_called_once() - assert dhcp_client._transaction_id == 0x1234567 - assert dhcp_client._start_time == 0 - assert dhcp_client.dhcp_server_ip == wiz_dhcp._BROADCAST_SERVER_ADDR - assert dhcp_client.local_ip == 0 - assert dhcp_client.gateway_ip == 0 - assert dhcp_client.subnet_mask == 0 - assert dhcp_client.dns_server_ip == 0 - assert dhcp_client._lease_time == 0 - assert dhcp_client._last_lease_time == 0 - assert dhcp_client._renew_in_sec == 0 - assert dhcp_client._rebind_in_sec == 0 - assert dhcp_client._t1 == 0 - assert dhcp_client._t2 == 0 - mac_string = "".join("{:02X}".format(o) for o in mac_address) - assert dhcp_client._hostname == bytes( - "WIZnet{}".split(".", maxsplit=1)[0].format(mac_string)[:42], "utf-8" - ) - - def test_dhcp_setup_other_args(self, wiznet): - mac_address = (7, 8, 9, 10, 11, 12) - dhcp_client = wiz_dhcp.DHCP( - wiznet, mac_address, hostname="fred.com", response_timeout=25.0, debug=True - ) - - assert dhcp_client._response_timeout == 25.0 - assert dhcp_client._debug is True - mac_string = "".join("{:02X}".format(o) for o in mac_address) - assert dhcp_client._hostname == bytes( - "fred.com".split(".", maxsplit=1)[0].format(mac_string)[:42], "utf-8" - ) - - -class TestSendDHCPMessage: - DHCP_SEND_01 = bytearray( - b"\x01\x01\x06\x00\xff\xff\xffo\x00\x17\x80\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" - b"\x08\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x01=" - b"\x07\x01\x04\x05\x06\x07\x08\t\x0c\x12WIZnet040506070809" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007\x06\x01\x03" - b"\x06\x0f:;\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - ) - - DHCP_SEND_02 = bytearray( - b"\x01\x01\x06\x00\xff\xff\xffo\x00#\x80\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18#.9DO\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x02=\x07\x01\x18#.9DO" - b"\x0c\x04bert\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007\x06" - b"\x01\x03\x06\x0f:;\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - ) - - DHCP_SEND_03 = bytearray( - b"\x01\x01\x06\x00\xff\xff\xffo\x00#\x80\x00\n\n\n+\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\xffa$e*c\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00c\x82Sc5\x01\x02=\x07\x01\xffa$e*c\x0c\x05cl" - b"ash\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007" - b"\x06\x01\x03\x06\x0f:;\xff\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - ) - - def test_send_with_defaults(self, wiznet, wrench): - assert len(wiz_dhcp._BUFF) == 318 - dhcp_client = wiz_dhcp.DHCP(wiznet, (4, 5, 6, 7, 8, 9)) - dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) - dhcp_client._transaction_id = 0x6FFFFFFF - dhcp_client.send_dhcp_message(1, 23.4) - dhcp_client._sock.send.assert_called_once_with(self.DHCP_SEND_01) - assert len(wiz_dhcp._BUFF) == 318 - - @pytest.mark.parametrize( - "mac_address, hostname, state, time_elapsed, renew, local_ip, server_ip, result", - ( - ( - (4, 5, 6, 7, 8, 9), - None, - wiz_dhcp._DHCP_DISCOVER, - 23.4, - False, - 0, - 0, - DHCP_SEND_01, - ), - ( - (24, 35, 46, 57, 68, 79), - "bert.co.uk", - wiz_dhcp._DHCP_REQUEST, - 35.5, - False, - (192, 168, 3, 4), - (222, 123, 23, 10), - DHCP_SEND_02, - ), - ( - (255, 97, 36, 101, 42, 99), - "clash.net", - wiz_dhcp._DHCP_REQUEST, - 35.5, - True, - (10, 10, 10, 43), - (145, 66, 45, 22), - DHCP_SEND_03, - ), - ), - ) - def test_send_dhcp_message( - self, - wiznet, - wrench, - mac_address, - hostname, - state, - time_elapsed, - renew, - local_ip, - server_ip, - result, - ): - dhcp_client = wiz_dhcp.DHCP(wiznet, mac_address, hostname=hostname) - # Mock out socket to check what is sent - dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) - # Set client attributes for test - dhcp_client.local_ip = local_ip - dhcp_client.dhcp_server_ip = server_ip - dhcp_client._transaction_id = 0x6FFFFFFF - # Test - dhcp_client.send_dhcp_message(state, time_elapsed, renew=renew) - dhcp_client._sock.send.assert_called_once_with(result) - assert len(wiz_dhcp._BUFF) == 318 - - -class TestParseDhcpMessage: - # Basic case, no extra fields, one each of router and DNS. - GOOD_DATA_01 = bytearray( - b"\x02\x00\x00\x00\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\xc0" - b"\xa8\x05\x16\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x05\x07\t\x0b\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01" - b"\x02\x01\x04\xc0\xa8\x06\x026\x04\xeao\xde{3\x04\x00\x01\x01\x00\x03" - b'\x04yy\x04\x05\x06\x04\x05\x06\x07\x08:\x04\x00""\x00;\x04\x0033\x00' - b"\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - ) - # Complex case, extra field, 2 each router and DNS. - GOOD_DATA_02 = bytearray( - b"\x02\x00\x00\x00\x9axV4\x00\x00\x00\x00\x00\x00\x00\x00\x12$@\n\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5" - b"\x01\x05<\x05\x01\x02\x03\x04\x05\x01\x04\n\x0b\x07\xde6\x04zN\x91\x03\x03" - b"\x08\n\x0b\x0e\x0f\xff\x00\xff\x00\x06\x08\x13\x11\x0b\x07****3\x04\x00\x00" - b"=;:\x04\x00\x0e\x17@;\x04\x02\x92]\xde\xff\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00" - ) - - @pytest.mark.parametrize( - "xid, local_ip, msg_type, subnet, dhcp_ip, gate_ip, dns_ip, lease, t1, t2, response", - ( - ( - 0x7FFFFFFF, - (192, 168, 5, 22), - 2, - (192, 168, 6, 2), - (234, 111, 222, 123), - (121, 121, 4, 5), - (5, 6, 7, 8), - 65792, - 2236928, - 3355392, - GOOD_DATA_01, - ), - ( - 0x3456789A, - (18, 36, 64, 10), - 5, - (10, 11, 7, 222), - (122, 78, 145, 3), - (10, 11, 14, 15), - (19, 17, 11, 7), - 15675, - 923456, - 43146718, - GOOD_DATA_02, - ), - ), - ) - # pylint: disable=too-many-locals - def test_parse_good_data( - self, - wiznet, - wrench, - xid, - local_ip, - msg_type, - subnet, - dhcp_ip, - gate_ip, - dns_ip, - lease, - t1, - t2, - response, - ): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) - dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) - dhcp_client._transaction_id = xid - dhcp_client._initial_xid = dhcp_client._transaction_id.to_bytes(4, "little") - dhcp_client._sock.recv.return_value = response - response_type, response_id = dhcp_client.parse_dhcp_response() - assert response_type == msg_type - assert response_id == bytearray(xid.to_bytes(4, "little")) - assert dhcp_client.local_ip == local_ip - assert dhcp_client.subnet_mask == subnet - assert dhcp_client.dhcp_server_ip == dhcp_ip - assert dhcp_client.gateway_ip == gate_ip - assert dhcp_client.dns_server_ip == dns_ip - assert dhcp_client._lease_time == lease - assert dhcp_client._t1 == t1 - assert dhcp_client._t2 == t2 - - BAD_DATA = bytearray( - b"\x02\x00\x00\x00\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x12$@\n\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc" - ) - - def test_parsing_failures(self, wiznet, wrench): - # Test for bad OP code, ID mismatch, no server ID, bad Magic Cookie - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) - dhcp_client._sock = wrench.socket(type=wrench.SOCK_DGRAM) - dhcp_client._sock.recv.return_value = self.BAD_DATA - # Transaction ID mismatch. - dhcp_client._transaction_id = 0x42424242 - dhcp_client._initial_xid = dhcp_client._transaction_id.to_bytes(4, "little") - with pytest.raises(ValueError): - dhcp_client.parse_dhcp_response() - # Bad OP code. - self.BAD_DATA[0] = 0 - dhcp_client._transaction_id = 0x7FFFFFFF - dhcp_client._initial_xid = dhcp_client._transaction_id.to_bytes(4, "little") - with pytest.raises(RuntimeError): - dhcp_client.parse_dhcp_response() - self.BAD_DATA[0] = 2 # Reset to good value - # No server ID. - self.BAD_DATA[28:34] = (0, 0, 0, 0, 0, 0) - with pytest.raises(ValueError): - dhcp_client.parse_dhcp_response() - self.BAD_DATA[28:34] = (1, 1, 1, 1, 1, 1) # Reset to good value - # Bad Magic Cookie. - self.BAD_DATA[236] = 0 - with pytest.raises(ValueError): - dhcp_client.parse_dhcp_response() - - -class TestStateMachine: - @pytest.mark.parametrize( - "dhcp_state, socket_state", - ( - (wiz_dhcp._STATE_DHCP_START, "Socket"), - (wiz_dhcp._STATE_DHCP_DISCOVER, None), - (wiz_dhcp._STATE_DHCP_REQUEST, None), - (wiz_dhcp._STATE_DHCP_LEASED, None), - (wiz_dhcp._STATE_DHCP_REREQUEST, None), - (wiz_dhcp._STATE_DHCP_RELEASE, None), - (wiz_dhcp._STATE_DHCP_WAIT, None), - ), - ) - def test_link_is_down_state_not_disconnected( - self, mocker, wiznet, dhcp_state, socket_state - ): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) - dhcp_client._eth.link_status = False - dhcp_client._eth.ifconfig = ( - (1, 1, 1, 1), - (1, 1, 1, 1), - (1, 1, 1, 1), - (1, 1, 1, 1), - ) - dhcp_client._last_lease_time = 1 - dhcp_client.dhcp_server_ip = (192, 234, 1, 75) - dhcp_client._dhcp_state = dhcp_state - # If a socket exists, close() will be called, so add a Mock. - if socket_state is not None: - dhcp_client._sock = mocker.MagicMock() - else: - dhcp_client._dhcp_state = None - # Test. - dhcp_client._dhcp_state_machine() - # DHCP state machine in correct state. - assert dhcp_client._dhcp_state == wiz_dhcp._STATE_DHCP_DISCONN - # Check that configurations are returned to defaults. - assert dhcp_client._eth.ifconfig == ( - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - ) - assert dhcp_client._last_lease_time == 0 - assert dhcp_client.dhcp_server_ip == wiz_dhcp._BROADCAST_SERVER_ADDR - assert dhcp_client._sock is None - - def test_link_is_down_state_disconnected(self, wiznet): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) - dhcp_client._eth.link_status = False - dhcp_client._eth.ifconfig = ( - (1, 1, 1, 1), - (1, 1, 1, 1), - (1, 1, 1, 1), - (1, 1, 1, 1), - ) - dhcp_client._last_lease_time = 1 - dhcp_client.dhcp_server_ip = (192, 234, 1, 75) - dhcp_client._sock = "socket" - dhcp_client._dhcp_state = wiz_dhcp._STATE_DHCP_DISCONN - # Test. - dhcp_client._dhcp_state_machine() - # DHCP state machine in correct state. - assert dhcp_client._dhcp_state == wiz_dhcp._STATE_DHCP_DISCONN - # Check that configurations are not altered because state has not changed. - assert dhcp_client._eth.ifconfig == ( - (1, 1, 1, 1), - (1, 1, 1, 1), - (1, 1, 1, 1), - (1, 1, 1, 1), - ) - assert dhcp_client._last_lease_time == 1 - assert dhcp_client.dhcp_server_ip == (192, 234, 1, 75) - assert dhcp_client._sock == "socket" - - def test_link_is_up_state_disconnected(self, wiznet, wrench): - dhcp_client = wiz_dhcp.DHCP(wiznet, (1, 2, 3, 4, 5, 6)) - wrench.socket.side_effect = [RuntimeError] - dhcp_client._eth.link_status = True - dhcp_client._dhcp_state = wiz_dhcp._STATE_DHCP_DISCONN - # Test. - dhcp_client._dhcp_state_machine() - # Assume state is set to START then becomes WAIT after START fails to set a socket - assert dhcp_client._dhcp_state == wiz_dhcp._STATE_DHCP_WAIT diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 6f72d2d..cdf5657 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -55,7 +55,7 @@ def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mac_address): mock_randint.assert_called_once() assert dhcp_client._transaction_id == 0x1234567 assert dhcp_client._start_time == 0 - assert dhcp_client.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR + assert dhcp_client.dhcp_server_ip == wiz_dhcp._BROADCAST_SERVER_ADDR assert dhcp_client.local_ip == wiz_dhcp._UNASSIGNED_IP_ADDR assert dhcp_client.gateway_ip == wiz_dhcp._UNASSIGNED_IP_ADDR assert dhcp_client.subnet_mask == wiz_dhcp._UNASSIGNED_IP_ADDR @@ -95,7 +95,7 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) dhcp_client._transaction_id = 0x6FFFFFFF dhcp_client._start_time = time.monotonic() - 23.4 - dhcp_client._generate_dhcp_message(message_type=wiz_dhcp.DHCP_DISCOVER) + dhcp_client._generate_dhcp_message(message_type=wiz_dhcp._DHCP_DISCOVER) assert wiz_dhcp._BUFF == dhcp_data.DHCP_SEND_01 assert len(wiz_dhcp._BUFF) == 318 @@ -106,7 +106,7 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): ( (4, 5, 6, 7, 8, 9), None, - wiz_dhcp.DHCP_DISCOVER, + wiz_dhcp._DHCP_DISCOVER, 23.4, False, False, @@ -117,7 +117,7 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): ( (24, 35, 46, 57, 68, 79), "bert.co.uk", - wiz_dhcp.DHCP_DISCOVER, + wiz_dhcp._DHCP_DISCOVER, 35.5, True, True, @@ -128,7 +128,7 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): ( (255, 97, 36, 101, 42, 99), "clash.net", - wiz_dhcp.DHCP_DISCOVER, + wiz_dhcp._DHCP_DISCOVER, 35.5, False, True, @@ -175,7 +175,7 @@ def test_generate_dhcp_message_discover_with_non_defaults( ( (255, 97, 36, 101, 42, 99), "helicopter.org", - wiz_dhcp.DHCP_REQUEST, + wiz_dhcp._DHCP_REQUEST, 16.3, False, True, @@ -186,7 +186,7 @@ def test_generate_dhcp_message_discover_with_non_defaults( ( (75, 63, 166, 4, 200, 101), None, - wiz_dhcp.DHCP_REQUEST, + wiz_dhcp._DHCP_REQUEST, 72.4, False, True, @@ -334,7 +334,7 @@ def test_dsm_reset(mocker, mock_wiznet5k): wiz_dhcp._UNASSIGNED_IP_ADDR, wiz_dhcp._UNASSIGNED_IP_ADDR, ) - assert dhcp_client.dhcp_server_ip == wiz_dhcp.BROADCAST_SERVER_ADDR + assert dhcp_client.dhcp_server_ip == wiz_dhcp._BROADCAST_SERVER_ADDR assert dhcp_client.local_ip == wiz_dhcp._UNASSIGNED_IP_ADDR assert dhcp_client.subnet_mask == wiz_dhcp._UNASSIGNED_IP_ADDR assert dhcp_client.dns_server_ip == wiz_dhcp._UNASSIGNED_IP_ADDR @@ -409,7 +409,7 @@ def test_setup_socket_with_no_error(self, mocker, mock_wiznet5k): mock_wiznet5k.write_sncr(2, 0x01) mock_wiznet5k.read_sncr.assert_called_with(2) mock_wiznet5k.write_sndport.assert_called_once_with( - 2, wiz_dhcp.DHCP_SERVER_PORT + 2, wiz_dhcp._DHCP_SERVER_PORT ) assert dhcp_client._wiz_sock == 2 @@ -438,8 +438,8 @@ class TestHandleDhcpMessage: @pytest.mark.parametrize( "fsm_state, msg_in", ( - (wiz_dhcp._STATE_SELECTING, wiz_dhcp.DHCP_DISCOVER), - (wiz_dhcp._STATE_REQUESTING, wiz_dhcp.DHCP_REQUEST), + (wiz_dhcp._STATE_SELECTING, wiz_dhcp._DHCP_DISCOVER), + (wiz_dhcp._STATE_REQUESTING, wiz_dhcp._DHCP_REQUEST), ), ) @freeze_time("2022-5-5") @@ -476,7 +476,7 @@ def test_good_data(self, mocker, mock_wiznet5k, fsm_state, msg_in): 3, dhcp_client.dhcp_server_ip ) dhcp_client._eth.write_sndport.assert_called_once_with( - dhcp_client._wiz_sock, wiz_dhcp.DHCP_SERVER_PORT + dhcp_client._wiz_sock, wiz_dhcp._DHCP_SERVER_PORT ) dhcp_client._eth.socket_write.assert_called_once_with(3, wiz_dhcp._BUFF[:300]) dhcp_client._next_retry_time.assert_called_once_with(attempt=0) @@ -716,25 +716,25 @@ class TestProcessMessagingStates: wiz_dhcp._STATE_SELECTING, ( 0, - wiz_dhcp.DHCP_ACK, - wiz_dhcp.DHCP_REQUEST, - wiz_dhcp.DHCP_DECLINE, - wiz_dhcp.DHCP_DISCOVER, - wiz_dhcp.DHCP_NAK, - wiz_dhcp.DHCP_INFORM, - wiz_dhcp.DHCP_RELEASE, + wiz_dhcp._DHCP_ACK, + wiz_dhcp._DHCP_REQUEST, + wiz_dhcp._DHCP_DECLINE, + wiz_dhcp._DHCP_DISCOVER, + wiz_dhcp._DHCP_NAK, + wiz_dhcp._DHCP_INFORM, + wiz_dhcp._DHCP_RELEASE, ), ), ( wiz_dhcp._STATE_REQUESTING, ( 0, - wiz_dhcp.DHCP_OFFER, - wiz_dhcp.DHCP_REQUEST, - wiz_dhcp.DHCP_DECLINE, - wiz_dhcp.DHCP_DISCOVER, - wiz_dhcp.DHCP_INFORM, - wiz_dhcp.DHCP_RELEASE, + wiz_dhcp._DHCP_OFFER, + wiz_dhcp._DHCP_REQUEST, + wiz_dhcp._DHCP_DECLINE, + wiz_dhcp._DHCP_DISCOVER, + wiz_dhcp._DHCP_INFORM, + wiz_dhcp._DHCP_RELEASE, ), ), ), @@ -753,7 +753,7 @@ def test_called_from_selecting_good_message(self, mock_dhcp): # Setup with the required state. mock_dhcp._dhcp_state = wiz_dhcp._STATE_SELECTING # Test. - mock_dhcp._process_messaging_states(message_type=wiz_dhcp.DHCP_OFFER) + mock_dhcp._process_messaging_states(message_type=wiz_dhcp._DHCP_OFFER) # Confirm correct new state. assert mock_dhcp._dhcp_state == wiz_dhcp._STATE_REQUESTING @@ -769,10 +769,10 @@ def test_called_from_requesting_with_ack(self, mock_dhcp, lease_time): # Set a start time. mock_dhcp._start_time = time.monotonic() # Test. - mock_dhcp._process_messaging_states(message_type=wiz_dhcp.DHCP_ACK) + mock_dhcp._process_messaging_states(message_type=wiz_dhcp._DHCP_ACK) # Confirm timers are correctly set. if lease_time == 0: - lease_time = wiz_dhcp.DEFAULT_LEASE_TIME + lease_time = wiz_dhcp._DEFAULT_LEASE_TIME assert mock_dhcp._t1 == time.monotonic() + lease_time // 2 assert mock_dhcp._t2 == time.monotonic() + lease_time - lease_time // 8 assert mock_dhcp._lease_time == time.monotonic() + lease_time @@ -785,6 +785,6 @@ def test_called_from_requesting_with_nak(self, mock_dhcp): # Setup with the required state. mock_dhcp._dhcp_state = wiz_dhcp._STATE_REQUESTING # Test. - mock_dhcp._process_messaging_states(message_type=wiz_dhcp.DHCP_NAK) + mock_dhcp._process_messaging_states(message_type=wiz_dhcp._DHCP_NAK) # FSM state should be init after receiving a NAK response. assert mock_dhcp._dhcp_state == wiz_dhcp._STATE_INIT From 1792b81ca255e12ef79c63a2493457da6ad56997 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 21 Jan 2023 20:50:31 +1100 Subject: [PATCH 48/80] Fixed a glitch in the docstrings that caused Sphinx to fail. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 8ff89d6..aff3f2e 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -117,9 +117,8 @@ class DHCP: until successful. If the lease expires, the client attempts to obtain a new lease in blocking mode when the maintenance routine is run. - In most circumstances, call `DHCP.request_lease` to obtain a lease, then periodically call - `DHCP.maintain_lease` in non-blocking mode so that the FSM can check whether the lease - needs to be renewed, and can then renew it. + These class methods are not designed to be called directly. They should be called via + methods in the WIZNET5K class. Since DHCP uses UDP, messages may be lost. The DHCP protocol uses exponential backoff for retrying. Retries occur after 4, 8, and 16 seconds (the final retry is followed by @@ -307,6 +306,8 @@ def _receive_dhcp_response(self, timeout: float) -> int: If the packet is too short, it is discarded and zero is returned. The maximum packet size is limited by the size of the global buffer. + :param float timeout: Seconds to wait for a process to complete. + :returns int: The number of bytes stored in the global buffer. """ _debugging_message("Receiving a DHCP response.", self._debug) @@ -606,14 +607,14 @@ def _parse_dhcp_response( """Parse DHCP response from DHCP server. Check that the message is for this client. Extract data from the fixed positions - in the first 236 bytes of the message, then cycle through the options for - additional data. + in the first 236 bytes of the message, then cycle through the options for + additional data. :returns Tuple[int, bytearray]: DHCP packet type and ID. :raises ValueError: Checks that the message is a reply, the transaction ID - matches, a client ID exists and the 'magic cookie' is set. If any of these tests - fail or no message type is found in the options, raises a ValueError. + matches, a client ID exists and the 'magic cookie' is set. If any of these tests + fail or no message type is found in the options, raises a ValueError. """ # pylint: disable=too-many-branches def option_reader(pointer: int) -> Tuple[int, int, bytes]: From 9121bbbab462e855fab60b8787d7e069f5b40db1 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 23 Jan 2023 10:27:31 +1100 Subject: [PATCH 49/80] Moved debug_msg() to __init__.py so that all modules can access it. --- adafruit_wiznet5k/__init__.py | 41 +++++++ adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 115 +++++++------------- 2 files changed, 81 insertions(+), 75 deletions(-) diff --git a/adafruit_wiznet5k/__init__.py b/adafruit_wiznet5k/__init__.py index e69de29..841ffd9 100644 --- a/adafruit_wiznet5k/__init__.py +++ b/adafruit_wiznet5k/__init__.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2023 Martin Stephens +# +# SPDX-License-Identifier: MIT + +"""Makes a debug message function available to all modules.""" +try: + from typing import TYPE_CHECKING, Optional, Union, Tuple, Sequence + + 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. + + :param Union[Exception, str, bytes, bytearray] message: The message to print. If the + message is a bytes type object, create a hexdump. + :param bool debugging: Only print if debugging is True. + """ + if debugging: + if isinstance(message, (bytes, bytearray)): + temp = "" + for index, value in enumerate(message): + if not index % 16: + temp += "\n" + elif not index % 8: + temp += " " + else: + temp += " " + temp += "{:02x}".format(value) + message = temp + print(message) + del message + gc.collect() diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index aff3f2e..30da957 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -28,6 +28,7 @@ import time from random import randint from micropython import const +from adafruit_wiznet5k import debug_msg # pylint: disable=ungrouped-imports # DHCP State Machine _STATE_INIT = const(0x01) @@ -83,26 +84,6 @@ _BUFF = bytearray(_BUFF_LENGTH) -def _debugging_message( - message: Union[Exception, str, bytes, bytearray], debugging: bool -) -> None: - """Helper function to print debugging messages.""" - if debugging: - if isinstance(message, (bytes, bytearray)): - temp = "" - for index, value in enumerate(message): - if not index % 16: - temp += "\n" - elif not index % 8: - temp += " " - else: - temp += " " - temp += "{:02x}".format(value) - message = temp - print(message) - gc.collect() - - class DHCP: """Wiznet5k DHCP Client. @@ -153,7 +134,7 @@ def __init__( :param bool debug: Enable debugging output. """ self._debug = debug - _debugging_message("Initialising DHCP instance.", self._debug) + debug_msg("Initialising DHCP instance.", self._debug) self._response_timeout = response_timeout # Prevent buffer overrun in send_dhcp_message() @@ -192,21 +173,19 @@ def __init__( def request_dhcp_lease(self) -> bool: """Request to renew or acquire a DHCP lease.""" - _debugging_message("Requesting DHCP lease.", self._debug) + debug_msg("Requesting DHCP lease.", self._debug) self._dhcp_state_machine(blocking=True) return self._dhcp_state == _STATE_BOUND def maintain_dhcp_lease(self, blocking: bool = False) -> None: """Maintain DHCP lease""" - _debugging_message( - "Maintaining lease with blocking = {}".format(blocking), self._debug - ) + debug_msg("Maintaining lease with blocking = {}".format(blocking), self._debug) self._dhcp_state_machine(blocking=blocking) def _dsm_reset(self) -> None: """Close the socket and set attributes to default values used by the state machine INIT state.""" - _debugging_message("Resetting DHCP state machine.", self._debug) + debug_msg("Resetting DHCP state machine.", self._debug) self._socket_release() self._dhcp_connection_setup() self.dhcp_server_ip = _BROADCAST_SERVER_ADDR @@ -226,7 +205,7 @@ def _dsm_reset(self) -> None: def _socket_release(self) -> None: """Close the socket if it exists.""" - _debugging_message("Releasing socket.", self._debug) + debug_msg("Releasing socket.", self._debug) if self._wiz_sock: self._eth.socket_close(self._wiz_sock) self._wiz_sock = None @@ -246,7 +225,7 @@ def _dhcp_connection_setup(self, timeout: float = 5.0) -> None: initialised. """ stop_time = time.monotonic() + timeout - _debugging_message("Setting up connection for DHCP.", self._debug) + debug_msg("Setting up connection for DHCP.", self._debug) while self._wiz_sock is None and time.monotonic() < stop_time: self._wiz_sock = self._eth.get_socket() if self._wiz_sock == 0xFF: @@ -261,14 +240,14 @@ def _dhcp_connection_setup(self, timeout: float = 5.0) -> None: time.sleep(0.001) if self._eth.read_snsr(self._wiz_sock) == b"\x22": self._eth.write_sndport(2, _DHCP_SERVER_PORT) - _debugging_message("+ Connection OK, port set.", self._debug) + debug_msg("+ Connection OK, port set.", self._debug) return self._wiz_sock = None raise RuntimeError("Unable to initialize UDP socket.") def _increment_transaction_id(self) -> None: """Increment the transaction ID and roll over from 0x7fffffff to 0.""" - _debugging_message("Incrementing transaction ID", self._debug) + debug_msg("Incrementing transaction ID", self._debug) self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF def _next_retry_time(self, *, attempt: int, interval: int = 4) -> float: @@ -288,9 +267,7 @@ def _next_retry_time(self, *, attempt: int, interval: int = 4) -> float: :raises ValueError: If the interval is not > 1 second as this could return a zero or negative delay. """ - _debugging_message( - "Calculating next retry time and incrementing retries.", self._debug - ) + debug_msg("Calculating next retry time and incrementing retries.", self._debug) if interval <= 1: raise ValueError("Retry interval must be > 1 second.") delay = 2**attempt * interval + randint(-1, 1) + time.monotonic() @@ -310,22 +287,22 @@ def _receive_dhcp_response(self, timeout: float) -> int: :returns int: The number of bytes stored in the global buffer. """ - _debugging_message("Receiving a DHCP response.", self._debug) + debug_msg("Receiving a DHCP response.", self._debug) # DHCP returns the query plus additional data. The query length is 236 bytes. minimum_packet_length = 236 buffer = bytearray(b"") bytes_read = 0 - _debugging_message("+ Beginning to receive…", self._debug) + debug_msg("+ Beginning to receive…", self._debug) while bytes_read <= minimum_packet_length and time.monotonic() < timeout: x = self._eth.read_udp(self._wiz_sock, _BUFF_LENGTH - bytes_read)[1] buffer.extend(x) bytes_read = len(buffer) - _debugging_message("+ Bytes read so far {}".format(bytes_read), self._debug) - _debugging_message(x, self._debug) + debug_msg("+ Bytes read so far {}".format(bytes_read), self._debug) + debug_msg(x, self._debug) if bytes_read == _BUFF_LENGTH: break - _debugging_message("Received {} bytes".format(bytes_read), self._debug) + debug_msg("Received {} bytes".format(bytes_read), self._debug) if bytes_read < minimum_packet_length: bytes_read = 0 else: @@ -333,7 +310,7 @@ def _receive_dhcp_response(self, timeout: float) -> int: _BUFF[bytes_read:] = bytearray(_BUFF_LENGTH - bytes_read) del buffer gc.collect() - _debugging_message(_BUFF[:bytes_read], self._debug) + debug_msg(_BUFF[:bytes_read], self._debug) return bytes_read def _process_messaging_states(self, *, message_type: int): @@ -347,19 +324,15 @@ def _process_messaging_states(self, *, message_type: int): :returns bool: True if the message was valid for the current state. """ if self._dhcp_state == _STATE_SELECTING and message_type == _DHCP_OFFER: - _debugging_message("FSM state is SELECTING with valid OFFER.", self._debug) + debug_msg("FSM state is SELECTING with valid OFFER.", self._debug) self._dhcp_state = _STATE_REQUESTING elif self._dhcp_state == _STATE_REQUESTING: - _debugging_message("FSM state is REQUESTING.", self._debug) + debug_msg("FSM state is REQUESTING.", self._debug) if message_type == _DHCP_NAK: - _debugging_message( - "Message is NAK, setting FSM state to INIT.", self._debug - ) + debug_msg("Message is NAK, setting FSM state to INIT.", self._debug) self._dhcp_state = _STATE_INIT elif message_type == _DHCP_ACK: - _debugging_message( - "Message is ACK, setting FSM state to BOUND.", self._debug - ) + debug_msg("Message is ACK, setting FSM state to BOUND.", self._debug) if self._lease_time == 0: self._lease_time = _DEFAULT_LEASE_TIME self._t1 = self._start_time + self._lease_time // 2 @@ -384,7 +357,7 @@ def _handle_dhcp_message(self) -> int: :raises TimeoutError: If the FSM is in blocking mode and no valid response has been received before the timeout expires. """ - _debugging_message("Processing SELECTING or REQUESTING state.", self._debug) + debug_msg("Processing SELECTING or REQUESTING state.", self._debug) if self._dhcp_state == _STATE_SELECTING: msg_type_out = _DHCP_DISCOVER elif self._dhcp_state == _STATE_REQUESTING: @@ -403,14 +376,14 @@ def _handle_dhcp_message(self) -> int: if self._receive_dhcp_response(next_resend): try: msg_type_in = self._parse_dhcp_response() - _debugging_message( + debug_msg( "Received message type {}".format(msg_type_in), self._debug ) return msg_type_in except ValueError as error: - _debugging_message(error, self._debug) + debug_msg(error, self._debug) if not self._blocking or self._renew: - _debugging_message( + debug_msg( "No message, FSM is nonblocking or renewing, exiting loop.", self._debug, ) @@ -424,48 +397,42 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: A finite state machine to allow the DHCP lease to be managed without blocking the main program. The initial lease... """ - _debugging_message( - "DHCP FSM called with blocking = {}".format(blocking), self._debug - ) - _debugging_message( - "FSM initial state is {}".format(self._dhcp_state), self._debug - ) + debug_msg("DHCP FSM called with blocking = {}".format(blocking), self._debug) + debug_msg("FSM initial state is {}".format(self._dhcp_state), self._debug) self._blocking = blocking while True: if self._dhcp_state == _STATE_BOUND: now = time.monotonic() if now < self._t1: - _debugging_message( - "No timers have expired. Exiting FSM.", self._debug - ) + debug_msg("No timers have expired. Exiting FSM.", self._debug) self._socket_release() return if now > self._lease_time: - _debugging_message( + debug_msg( "Lease has expired, switching state to INIT.", self._debug ) self._blocking = True self._dhcp_state = _STATE_INIT elif now > self._t2: - _debugging_message( + debug_msg( "T2 has expired, switching state to REBINDING.", self._debug ) self._dhcp_state = _STATE_REBINDING else: - _debugging_message( + debug_msg( "T1 has expired, switching state to RENEWING.", self._debug ) self._dhcp_state = _STATE_RENEWING if self._dhcp_state == _STATE_RENEWING: - _debugging_message("FSM state is RENEWING.", self._debug) + debug_msg("FSM state is RENEWING.", self._debug) self._renew = True self._dhcp_connection_setup() self._start_time = time.monotonic() self._dhcp_state = _STATE_REQUESTING if self._dhcp_state == _STATE_REBINDING: - _debugging_message("FSM state is REBINDING.", self._debug) + debug_msg("FSM state is REBINDING.", self._debug) self._renew = True self.dhcp_server_ip = _BROADCAST_SERVER_ADDR self._dhcp_connection_setup() @@ -473,20 +440,20 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._dhcp_state = _STATE_REQUESTING if self._dhcp_state == _STATE_INIT: - _debugging_message("FSM state is INIT.", self._debug) + debug_msg("FSM state is INIT.", self._debug) self._dsm_reset() self._dhcp_state = _STATE_SELECTING if self._dhcp_state == _STATE_SELECTING: - _debugging_message("FSM state is SELECTING.", self._debug) + debug_msg("FSM state is SELECTING.", self._debug) self._process_messaging_states(message_type=self._handle_dhcp_message()) if self._dhcp_state == _STATE_REQUESTING: - _debugging_message("FSM state is REQUESTING.", self._debug) + debug_msg("FSM state is REQUESTING.", self._debug) self._process_messaging_states(message_type=self._handle_dhcp_message()) if self._renew: - _debugging_message( + debug_msg( "Lease has not expired, resetting state to BOUND and exiting FSM.", self._debug, ) @@ -532,9 +499,7 @@ def option_writer( _BUFF[offset:data_end] = bytes(option_data) return data_end - _debugging_message( - "Generating DHCP message type {}".format(message_type), self._debug - ) + debug_msg("Generating DHCP message type {}".format(message_type), self._debug) # global _BUFF # pylint: disable=global-variable-not-assigned _BUFF[:] = bytearray(_BUFF_LENGTH) # OP.HTYPE.HLEN.HOPS @@ -598,7 +563,7 @@ def option_writer( pointer += 1 if pointer > _BUFF_LENGTH: raise ValueError("DHCP message too long.") - _debugging_message(_BUFF[:pointer], self._debug) + debug_msg(_BUFF[:pointer], self._debug) return pointer def _parse_dhcp_response( @@ -634,7 +599,7 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: option_data = _BUFF[pointer:data_end] return data_end, option_type, option_data - _debugging_message("Parsing DHCP message.", self._debug) + debug_msg("Parsing DHCP message.", self._debug) # Validate OP if _BUFF[0] != _DHCP_BOOT_REPLY: raise ValueError("DHCP message is not the expected DHCP Reply.") @@ -675,7 +640,7 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: elif data_type == 0: break - _debugging_message( + debug_msg( "Msg Type: {}\nSubnet Mask: {}\nDHCP Server IP: {}\nDNS Server IP: {}\ \nGateway IP: {}\nLocal IP: {}\nT1: {}\nT2: {}\nLease Time: {}".format( msg_type, From 9a276127508842d443d68d3370c5016b5f889bb8 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 23 Jan 2023 10:54:29 +1100 Subject: [PATCH 50/80] Refactored wiznet5k.py to use debug_msg. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 110 +++++++++++-------------- 1 file changed, 48 insertions(+), 62 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 5be70b4..608c96a 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -48,6 +48,7 @@ from micropython import const from adafruit_bus_device.spi_device import SPIDevice +from adafruit_wiznet5k import debug_msg import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as dhcp import adafruit_wiznet5k.adafruit_wiznet5k_dns as dns @@ -210,8 +211,7 @@ def __init__( if self.link_status or ((time.monotonic() - start_time) > 5): break time.sleep(1) - if self._debug: - print("My Link is:", self.link_status) + debug_msg("My Link is: {}".format(self.link_status), self._debug) self._dhcp_client = None # Set DHCP @@ -236,23 +236,18 @@ def set_dhcp( :return int: 0 if DHCP configured, -1 otherwise. """ - if self._debug: - print("* Initializing DHCP") - + debug_msg("* Initializing DHCP", self._debug) # Return IP assigned by DHCP self._dhcp_client = dhcp.DHCP( self, self.mac_address, hostname, response_timeout, debug=self._debug ) ret = self._dhcp_client.request_dhcp_lease() if ret == 1: - if self._debug: - _ifconfig = self.ifconfig - print("* Found DHCP Server:") - print( - "IP: {}\nSubnet Mask: {}\nGW Addr: {}\nDNS Server: {}".format( - *_ifconfig - ) - ) + debug_msg( + "Found DHCP Server:\nIP: {}\n Subnet Mask: {}\n GW Addr: {}" + "\n DNS Server: {}".format(*self.ifconfig), + self._debug, + ) return 0 return -1 @@ -269,15 +264,13 @@ def get_host_by_name(self, hostname: str) -> bytes: :return Union[int, bytes]: a 4 bytearray. """ - if self._debug: - print("* Get host by name") + debug_msg("Get host by name", self._debug) if isinstance(hostname, str): hostname = bytes(hostname, "utf-8") # 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) + debug_msg("* Resolved IP: {}".format(ret), self._debug) if ret == -1: raise RuntimeError("Failed to resolve hostname!") return ret @@ -627,12 +620,12 @@ def socket_available(self, socket_num: int, sock_type: int = _SNMR_TCP) -> int: :return int: Number of bytes available to read. """ - if self._debug: - print( - "* socket_available called on socket {}, protocol {}".format( - socket_num, sock_type - ) - ) + debug_msg( + "socket_available called on socket {}, protocol {}".format( + socket_num, sock_type + ), + self._debug, + ) if socket_num > self.max_sockets: raise ValueError("Provided socket exceeds max_sockets.") @@ -689,12 +682,12 @@ def socket_connect( """ if not self.link_status: raise ConnectionError("Ethernet cable disconnected!") - if self._debug: - print( - "* w5k socket connect, protocol={}, port={}, ip={}".format( - conn_mode, port, self.pretty_ip(dest) - ) - ) + debug_msg( + "W5K socket connect, protocol={}, port={}, ip={}".format( + conn_mode, port, self.pretty_ip(dest) + ), + self._debug, + ) # initialize a socket and set the mode res = self.socket_open(socket_num, conn_mode=conn_mode) if res == 1: @@ -709,8 +702,9 @@ def socket_connect( # wait for tcp connection establishment while self.socket_status(socket_num)[0] != SNSR_SOCK_ESTABLISHED: time.sleep(0.001) - if self._debug: - print("SN_SR:", self.socket_status(socket_num)[0]) + debug_msg( + "SNSR: {}".format(self.socket_status(socket_num)[0]), self._debug + ) if self.socket_status(socket_num)[0] == SNSR_SOCK_CLOSED: raise ConnectionError("Failed to establish connection.") elif conn_mode == SNMR_UDP: @@ -721,8 +715,7 @@ def _send_socket_cmd(self, socket: int, cmd: int) -> None: """Send a socket command to a socket.""" self.write_sncr(socket, cmd) while self.read_sncr(socket) != b"\x00": - if self._debug: - print("waiting for sncr to clear...") + debug_msg("waiting for SNCR to clear...", self._debug) def get_socket(self) -> int: """Request, allocate and return a socket from the W5k chip. @@ -731,8 +724,7 @@ def get_socket(self) -> int: :return int: The first available socket. Returns 0xFF if no sockets are free. """ - if self._debug: - print("*** Get socket") + debug_msg("get_socket", self._debug) sock = _SOCKET_INVALID for _sock in range(self.max_sockets): @@ -740,9 +732,7 @@ def get_socket(self) -> int: if status == SNSR_SOCK_CLOSED: sock = _sock break - - if self._debug: - print("Allocated socket #{}".format(sock)) + debug_msg("Allocated socket #{}".format(sock), self._debug) return sock def socket_listen( @@ -758,12 +748,12 @@ def socket_listen( """ if not self.link_status: raise ConnectionError("Ethernet cable disconnected!") - if self._debug: - print( - "* Listening on port={}, ip={}".format( - port, self.pretty_ip(self.ip_address) - ) - ) + debug_msg( + "* Listening on port={}, ip={}".format( + port, self.pretty_ip(self.ip_address) + ), + self._debug, + ) # Initialize a socket and set the mode self.src_port = port res = self.socket_open(socket_num, conn_mode=conn_mode) @@ -802,12 +792,12 @@ def socket_accept( dest_ip = self.remote_ip(socket_num) dest_port = self.remote_port(socket_num) next_socknum = self.get_socket() - if self._debug: - print( - "* Dest is ({}, {}), Next listen socknum is #{}".format( - dest_ip, dest_port, next_socknum - ) - ) + debug_msg( + "Dest is ({}, {}), Next listen socknum is #{}".format( + dest_ip, dest_port, next_socknum + ), + self._debug, + ) return next_socknum, (dest_ip, dest_port) def socket_open(self, socket_num: int, conn_mode: int = _SNMR_TCP) -> int: @@ -823,8 +813,7 @@ def socket_open(self, socket_num: int, conn_mode: int = _SNMR_TCP) -> int: """ if not self.link_status: raise ConnectionError("Ethernet cable disconnected!") - if self._debug: - print("*** Opening socket %d" % socket_num) + debug_msg("*** Opening socket {}".format(socket_num), self._debug) status = self.read_snsr(socket_num)[0] if status in ( SNSR_SOCK_CLOSED, @@ -834,8 +823,9 @@ def socket_open(self, socket_num: int, conn_mode: int = _SNMR_TCP) -> int: _SNSR_SOCK_CLOSING, _SNSR_SOCK_UDP, ): - if self._debug: - print("* Opening W5k Socket, protocol={}".format(conn_mode)) + debug_msg( + "* Opening W5k Socket, protocol={}".format(conn_mode), self._debug + ) time.sleep(0.00025) self.write_snmr(socket_num, conn_mode) @@ -865,8 +855,7 @@ def socket_close(self, socket_num: int) -> None: :param int socket_num: The socket to close. """ - if self._debug: - print("*** Closing socket #%d" % socket_num) + debug_msg("*** Closing socket {}".format(socket_num), self._debug) self.write_sncr(socket_num, _CMD_SOCK_CLOSE) while self.read_sncr(socket_num): time.sleep(0.0001) @@ -879,8 +868,7 @@ def socket_disconnect(self, socket_num: int) -> None: :param int socket_num: The socket to close. """ - if self._debug: - print("*** Disconnecting socket #%d" % socket_num) + debug_msg("*** Disconnecting socket {}".format(socket_num), self._debug) self.write_sncr(socket_num, _CMD_SOCK_DISCON) self.read_sncr(socket_num) @@ -905,8 +893,7 @@ def socket_read(self, socket_num: int, length: int) -> Tuple[int, bytes]: # Check if there is data available on the socket resp = b"" ret = self._get_rx_rcv_size(socket_num) - if self._debug: - print("Bytes avail. on sock: ", ret) + debug_msg("Bytes avail. on sock: {}".format(ret), self._debug) if ret == 0: # no data on socket? status = self._read_snmr(socket_num) @@ -919,8 +906,7 @@ def socket_read(self, socket_num: int, length: int) -> Tuple[int, bytes]: # set ret to the length of buffer ret = length if ret > 0: - if self._debug: - print("\t * Processing {} bytes of data".format(ret)) + debug_msg("* Processing {} bytes of data".format(ret), self._debug) # Read the starting save address of the received data ptr = self._read_snrx_rd(socket_num) From fe302daa084eeab51deb0e8b770e9041d60fa4a7 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 23 Jan 2023 14:11:03 +1100 Subject: [PATCH 51/80] DHCP client makes a new connection. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 4 +--- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 608c96a..5e00eec 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -936,7 +936,7 @@ def socket_read(self, socket_num: int, length: int) -> Tuple[int, bytes]: # Notify the W5k of the updated Sn_Rx_RD self.write_sncr(socket_num, _CMD_SOCK_RECV) - while self.read_sncr(socket_num): + while self.read_sncr(socket_num)[0] & _CMD_SOCK_RECV: time.sleep(0.0001) return ret, resp @@ -952,7 +952,6 @@ def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: If the read was unsuccessful then (0, b"") is returned. """ if self.udp_datasize[socket_num] > 0: - print("+ bytes on socket.") if self.udp_datasize[socket_num] <= length: ret, resp = self.socket_read(socket_num, self.udp_datasize[socket_num]) else: @@ -961,7 +960,6 @@ def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: self.socket_read(socket_num, self.udp_datasize[socket_num] - length) self.udp_datasize[socket_num] = 0 return ret, resp - print("+ No bytes on socket.") return 0, b"" def socket_write( diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 30da957..02f0bab 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -61,6 +61,8 @@ _MAGIC_COOKIE = b"c\x82Sc" # Four bytes 99.130.83.99 _MAX_DHCP_OPT = const(0x10) +_SNMR_UDP = const(0x02) + # Default DHCP Server port _DHCP_SERVER_PORT = const(67) # DHCP Lease Time, in seconds @@ -294,12 +296,12 @@ def _receive_dhcp_response(self, timeout: float) -> int: bytes_read = 0 debug_msg("+ Beginning to receive…", self._debug) while bytes_read <= minimum_packet_length and time.monotonic() < timeout: - x = self._eth.read_udp(self._wiz_sock, _BUFF_LENGTH - bytes_read)[1] - buffer.extend(x) - bytes_read = len(buffer) - debug_msg("+ Bytes read so far {}".format(bytes_read), self._debug) - debug_msg(x, self._debug) - + if self._eth.socket_available(self._wiz_sock, _SNMR_UDP): + x = self._eth.read_udp(self._wiz_sock, _BUFF_LENGTH - bytes_read)[1] + buffer.extend(x) + bytes_read = len(buffer) + debug_msg("+ Bytes read so far {}".format(bytes_read), self._debug) + debug_msg(x, self._debug) if bytes_read == _BUFF_LENGTH: break debug_msg("Received {} bytes".format(bytes_read), self._debug) @@ -368,7 +370,12 @@ def _handle_dhcp_message(self) -> int: ) for attempt in range(4): # Initial attempt plus 3 retries. message_length = self._generate_dhcp_message(message_type=msg_type_out) - self._eth.write_sndipr(self._wiz_sock, self.dhcp_server_ip) + + if self._renew: + dhcp_server_address = self.dhcp_server_ip + else: + dhcp_server_address = _BROADCAST_SERVER_ADDR + self._eth.write_sndipr(self._wiz_sock, dhcp_server_address) self._eth.write_sndport(self._wiz_sock, _DHCP_SERVER_PORT) self._eth.socket_write(self._wiz_sock, _BUFF[:message_length]) next_resend = self._next_retry_time(attempt=attempt) From 56d2ad01bee689217dda1a5be0a0f63c9401a283 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 24 Jan 2023 18:23:10 +1100 Subject: [PATCH 52/80] Restored stashed changes. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 5e00eec..a2711b5 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -952,6 +952,7 @@ def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: If the read was unsuccessful then (0, b"") is returned. """ if self.udp_datasize[socket_num] > 0: + print("+ bytes on socket.") if self.udp_datasize[socket_num] <= length: ret, resp = self.socket_read(socket_num, self.udp_datasize[socket_num]) else: @@ -960,6 +961,7 @@ def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: self.socket_read(socket_num, self.udp_datasize[socket_num] - length) self.udp_datasize[socket_num] = 0 return ret, resp + print("+ No bytes on socket.") return 0, b"" def socket_write( From 05ba882d95603ef242dcec88f2a899b870e58a8e Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 25 Jan 2023 08:21:14 +1100 Subject: [PATCH 53/80] Refactored DHCP to store IP addresses as byte objects instead of tuples. --- adafruit_wiznet5k/__init__.py | 6 ++-- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 14 ++++----- tests/test_dhcp_helper_functions.py | 32 ++++++++++----------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/adafruit_wiznet5k/__init__.py b/adafruit_wiznet5k/__init__.py index 841ffd9..7c04439 100644 --- a/adafruit_wiznet5k/__init__.py +++ b/adafruit_wiznet5k/__init__.py @@ -18,10 +18,10 @@ def debug_msg( message: Union[Exception, str, bytes, bytearray], debugging: bool ) -> None: """ - Helper function to print debugging messages. + 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. 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: diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 02f0bab..0c40c6e 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -603,7 +603,7 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: data_length = _BUFF[pointer] pointer += 1 data_end = pointer + data_length - option_data = _BUFF[pointer:data_end] + option_data = bytes(_BUFF[pointer:data_end]) return data_end, option_type, option_data debug_msg("Parsing DHCP message.", self._debug) @@ -614,14 +614,14 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: xid = _BUFF[4:8] if xid != self._transaction_id.to_bytes(4, "big"): raise ValueError("DHCP response ID mismatch.") - # Set the IP address to Claddr - self.local_ip = tuple(_BUFF[16:20]) # Check that there is a client ID. if _BUFF[28:34] == b"\x00\x00\x00\x00\x00\x00": raise ValueError("No client hardware MAC address in the response.") # Check for the magic cookie. if _BUFF[236:240] != _MAGIC_COOKIE: raise ValueError("No DHCP Magic Cookie in the response.") + # Set the IP address to Claddr + self.local_ip = bytes(_BUFF[16:20]) # Parse options msg_type = None @@ -631,15 +631,15 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: if data_type == _MSG_TYPE: msg_type = data[0] elif data_type == _SUBNET_MASK: - self.subnet_mask = tuple(data) + self.subnet_mask = data elif data_type == _DHCP_SERVER_ID: - self.dhcp_server_ip = tuple(data) + self.dhcp_server_ip = data elif data_type == _LEASE_TIME: self._lease_time = int.from_bytes(data, "big") elif data_type == _ROUTERS_ON_SUBNET: - self.gateway_ip = tuple(data[:4]) + self.gateway_ip = data[:4] elif data_type == _DNS_SERVERS: - self.dns_server_ip = tuple(data[:4]) + self.dns_server_ip = data[:4] elif data_type == _T1_VAL: self._t1 = int.from_bytes(data, "big") elif data_type == _T2_VAL: diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index cdf5657..cc61ab8 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -110,8 +110,8 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): 23.4, False, False, - (0, 0, 0, 0), - (0, 0, 0, 0), + b"\x00\x00\x00\x00", + b"\x00\x00\x00\x00", dhcp_data.DHCP_SEND_02, ), ( @@ -121,8 +121,8 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): 35.5, True, True, - (192, 168, 3, 4), - (222, 123, 23, 10), + b"\xc0\xa8\x03\x04", + b"\xe0\x7b\x17\x0a", dhcp_data.DHCP_SEND_03, ), ( @@ -132,8 +132,8 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): 35.5, False, True, - (10, 10, 10, 43), - (145, 66, 45, 22), + b"\x0a\x0a\x0a\x2b", + b"\x91\x42\x2d\x16", dhcp_data.DHCP_SEND_04, ), ), @@ -229,12 +229,12 @@ class TestParseDhcpMessage: ( ( 0x7FFFFFFF, - (192, 168, 5, 22), + b"\xc0\xa8\x05\x16", 2, - (192, 168, 6, 2), - (234, 111, 222, 123), - (121, 121, 4, 5), - (5, 6, 7, 8), + b"\xc0\xa8\x06\x02", + b"\xeao\xde{", + b"yy\x04\x05", + b"\x05\x06\x07\x08", 65792, 2236928, 3355392, @@ -242,12 +242,12 @@ class TestParseDhcpMessage: ), ( 0x3456789A, - (18, 36, 64, 10), + b"\x12$@\n", 5, - (10, 11, 7, 222), - (122, 78, 145, 3), - (10, 11, 14, 15), - (19, 17, 11, 7), + b"\n\x0b\x07\xde", + b"zN\x91\x03", + b"\n\x0b\x0e\x0f", + b"\x13\x11\x0b\x07", 15675, 923456, 43146718, From 9a68fd4430839771bd1089c8c8f32b9568983519 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 25 Jan 2023 13:41:54 +1100 Subject: [PATCH 54/80] Refactored DHCP to only accept a MAC address as a bytes object. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 12 +-- tests/test_dhcp_helper_functions.py | 87 +++++++++++++-------- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 0c40c6e..2760565 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -17,7 +17,7 @@ from __future__ import annotations try: - from typing import TYPE_CHECKING, Optional, Union, Tuple, Sequence + from typing import TYPE_CHECKING, Optional, Union, Tuple if TYPE_CHECKING: from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K @@ -122,14 +122,14 @@ class DHCP: def __init__( self, eth: WIZNET5K, - mac_address: Sequence[Union[int, bytes]], + mac_address: bytes, hostname: Optional[str] = None, response_timeout: float = 30.0, debug: bool = False, ) -> None: """ :param adafruit_wiznet5k.WIZNET5K eth: Wiznet 5k object - :param Sequence[Union[int, bytes]] mac_address: Hardware MAC address. + :param bytes mac_address: Hardware MAC address. :param Optional[str] hostname: The desired hostname, with optional {} to fill in the MAC address, defaults to None. :param float response_timeout: DHCP Response timeout in seconds, defaults to 30. @@ -139,9 +139,11 @@ def __init__( debug_msg("Initialising DHCP instance.", self._debug) self._response_timeout = response_timeout + if not isinstance(mac_address, bytes): + raise TypeError("MAC address must be a bytes object.") # Prevent buffer overrun in send_dhcp_message() if len(mac_address) != 6: - raise ValueError("The MAC address must be 6 bytes.") + raise ValueError("MAC address must be 6 bytes.") self._mac_address = mac_address # Set socket interface @@ -545,7 +547,7 @@ def option_writer( pointer = option_writer( offset=pointer, option_code=61, - option_data=tuple(b"\x01" + bytes(self._mac_address)), + option_data=b"\x01" + self._mac_address, ) # Request subnet mask, router and DNS server. diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index cc61ab8..8fb9d8e 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -22,7 +22,7 @@ def mock_wiznet5k(mocker): @pytest.fixture def mock_dhcp(mock_wiznet5k): - dhcp = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) return dhcp @@ -33,9 +33,9 @@ def test_constants(self): @pytest.mark.parametrize( "mac_address", ( - [1, 2, 3, 4, 5, 6], - (7, 8, 9, 10, 11, 12), - bytes([1, 2, 4, 6, 7, 8]), + bytes((1, 2, 3, 4, 5, 6)), + bytes((7, 8, 9, 10, 11, 12)), + bytes((1, 2, 4, 6, 7, 8)), ), ) def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mac_address): @@ -70,7 +70,7 @@ def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mac_address): def test_dhcp_setup_other_args(self, mock_wiznet5k): """Test instantiating DHCP with none default values.""" - mac_address = (7, 8, 9, 10, 11, 12) + mac_address = bytes((7, 8, 9, 10, 11, 12)) dhcp_client = wiz_dhcp.DHCP( mock_wiznet5k, mac_address, @@ -86,13 +86,32 @@ def test_dhcp_setup_other_args(self, mock_wiznet5k): "fred.com".split(".", maxsplit=1)[0].format(mac_string)[:42], "utf-8" ) + @pytest.mark.parametrize( + "mac_address, error_type", + ( + ("fdsafa", TypeError), + ((1, 2, 3, 4, 5, 6), TypeError), + (b"12345", ValueError), + (b"1234567", ValueError), + ), + ) + def test_mac_address_checking(self, mock_wiznet5k, mac_address, error_type): + with pytest.raises(error_type): + wiz_dhcp.DHCP( + mock_wiznet5k, + mac_address, + hostname="fred.com", + response_timeout=25.0, + debug=True, + ) + @freeze_time("2022-10-20") class TestSendDHCPMessage: def test_generate_message_with_default_attributes(self, mock_wiznet5k): """Test the _generate_message function with default values.""" assert len(wiz_dhcp._BUFF) == 318 - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (4, 5, 6, 7, 8, 9)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((4, 5, 6, 7, 8, 9))) dhcp_client._transaction_id = 0x6FFFFFFF dhcp_client._start_time = time.monotonic() - 23.4 dhcp_client._generate_dhcp_message(message_type=wiz_dhcp._DHCP_DISCOVER) @@ -104,7 +123,7 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): broadcast_only, local_ip, server_ip, result", ( ( - (4, 5, 6, 7, 8, 9), + bytes((4, 5, 6, 7, 8, 9)), None, wiz_dhcp._DHCP_DISCOVER, 23.4, @@ -115,7 +134,7 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): dhcp_data.DHCP_SEND_02, ), ( - (24, 35, 46, 57, 68, 79), + bytes((24, 35, 46, 57, 68, 79)), "bert.co.uk", wiz_dhcp._DHCP_DISCOVER, 35.5, @@ -126,7 +145,7 @@ def test_generate_message_with_default_attributes(self, mock_wiznet5k): dhcp_data.DHCP_SEND_03, ), ( - (255, 97, 36, 101, 42, 99), + bytes((255, 97, 36, 101, 42, 99)), "clash.net", wiz_dhcp._DHCP_DISCOVER, 35.5, @@ -173,25 +192,25 @@ def test_generate_dhcp_message_discover_with_non_defaults( broadcast_only, local_ip, server_ip, result", ( ( - (255, 97, 36, 101, 42, 99), + bytes((255, 97, 36, 101, 42, 99)), "helicopter.org", wiz_dhcp._DHCP_REQUEST, 16.3, False, True, - (10, 10, 10, 43), - (145, 66, 45, 22), + bytes((10, 10, 10, 43)), + bytes((145, 66, 45, 22)), dhcp_data.DHCP_SEND_05, ), ( - (75, 63, 166, 4, 200, 101), + bytes((75, 63, 166, 4, 200, 101)), None, wiz_dhcp._DHCP_REQUEST, 72.4, False, True, - (100, 101, 102, 4), - (245, 166, 5, 11), + bytes((100, 101, 102, 4)), + bytes((245, 166, 5, 11)), dhcp_data.DHCP_SEND_06, ), ), @@ -272,7 +291,7 @@ def test_parse_good_data( response, ): wiz_dhcp._BUFF = response - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) dhcp_client._transaction_id = xid response_type = dhcp_client._parse_dhcp_response() assert response_type == msg_type @@ -288,7 +307,7 @@ def test_parse_good_data( def test_parsing_failures(self, mock_wiznet5k): # Test for bad OP code, ID mismatch, no server ID, bad Magic Cookie bad_data = dhcp_data.BAD_DATA - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) dhcp_client._eth._read_socket.return_value = (len(bad_data), bad_data) # Transaction ID mismatch. dhcp_client._transaction_id = 0x42424242 @@ -313,13 +332,13 @@ def test_parsing_failures(self, mock_wiznet5k): @freeze_time("2022-11-10") def test_dsm_reset(mocker, mock_wiznet5k): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) mocker.patch.object(dhcp_client, "_dhcp_connection_setup", autospec=True) mocker.patch.object(dhcp_client, "_socket_release", autospec=True) - dhcp_client.dhcp_server_ip = (1, 2, 3, 4) - dhcp_client.local_ip = (2, 3, 4, 5) - dhcp_client.subnet_mask = (3, 4, 5, 6) - dhcp_client.dns_server_ip = (7, 8, 8, 10) + dhcp_client.dhcp_server_ip = bytes((1, 2, 3, 4)) + dhcp_client.local_ip = bytes((2, 3, 4, 5)) + dhcp_client.subnet_mask = bytes((3, 4, 5, 6)) + dhcp_client.dns_server_ip = bytes((7, 8, 8, 10)) dhcp_client._renew = True dhcp_client._retries = 4 dhcp_client._transaction_id = 3 @@ -345,7 +364,7 @@ def test_dsm_reset(mocker, mock_wiznet5k): class TestSocketRelease: def test_socket_set_to_none(self, mock_wiznet5k): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) dhcp_client._socket_release() assert dhcp_client._wiz_sock is None @@ -356,7 +375,7 @@ def test_socket_set_to_none(self, mock_wiznet5k): class TestSmallHelperFunctions: def test_increment_transaction_id(self, mock_wiznet5k): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) # Test that transaction_id increments. dhcp_client._transaction_id = 4 dhcp_client._increment_transaction_id() @@ -369,7 +388,7 @@ def test_increment_transaction_id(self, mock_wiznet5k): @freeze_time("2022-10-10") @pytest.mark.parametrize("rand_int", (-1, 0, 1)) def test_next_retry_time_default_attrs(self, mocker, mock_wiznet5k, rand_int): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", autospec=True, @@ -384,7 +403,7 @@ def test_next_retry_time_default_attrs(self, mocker, mock_wiznet5k, rand_int): @freeze_time("2022-10-10") @pytest.mark.parametrize("interval", (2, 7, 10)) def test_next_retry_time_optional_attrs(self, mocker, mock_wiznet5k, interval): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) mocker.patch( "adafruit_wiznet5k.adafruit_wiznet5k_dhcp.randint", autospec=True, @@ -401,7 +420,7 @@ def test_setup_socket_with_no_error(self, mocker, mock_wiznet5k): mocker.patch.object(mock_wiznet5k, "get_socket", return_value=2) mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=b"\x00") mocker.patch.object(mock_wiznet5k, "read_snsr", return_value=b"\x22") - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) dhcp_client._dhcp_connection_setup() mock_wiznet5k.get_socket.assert_called_once() mock_wiznet5k.write_snmr.assert_called_once_with(2, 0x02) @@ -418,7 +437,7 @@ def test_setup_socket_with_timeout_on_get_socket(self, mocker, mock_wiznet5k): mocker.patch.object(mock_wiznet5k, "get_socket", return_value=0xFF) mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=b"\x00") mocker.patch.object(mock_wiznet5k, "read_snsr", return_value=b"\x22") - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) with pytest.raises(RuntimeError): dhcp_client._dhcp_connection_setup() assert dhcp_client._wiz_sock is None @@ -428,7 +447,7 @@ def test_setup_socket_with_timeout_on_socket_is_udp(self, mocker, mock_wiznet5k) mocker.patch.object(mock_wiznet5k, "get_socket", return_value=2) mocker.patch.object(mock_wiznet5k, "read_sncr", return_value=b"\x00") mocker.patch.object(mock_wiznet5k, "read_snsr", return_value=b"\x21") - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) with pytest.raises(RuntimeError): dhcp_client._dhcp_connection_setup() assert dhcp_client._wiz_sock is None @@ -444,7 +463,7 @@ class TestHandleDhcpMessage: ) @freeze_time("2022-5-5") def test_good_data(self, mocker, mock_wiznet5k, fsm_state, msg_in): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) # Mock out methods to allow _handle_dhcp_message to run. mocker.patch.object( dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 @@ -486,7 +505,7 @@ def test_good_data(self, mocker, mock_wiznet5k, fsm_state, msg_in): @freeze_time("2022-5-5", auto_tick_seconds=1) def test_timeout_blocking_no_response(self, mocker, mock_wiznet5k): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) # Mock out methods to allow _handle_dhcp_message to run. mocker.patch.object( dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 @@ -520,7 +539,7 @@ def test_timeout_blocking_no_response(self, mocker, mock_wiznet5k): @freeze_time("2022-5-5", auto_tick_seconds=1) def test_timeout_blocking_bad_message(self, mocker, mock_wiznet5k): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) # Mock out methods to allow _handle_dhcp_message to run. mocker.patch.object( dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 @@ -560,7 +579,7 @@ def test_timeout_blocking_bad_message(self, mocker, mock_wiznet5k): def test_no_response_non_blocking_renewing( self, mocker, mock_wiznet5k, renew, blocking ): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) # Mock out methods to allow _handle_dhcp_message to run. mocker.patch.object( dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 @@ -599,7 +618,7 @@ def test_no_response_non_blocking_renewing( def test_bad_message_non_blocking_renewing( self, mocker, mock_wiznet5k, renew, blocking ): - dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, (1, 2, 3, 4, 5, 6)) + dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, bytes((1, 2, 3, 4, 5, 6))) # Mock out methods to allow _handle_dhcp_message to run. mocker.patch.object( dhcp_client, "_generate_dhcp_message", autospec=True, return_value=300 From fa52aab775b32dfe84bbddd9dcf08482fc8d2230 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 25 Jan 2023 13:45:14 +1100 Subject: [PATCH 55/80] Removed _DEFAULT_LEASE_TIME. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 2760565..4aae1c4 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -66,7 +66,6 @@ # Default DHCP Server port _DHCP_SERVER_PORT = const(67) # DHCP Lease Time, in seconds -_DEFAULT_LEASE_TIME = const(900) _BROADCAST_SERVER_ADDR = b"\xff\xff\xff\xff" # (255.255.255.255) _UNASSIGNED_IP_ADDR = b"\x00\x00\x00\x00" # (0.0.0.0) @@ -337,8 +336,6 @@ def _process_messaging_states(self, *, message_type: int): self._dhcp_state = _STATE_INIT elif message_type == _DHCP_ACK: debug_msg("Message is ACK, setting FSM state to BOUND.", self._debug) - if self._lease_time == 0: - self._lease_time = _DEFAULT_LEASE_TIME self._t1 = self._start_time + self._lease_time // 2 self._t2 = self._start_time + self._lease_time - self._lease_time // 8 self._lease_time += self._start_time From 9ec6b1632868b5509216b4b390fc42997ca6872c Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 25 Jan 2023 13:46:41 +1100 Subject: [PATCH 56/80] Updated tests to remove _DEFAULT_LEASE_TIME. --- tests/test_dhcp_helper_functions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index 8fb9d8e..bee7a4f 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -777,7 +777,7 @@ def test_called_from_selecting_good_message(self, mock_dhcp): assert mock_dhcp._dhcp_state == wiz_dhcp._STATE_REQUESTING @freeze_time("2022-3-4") - @pytest.mark.parametrize("lease_time", (0, 8000)) + @pytest.mark.parametrize("lease_time", (200, 8000)) def test_called_from_requesting_with_ack(self, mock_dhcp, lease_time): # Setup with the required state. mock_dhcp._dhcp_state = wiz_dhcp._STATE_REQUESTING @@ -790,8 +790,6 @@ def test_called_from_requesting_with_ack(self, mock_dhcp, lease_time): # Test. mock_dhcp._process_messaging_states(message_type=wiz_dhcp._DHCP_ACK) # Confirm timers are correctly set. - if lease_time == 0: - lease_time = wiz_dhcp._DEFAULT_LEASE_TIME assert mock_dhcp._t1 == time.monotonic() + lease_time // 2 assert mock_dhcp._t2 == time.monotonic() + lease_time - lease_time // 8 assert mock_dhcp._lease_time == time.monotonic() + lease_time From 53d2cfa44381a12d8a5e124499324eddd30b206f Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 25 Jan 2023 13:57:25 +1100 Subject: [PATCH 57/80] Removed unused response_timeout arg from DHCP and tests. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 9 +++------ tests/test_dhcp_helper_functions.py | 4 ---- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 4aae1c4..58ce944 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -103,8 +103,8 @@ class DHCP: methods in the WIZNET5K class. Since DHCP uses UDP, messages may be lost. The DHCP protocol uses exponential backoff - for retrying. Retries occur after 4, 8, and 16 seconds (the final retry is followed by - a wait of 32 seconds) so it will take about a minute to decide that no DHCP server + for retrying. Retries occur after 4, 8, and 16 +/- 1 seconds (the final retry is followed + by a wait of 32 seconds) so it takes about a minute to decide that no DHCP server is available. The DHCP client cannot check whether the allocated IP address is already in use because @@ -123,7 +123,6 @@ def __init__( eth: WIZNET5K, mac_address: bytes, hostname: Optional[str] = None, - response_timeout: float = 30.0, debug: bool = False, ) -> None: """ @@ -131,12 +130,10 @@ def __init__( :param bytes mac_address: Hardware MAC address. :param Optional[str] hostname: The desired hostname, with optional {} to fill in the MAC address, defaults to None. - :param float response_timeout: DHCP Response timeout in seconds, defaults to 30. :param bool debug: Enable debugging output. """ self._debug = debug - debug_msg("Initialising DHCP instance.", self._debug) - self._response_timeout = response_timeout + debug_msg("Initialising DHCP client instance.", self._debug) if not isinstance(mac_address, bytes): raise TypeError("MAC address must be a bytes object.") diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index bee7a4f..e7c4dc5 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -47,7 +47,6 @@ def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mac_address): mock_randint.return_value = 0x1234567 dhcp_client = wiz_dhcp.DHCP(mock_wiznet5k, mac_address) assert dhcp_client._eth == mock_wiznet5k - assert dhcp_client._response_timeout == 30.0 assert dhcp_client._debug is False assert dhcp_client._mac_address == mac_address assert dhcp_client._wiz_sock is None @@ -75,11 +74,9 @@ def test_dhcp_setup_other_args(self, mock_wiznet5k): mock_wiznet5k, mac_address, hostname="fred.com", - response_timeout=25.0, debug=True, ) - assert dhcp_client._response_timeout == 25.0 assert dhcp_client._debug is True mac_string = "".join("{:02X}".format(o) for o in mac_address) assert dhcp_client._hostname == bytes( @@ -101,7 +98,6 @@ def test_mac_address_checking(self, mock_wiznet5k, mac_address, error_type): mock_wiznet5k, mac_address, hostname="fred.com", - response_timeout=25.0, debug=True, ) From a3c71f9cf62d46c04f46654ff2e5497096555a29 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 25 Jan 2023 15:32:01 +1100 Subject: [PATCH 58/80] Refactored mac_address to return a bytes object and added error checking to the setter. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index ca1a078..c724ddd 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -328,21 +328,29 @@ def unpretty_ip( return bytes(octets) @property - def mac_address(self) -> bytearray: + def mac_address(self) -> bytes: """ Ethernet hardware's MAC address. :return bytearray: Six byte MAC address.""" - return self.read(_REG_SHAR, 0x00, 6) + return bytes(self.read(_REG_SHAR, 0x00, 6)) @mac_address.setter - def mac_address(self, address: Sequence[Union[int, bytes]]) -> None: + def mac_address(self, address: Tuple[int]) -> None: """ - Sets the hardware MAC address. + Set the hardware MAC address. - :param tuple address: Hardware MAC address. + :param Tuple address: A 6 byte hardware MAC address. + + :raises ValueError: If the MAC address in invalid """ - self.write(_REG_SHAR, 0x04, address) + # Check that the MAC is a valid 6 byte address. + if len(address) == 6 and False not in [ + (isinstance(x, int) and 0 <= x <= 255) for x in address + ]: + self.write(_REG_SHAR, 0x04, address) + else: + raise ValueError("Invalid MAC address.") def pretty_mac( self, From 468d8028bdbd0e9ef3fdcb93b017fee8b3b82983 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 25 Jan 2023 16:49:14 +1100 Subject: [PATCH 59/80] Removed spurious print statements. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 2 -- adafruit_wiznet5k/adafruit_wiznet5k_socket.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index c724ddd..d13fb30 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -952,7 +952,6 @@ def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: If the read was unsuccessful then (0, b"") is returned. """ if self.udp_datasize[socket_num] > 0: - print("+ bytes on socket.") if self.udp_datasize[socket_num] <= length: ret, resp = self.socket_read(socket_num, self.udp_datasize[socket_num]) else: @@ -961,7 +960,6 @@ def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: self.socket_read(socket_num, self.udp_datasize[socket_num] - length) self.udp_datasize[socket_num] = 0 return ret, resp - print("+ No bytes on socket.") return 0, b"" def socket_write( diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_socket.py b/adafruit_wiznet5k/adafruit_wiznet5k_socket.py index ad327c7..76f8677 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_socket.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_socket.py @@ -466,7 +466,6 @@ def embed_recv( :return bytes: All data available from the connection. """ - # print("Socket read", bufsize) ret = None avail = self.available() if avail: @@ -476,7 +475,6 @@ def embed_recv( self._buffer += _the_interface.read_udp(self.socknum, avail)[1] gc.collect() ret = self._buffer - # print("RET ptr:", id(ret), id(self._buffer)) self._buffer = b"" gc.collect() return ret From 1f286bf93b667d1be4b00b13bcc372656f7a8c04 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 25 Jan 2023 22:12:09 +1100 Subject: [PATCH 60/80] Added a gc.collect to end of FSM. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 58ce944..b74f1dc 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -461,6 +461,7 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._debug, ) self._dhcp_state = _STATE_BOUND + gc.collect() def _generate_dhcp_message( self, From d2a808c36f18e80372523c90bf6585a891178044 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 28 Jan 2023 18:28:48 +0700 Subject: [PATCH 61/80] Improved hexdump for debug_msg function. --- adafruit_wiznet5k/__init__.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/adafruit_wiznet5k/__init__.py b/adafruit_wiznet5k/__init__.py index 7c04439..3801895 100644 --- a/adafruit_wiznet5k/__init__.py +++ b/adafruit_wiznet5k/__init__.py @@ -26,16 +26,25 @@ def debug_msg( """ if debugging: if isinstance(message, (bytes, bytearray)): - temp = "" - for index, value in enumerate(message): - if not index % 16: - temp += "\n" - elif not index % 8: - temp += " " - else: - temp += " " - temp += "{:02x}".format(value) - message = temp + message = _hexdump(message) print(message) del message gc.collect() + + +def _hexdump(src: bytes, length: int = 16): + """ + Create a hexdump of a bytes object. + + :param bytes src: The bytes object to hexdump. + :param int length: The number of bytes per line of the hexdump. Defaults to 16. + + :returns str: The hexdump. + """ + result = [] + for i in range(0, len(src), length): + chunk = src[i : i + length] + hexa = " ".join(("%0*X" % (2, x) for x in chunk)) + text = "".join((chr(x) if 0x20 <= x < 0x7F else "." for x in chunk)) + result.append("%04X %-*s %s" % (i, length * (2 + 1), hexa, text)) + return "\n".join(result) From 08baf1c4758b0c3511ab28fa3fa8173f4f52578e Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 2 Feb 2023 05:02:22 +0300 Subject: [PATCH 62/80] Replaced %S with .format in hexdump. --- adafruit_wiznet5k/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/adafruit_wiznet5k/__init__.py b/adafruit_wiznet5k/__init__.py index 3801895..bf1635b 100644 --- a/adafruit_wiznet5k/__init__.py +++ b/adafruit_wiznet5k/__init__.py @@ -32,19 +32,18 @@ def debug_msg( gc.collect() -def _hexdump(src: bytes, length: int = 16): +def _hexdump(src: bytes): """ - Create a hexdump of a bytes object. + Create a 16 column hexdump of a bytes object. :param bytes src: The bytes object to hexdump. - :param int length: The number of bytes per line of the hexdump. Defaults to 16. :returns str: The hexdump. """ result = [] - for i in range(0, len(src), length): - chunk = src[i : i + length] - hexa = " ".join(("%0*X" % (2, x) for x in chunk)) + 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 %-*s %s" % (i, length * (2 + 1), hexa, text)) + result.append("{:04x} {:<48} {}".format(i, hexa, text)) return "\n".join(result) From 8bc470ef52eac96928354cc1130229e1905391eb Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Fri, 3 Feb 2023 22:39:17 +0300 Subject: [PATCH 63/80] Tidied some doc-strings. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index b74f1dc..5951339 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -172,13 +172,20 @@ def __init__( ) def request_dhcp_lease(self) -> bool: - """Request to renew or acquire a DHCP lease.""" + """ + Request acquire a DHCP lease. + + :returns bool: A lease has been acquired. + """ debug_msg("Requesting DHCP lease.", self._debug) self._dhcp_state_machine(blocking=True) return self._dhcp_state == _STATE_BOUND def maintain_dhcp_lease(self, blocking: bool = False) -> None: - """Maintain DHCP lease""" + """ + Maintain a DHCP lease. + :param bool blocking: Run the DHCP FSM in blocking mode. + """ debug_msg("Maintaining lease with blocking = {}".format(blocking), self._debug) self._dhcp_state_machine(blocking=blocking) From a89bd7fdb60bd06011871e713aaa632e5e1c5604 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 6 Feb 2023 21:22:39 +0300 Subject: [PATCH 64/80] Added code to assign the ifconfig data to WIZNET5K and to use self._renew in message generation. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 13 +++++++++---- tests/test_dhcp_helper_functions.py | 5 ++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 5951339..c659764 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -184,7 +184,7 @@ def request_dhcp_lease(self) -> bool: def maintain_dhcp_lease(self, blocking: bool = False) -> None: """ Maintain a DHCP lease. - :param bool blocking: Run the DHCP FSM in blocking mode. + :param bool blocking: Run the DHCP FSM in non-blocking mode. """ debug_msg("Maintaining lease with blocking = {}".format(blocking), self._debug) self._dhcp_state_machine(blocking=blocking) @@ -344,6 +344,13 @@ def _process_messaging_states(self, *, message_type: int): self._t2 = self._start_time + self._lease_time - self._lease_time // 8 self._lease_time += self._start_time self._increment_transaction_id() + if not self._renew: + self._eth.ifconfig = ( + self.local_ip, + self.subnet_mask, + self.gateway_ip, + self.dns_server_ip, + ) self._renew = False self._dhcp_state = _STATE_BOUND @@ -475,7 +482,6 @@ def _generate_dhcp_message( *, message_type: int, broadcast: bool = False, - renew: bool = False, ) -> int: """ Assemble a DHCP message. The content will vary depending on which type of @@ -484,7 +490,6 @@ def _generate_dhcp_message( :param int message_type: Type of message to generate. :param bool broadcast: Used to set the flag requiring a broadcast reply from the DHCP server. Defaults to False which matches the DHCP standard. - :param bool renew: Set True for renewing and rebinding operations, defaults to False. :returns int: The length of the DHCP message. """ @@ -526,7 +531,7 @@ def option_writer( _BUFF[10] = 0b10000000 else: _BUFF[10] = 0b00000000 - if renew: + if self._renew: _BUFF[12:16] = bytes(self.local_ip) # chaddr _BUFF[28:34] = self._mac_address diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index e7c4dc5..e77b7e2 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -174,10 +174,10 @@ def test_generate_dhcp_message_discover_with_non_defaults( dhcp_client.dhcp_server_ip = server_ip dhcp_client._transaction_id = 0x6FFFFFFF dhcp_client._start_time = time.monotonic() - time_elapsed + dhcp_client._renew = renew # Test dhcp_client._generate_dhcp_message( message_type=msg_type, - renew=renew, broadcast=broadcast_only, ) assert len(wiz_dhcp._BUFF) == 318 @@ -218,7 +218,6 @@ def test_generate_dhcp_message_with_request_options( hostname, msg_type, time_elapsed, - renew, broadcast_only, local_ip, server_ip, @@ -232,7 +231,7 @@ def test_generate_dhcp_message_with_request_options( dhcp_client._start_time = time.monotonic() - time_elapsed # Test dhcp_client._generate_dhcp_message( - message_type=msg_type, renew=renew, broadcast=broadcast_only + message_type=msg_type, broadcast=broadcast_only ) assert len(wiz_dhcp._BUFF) == 318 assert wiz_dhcp._BUFF == result From 94e7ad309e3cfafa8eea49d6dc1448292b69758f Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Mon, 6 Feb 2023 21:58:33 +0300 Subject: [PATCH 65/80] Added a return to FSM for when renewing fails. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index c659764..8fddb1f 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -475,6 +475,7 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: self._debug, ) self._dhcp_state = _STATE_BOUND + return gc.collect() def _generate_dhcp_message( From 29727c3cd3e9bf9211e05d4bcfa52542a478f13b Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 9 Feb 2023 22:40:24 +0300 Subject: [PATCH 66/80] Refactored self._renew to None, "renew" or "rebind" to control content of DHCP request messages. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 17 +++++++++-------- tests/test_dhcp_helper_functions.py | 18 ++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 8fddb1f..6efafd4 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -151,7 +151,7 @@ def __init__( self._transaction_id = randint(1, 0x7FFFFFFF) self._start_time = 0.0 self._blocking = False - self._renew = False + self._renew = None # DHCP binding configuration self.dhcp_server_ip = _BROADCAST_SERVER_ADDR @@ -206,7 +206,7 @@ def _dsm_reset(self) -> None: self.local_ip = _UNASSIGNED_IP_ADDR self.subnet_mask = _UNASSIGNED_IP_ADDR self.dns_server_ip = _UNASSIGNED_IP_ADDR - self._renew = False + self._renew = None self._increment_transaction_id() self._start_time = time.monotonic() @@ -351,7 +351,7 @@ def _process_messaging_states(self, *, message_type: int): self.gateway_ip, self.dns_server_ip, ) - self._renew = False + self._renew = None self._dhcp_state = _STATE_BOUND def _handle_dhcp_message(self) -> int: @@ -443,14 +443,14 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: if self._dhcp_state == _STATE_RENEWING: debug_msg("FSM state is RENEWING.", self._debug) - self._renew = True + self._renew = "renew" self._dhcp_connection_setup() self._start_time = time.monotonic() self._dhcp_state = _STATE_REQUESTING if self._dhcp_state == _STATE_REBINDING: debug_msg("FSM state is REBINDING.", self._debug) - self._renew = True + self._renew = "rebind" self.dhcp_server_ip = _BROADCAST_SERVER_ADDR self._dhcp_connection_setup() self._start_time = time.monotonic() @@ -572,9 +572,10 @@ def option_writer( offset=pointer, option_code=50, option_data=self.local_ip ) # Set Server ID to chosen DHCP server IP address. - pointer = option_writer( - offset=pointer, option_code=54, option_data=self.dhcp_server_ip - ) + if self._renew != "rebind": + pointer = option_writer( + offset=pointer, option_code=54, option_data=self.dhcp_server_ip + ) _BUFF[pointer] = 0xFF pointer += 1 diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index e77b7e2..d5b7ae1 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -184,7 +184,7 @@ def test_generate_dhcp_message_discover_with_non_defaults( assert wiz_dhcp._BUFF == result @pytest.mark.parametrize( - "mac_address, hostname, msg_type, time_elapsed, renew, \ + "mac_address, hostname, msg_type, time_elapsed, \ broadcast_only, local_ip, server_ip, result", ( ( @@ -192,7 +192,6 @@ def test_generate_dhcp_message_discover_with_non_defaults( "helicopter.org", wiz_dhcp._DHCP_REQUEST, 16.3, - False, True, bytes((10, 10, 10, 43)), bytes((145, 66, 45, 22)), @@ -203,7 +202,6 @@ def test_generate_dhcp_message_discover_with_non_defaults( None, wiz_dhcp._DHCP_REQUEST, 72.4, - False, True, bytes((100, 101, 102, 4)), bytes((245, 166, 5, 11)), @@ -352,7 +350,7 @@ def test_dsm_reset(mocker, mock_wiznet5k): assert dhcp_client.local_ip == wiz_dhcp._UNASSIGNED_IP_ADDR assert dhcp_client.subnet_mask == wiz_dhcp._UNASSIGNED_IP_ADDR assert dhcp_client.dns_server_ip == wiz_dhcp._UNASSIGNED_IP_ADDR - assert dhcp_client._renew is False + assert dhcp_client._renew is None assert dhcp_client._transaction_id == 4 assert dhcp_client._start_time == time.monotonic() @@ -569,7 +567,7 @@ def test_timeout_blocking_bad_message(self, mocker, mock_wiznet5k): @freeze_time("2022-5-5") @pytest.mark.parametrize( - "renew, blocking", ((True, False), (True, True), (False, False)) + "renew, blocking", (("renew", False), ("renew", True), (None, False)) ) def test_no_response_non_blocking_renewing( self, mocker, mock_wiznet5k, renew, blocking @@ -608,7 +606,7 @@ def test_no_response_non_blocking_renewing( @freeze_time("2022-5-5") @pytest.mark.parametrize( - "renew, blocking", ((True, False), (True, True), (False, False)) + "renew, blocking", (("renew", False), ("renew", True), (None, False)) ) def test_bad_message_non_blocking_renewing( self, mocker, mock_wiznet5k, renew, blocking @@ -778,8 +776,8 @@ def test_called_from_requesting_with_ack(self, mock_dhcp, lease_time): mock_dhcp._dhcp_state = wiz_dhcp._STATE_REQUESTING # Set the lease_time (zero forces a default to be used). mock_dhcp._lease_time = lease_time - # Set renew to True to confirm that an ACK sets it to False. - mock_dhcp._renew = True + # Set renew to "renew" to confirm that an ACK sets it to None. + mock_dhcp._renew = "renew" # Set a start time. mock_dhcp._start_time = time.monotonic() # Test. @@ -788,8 +786,8 @@ def test_called_from_requesting_with_ack(self, mock_dhcp, lease_time): assert mock_dhcp._t1 == time.monotonic() + lease_time // 2 assert mock_dhcp._t2 == time.monotonic() + lease_time - lease_time // 8 assert mock_dhcp._lease_time == time.monotonic() + lease_time - # Check that renew is forced to False - assert mock_dhcp._renew is False + # Check that renew is forced to None + assert mock_dhcp._renew is None # FSM state should be bound. assert mock_dhcp._dhcp_state == wiz_dhcp._STATE_BOUND From 895a0e02436cb8924187151afa876f8ecb494a14 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 9 Feb 2023 23:16:01 +0300 Subject: [PATCH 67/80] Refactored self._lease_time to self._lease. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 16 ++++++++-------- tests/test_dhcp_helper_functions.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 6efafd4..824b833 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -160,10 +160,10 @@ def __init__( self.subnet_mask = _UNASSIGNED_IP_ADDR self.dns_server_ip = _UNASSIGNED_IP_ADDR - # Lease configuration - self._lease_time = 0 + # Lease expiry times self._t1 = 0 self._t2 = 0 + self._lease = 0 # Host name mac_string = "".join("{:02X}".format(o) for o in mac_address) @@ -340,9 +340,9 @@ def _process_messaging_states(self, *, message_type: int): self._dhcp_state = _STATE_INIT elif message_type == _DHCP_ACK: debug_msg("Message is ACK, setting FSM state to BOUND.", self._debug) - self._t1 = self._start_time + self._lease_time // 2 - self._t2 = self._start_time + self._lease_time - self._lease_time // 8 - self._lease_time += self._start_time + self._t1 = self._start_time + self._lease // 2 + self._t2 = self._start_time + self._lease - self._lease // 8 + self._lease += self._start_time self._increment_transaction_id() if not self._renew: self._eth.ifconfig = ( @@ -424,7 +424,7 @@ def _dhcp_state_machine(self, *, blocking: bool = False) -> None: debug_msg("No timers have expired. Exiting FSM.", self._debug) self._socket_release() return - if now > self._lease_time: + if now > self._lease: debug_msg( "Lease has expired, switching state to INIT.", self._debug ) @@ -646,7 +646,7 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: elif data_type == _DHCP_SERVER_ID: self.dhcp_server_ip = data elif data_type == _LEASE_TIME: - self._lease_time = int.from_bytes(data, "big") + self._lease = int.from_bytes(data, "big") elif data_type == _ROUTERS_ON_SUBNET: self.gateway_ip = data[:4] elif data_type == _DNS_SERVERS: @@ -669,7 +669,7 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: self.local_ip, self._t1, self._t2, - self._lease_time, + self._lease, ), self._debug, ) diff --git a/tests/test_dhcp_helper_functions.py b/tests/test_dhcp_helper_functions.py index d5b7ae1..0a7134e 100644 --- a/tests/test_dhcp_helper_functions.py +++ b/tests/test_dhcp_helper_functions.py @@ -59,7 +59,7 @@ def test_dhcp_setup_default(self, mocker, mock_wiznet5k, mac_address): assert dhcp_client.gateway_ip == wiz_dhcp._UNASSIGNED_IP_ADDR assert dhcp_client.subnet_mask == wiz_dhcp._UNASSIGNED_IP_ADDR assert dhcp_client.dns_server_ip == wiz_dhcp._UNASSIGNED_IP_ADDR - assert dhcp_client._lease_time == 0 + assert dhcp_client._lease == 0 assert dhcp_client._t1 == 0 assert dhcp_client._t2 == 0 mac_string = "".join("{:02X}".format(o) for o in mac_address) @@ -293,7 +293,7 @@ def test_parse_good_data( assert dhcp_client.dhcp_server_ip == dhcp_ip assert dhcp_client.gateway_ip == gate_ip assert dhcp_client.dns_server_ip == dns_ip - assert dhcp_client._lease_time == lease + assert dhcp_client._lease == lease assert dhcp_client._t1 == t1 assert dhcp_client._t2 == t2 @@ -775,7 +775,7 @@ def test_called_from_requesting_with_ack(self, mock_dhcp, lease_time): # Setup with the required state. mock_dhcp._dhcp_state = wiz_dhcp._STATE_REQUESTING # Set the lease_time (zero forces a default to be used). - mock_dhcp._lease_time = lease_time + mock_dhcp._lease = lease_time # Set renew to "renew" to confirm that an ACK sets it to None. mock_dhcp._renew = "renew" # Set a start time. @@ -785,7 +785,7 @@ def test_called_from_requesting_with_ack(self, mock_dhcp, lease_time): # Confirm timers are correctly set. assert mock_dhcp._t1 == time.monotonic() + lease_time // 2 assert mock_dhcp._t2 == time.monotonic() + lease_time - lease_time // 8 - assert mock_dhcp._lease_time == time.monotonic() + lease_time + assert mock_dhcp._lease == time.monotonic() + lease_time # Check that renew is forced to None assert mock_dhcp._renew is None # FSM state should be bound. From 0fd0fa27ee50ce6ef96e6bbd55a3f8c166df200c Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Fri, 10 Feb 2023 22:09:49 +0300 Subject: [PATCH 68/80] Changed self._sock.recv() back to self._sock.recv(512) in wiznet5k_dns.py 276. --- adafruit_wiznet5k/adafruit_wiznet5k_dns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dns.py b/adafruit_wiznet5k/adafruit_wiznet5k_dns.py index 6940f98..b633276 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dns.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dns.py @@ -273,7 +273,7 @@ def gethostbyname(self, hostname: bytes) -> Union[int, bytes]: return -1 time.sleep(0.05) # recv packet into buf - buffer = self._sock.recv() + buffer = self._sock.recv(512) _debug_print( debug=self._debug, message="DNS Packet Received: {}".format(buffer), From 793e93fa1a56e7e06382edc7a688256acbe7d918 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Fri, 10 Feb 2023 22:24:59 +0300 Subject: [PATCH 69/80] Moved debug_msg() from init.py to adafruit_wiznet5k_debug.py. --- adafruit_wiznet5k/__init__.py | 49 -------------------- adafruit_wiznet5k/adafruit_wiznet5k.py | 2 +- adafruit_wiznet5k/adafruit_wiznet5k_debug.py | 49 ++++++++++++++++++++ adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 4 +- 4 files changed, 53 insertions(+), 51 deletions(-) create mode 100644 adafruit_wiznet5k/adafruit_wiznet5k_debug.py diff --git a/adafruit_wiznet5k/__init__.py b/adafruit_wiznet5k/__init__.py index bf1635b..e69de29 100644 --- a/adafruit_wiznet5k/__init__.py +++ b/adafruit_wiznet5k/__init__.py @@ -1,49 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Martin Stephens -# -# SPDX-License-Identifier: MIT - -"""Makes a debug message function available to all modules.""" -try: - from typing import TYPE_CHECKING, Optional, Union, Tuple, Sequence - - 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.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index d13fb30..4d25709 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -48,7 +48,7 @@ from micropython import const from adafruit_bus_device.spi_device import SPIDevice -from adafruit_wiznet5k import debug_msg +from adafruit_wiznet5k.adafruit_wiznet5k_debug import debug_msg import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as dhcp import adafruit_wiznet5k.adafruit_wiznet5k_dns as dns 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_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 824b833..37dfb7c 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -28,7 +28,9 @@ import time from random import randint from micropython import const -from adafruit_wiznet5k import debug_msg # pylint: disable=ungrouped-imports +from adafruit_wiznet5k.adafruit_wiznet5k_debug import ( # pylint: disable=ungrouped-imports + debug_msg, +) # DHCP State Machine _STATE_INIT = const(0x01) From 5ea607a8957efad4acccc6833ef88b1a1342d9b8 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 14 Feb 2023 22:38:51 +0300 Subject: [PATCH 70/80] Convert DNS server IP address to dot-quad string before calling socket.connect. --- adafruit_wiznet5k/adafruit_wiznet5k_dns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dns.py b/adafruit_wiznet5k/adafruit_wiznet5k_dns.py index b633276..6824915 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dns.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dns.py @@ -252,7 +252,7 @@ def gethostbyname(self, hostname: bytes) -> Union[int, bytes]: # Send DNS request packet self._sock.bind((None, _DNS_PORT)) - self._sock.connect((self._dns_server, _DNS_PORT)) + self._sock.connect((self._iface.pretty_ip(self._dns_server), _DNS_PORT)) _debug_print(debug=self._debug, message="* DNS: Sending request packet...") self._sock.send(buffer) From aa30f2ec3991f47f01258b7bca2132fb06e92cff Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 22 Feb 2023 07:42:17 +0300 Subject: [PATCH 71/80] Updated to latest version of socket.py. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 26 +- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 1 + adafruit_wiznet5k/adafruit_wiznet5k_dns.py | 12 +- adafruit_wiznet5k/adafruit_wiznet5k_ntp.py | 2 +- adafruit_wiznet5k/adafruit_wiznet5k_socket.py | 581 ++++++++++-------- .../adafruit_wiznet5k_wsgiserver.py | 17 +- tests/test_dns_server_nonbreaking_changes.py | 22 +- 7 files changed, 377 insertions(+), 284 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 4d25709..a0ba92d 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -89,8 +89,8 @@ SNSR_SOCK_ESTABLISHED = const(0x17) SNSR_SOCK_FIN_WAIT = const(0x18) _SNSR_SOCK_CLOSING = const(0x1A) -_SNSR_SOCK_TIME_WAIT = const(0x1B) -_SNSR_SOCK_CLOSE_WAIT = const(0x1C) +SNSR_SOCK_TIME_WAIT = const(0x1B) +SNSR_SOCK_CLOSE_WAIT = const(0x1C) _SNSR_SOCK_LAST_ACK = const(0x1D) _SNSR_SOCK_UDP = const(0x22) _SNSR_SOCK_IPRAW = const(0x32) @@ -260,7 +260,9 @@ def get_host_by_name(self, hostname: str) -> bytes: if isinstance(hostname, str): hostname = bytes(hostname, "utf-8") # Return IP assigned by DHCP - _dns_client = dns.DNS(self, self._dns, debug=self._debug) + _dns_client = dns.DNS( + self, self.pretty_ip(bytearray(self._dns)), debug=self._debug + ) ret = _dns_client.gethostbyname(hostname) debug_msg("* Resolved IP: {}".format(ret), self._debug) if ret == -1: @@ -817,9 +819,9 @@ def socket_open(self, socket_num: int, conn_mode: int = _SNMR_TCP) -> int: status = self.read_snsr(socket_num)[0] if status in ( SNSR_SOCK_CLOSED, - _SNSR_SOCK_TIME_WAIT, + SNSR_SOCK_TIME_WAIT, SNSR_SOCK_FIN_WAIT, - _SNSR_SOCK_CLOSE_WAIT, + SNSR_SOCK_CLOSE_WAIT, _SNSR_SOCK_CLOSING, _SNSR_SOCK_UDP, ): @@ -897,7 +899,7 @@ def socket_read(self, socket_num: int, length: int) -> Tuple[int, bytes]: if ret == 0: # no data on socket? status = self._read_snmr(socket_num) - if status in (SNSR_SOCK_LISTEN, SNSR_SOCK_CLOSED, _SNSR_SOCK_CLOSE_WAIT): + if status in (SNSR_SOCK_LISTEN, SNSR_SOCK_CLOSED, SNSR_SOCK_CLOSE_WAIT): # remote end closed its side of the connection, EOF state raise RuntimeError("Lost connection to peer.") # connection is alive, no data waiting to be read @@ -993,7 +995,7 @@ def socket_write( while free_size < ret: free_size = self._get_tx_free_size(socket_num) status = self.socket_status(socket_num)[0] - if status not in (SNSR_SOCK_ESTABLISHED, _SNSR_SOCK_CLOSE_WAIT) or ( + if status not in (SNSR_SOCK_ESTABLISHED, SNSR_SOCK_CLOSE_WAIT) or ( timeout and time.monotonic() - stamp > timeout ): ret = 0 @@ -1032,18 +1034,18 @@ def socket_write( time.sleep(0.001) # check data was transferred correctly - while not self._read_snir(socket_num)[0] & _SNIR_SEND_OK: + while not self.read_snir(socket_num)[0] & _SNIR_SEND_OK: if self.socket_status(socket_num)[0] in ( SNSR_SOCK_CLOSED, - _SNSR_SOCK_TIME_WAIT, + SNSR_SOCK_TIME_WAIT, SNSR_SOCK_FIN_WAIT, - _SNSR_SOCK_CLOSE_WAIT, + SNSR_SOCK_CLOSE_WAIT, _SNSR_SOCK_CLOSING, ): raise RuntimeError("Socket closed before data was sent.") if timeout and time.monotonic() - stamp > timeout: raise RuntimeError("Operation timed out. No data sent.") - if self._read_snir(socket_num)[0] & _SNIR_TIMEOUT: + if self.read_snir(socket_num)[0] & _SNIR_TIMEOUT: raise TimeoutError( "Hardware timeout while sending on socket {}.".format(socket_num) ) @@ -1127,7 +1129,7 @@ def read_snsr(self, sock: int) -> Optional[bytearray]: """Read Socket n Status Register.""" return self._read_socket(sock, _REG_SNSR) - def _read_snir(self, sock: int) -> Optional[bytearray]: + def read_snir(self, sock: int) -> Optional[bytearray]: """Read Socket n Interrupt Register.""" return self._read_socket(sock, _REG_SNIR) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 37dfb7c..0aff710 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -24,6 +24,7 @@ except ImportError: pass + import gc import time from random import randint diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dns.py b/adafruit_wiznet5k/adafruit_wiznet5k_dns.py index 6824915..0ed7ad1 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dns.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dns.py @@ -251,8 +251,8 @@ def gethostbyname(self, hostname: bytes) -> Union[int, bytes]: self._query_id, self._query_length, buffer = _build_dns_query(hostname) # Send DNS request packet - self._sock.bind((None, _DNS_PORT)) - self._sock.connect((self._iface.pretty_ip(self._dns_server), _DNS_PORT)) + self._sock.bind(("", _DNS_PORT)) + self._sock.connect((self._dns_server, _DNS_PORT)) _debug_print(debug=self._debug, message="* DNS: Sending request packet...") self._sock.send(buffer) @@ -261,9 +261,11 @@ def gethostbyname(self, hostname: bytes) -> Union[int, bytes]: for _ in range(5): # wait for a response socket_timeout = time.monotonic() + 1.0 - packet_size = self._sock.available() + packet_size = self._sock._available() # pylint: disable=protected-access while packet_size == 0: - packet_size = self._sock.available() + packet_size = ( + self._sock._available() # pylint: disable=protected-access + ) if time.monotonic() > socket_timeout: _debug_print( debug=self._debug, @@ -273,7 +275,7 @@ def gethostbyname(self, hostname: bytes) -> Union[int, bytes]: return -1 time.sleep(0.05) # recv packet into buf - buffer = self._sock.recv(512) + buffer = self._sock.recv(512) # > UDP payload length _debug_print( debug=self._debug, message="DNS Packet Received: {}".format(buffer), diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_ntp.py b/adafruit_wiznet5k/adafruit_wiznet5k_ntp.py index a1a98fd..f15d8d4 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_ntp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_ntp.py @@ -74,7 +74,7 @@ def get_time(self) -> time.struct_time: self._sock.sendto(self._pkt_buf_, (self._ntp_server, 123)) end_time = time.monotonic() + 0.2 * 2**retry while time.monotonic() < end_time: - data = self._sock.recv() + data = self._sock.recv(64) # NTP returns a 48 byte message. if data: sec = data[40:44] int_cal = int.from_bytes(sec, "big") diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_socket.py b/adafruit_wiznet5k/adafruit_wiznet5k_socket.py index 76f8677..eb496bf 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_socket.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_socket.py @@ -2,14 +2,16 @@ # SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries # # SPDX-License-Identifier: MIT - +# +# CPython uses type as an argument in socket.socket, so disable checking in Pylint +# pylint: disable=redefined-builtin """ `adafruit_wiznet5k_socket` ================================================================================ A socket compatible interface with the Wiznet5k module. -* Author(s): ladyada, Brent Rubell, Patrick Van Oosterwijck, Adam Cummick +* Author(s): ladyada, Brent Rubell, Patrick Van Oosterwijck, Adam Cummick, Martin Stephens """ from __future__ import annotations @@ -24,12 +26,29 @@ import gc import time +from sys import byteorder from micropython import const import adafruit_wiznet5k as wiznet5k # pylint: disable=invalid-name _the_interface: Optional[WIZNET5K] = None +_default_socket_timeout = None + + +def _is_ipv4_string(ipv4_address: str) -> bool: + """Check for a valid IPv4 address in dotted-quad string format + (for example, "123.45.67.89"). + + :param: str ipv4_address: The string to test. + + :return bool: True if a valid IPv4 address, False otherwise. + """ + octets = ipv4_address.split(".", 3) + if len(octets) == 4 and "".join(octets).isdigit(): + if all((0 <= int(octet) <= 255 for octet in octets)): + return True + return False def set_interface(iface: WIZNET5K) -> None: @@ -42,6 +61,30 @@ def set_interface(iface: WIZNET5K) -> None: _the_interface = iface +def getdefaulttimeout() -> Optional[float]: + """ + Return the default timeout in seconds for new socket objects. A value of + None indicates that new socket objects have no timeout. When the socket module is + first imported, the default is None. + """ + return _default_socket_timeout + + +def setdefaulttimeout(timeout: Optional[float]) -> None: + """ + Set the default timeout in seconds (float) for new socket objects. When the socket + module is first imported, the default is None. See settimeout() for possible values + and their respective meanings. + + :param Optional[float] timeout: The default timeout in seconds or None. + """ + global _default_socket_timeout # pylint: disable=global-statement + if timeout is None or (isinstance(timeout, (int, float)) and timeout >= 0): + _default_socket_timeout = timeout + else: + raise ValueError("Timeout must be None, 0.0 or a positive numeric value.") + + def htonl(x: int) -> int: """ Convert 32-bit positive integer from host to network byte order. @@ -50,12 +93,9 @@ def htonl(x: int) -> int: :return int: 32-bit positive integer in network byte order. """ - return ( - ((x) << 24 & 0xFF000000) - | ((x) << 8 & 0x00FF0000) - | ((x) >> 8 & 0x0000FF00) - | ((x) >> 24 & 0x000000FF) - ) + if byteorder == "big": + return x + return int.from_bytes(x.to_bytes(4, "little"), "big") def htons(x: int) -> int: @@ -66,7 +106,43 @@ def htons(x: int) -> int: :return int: 16-bit positive integer in network byte order. """ - return (((x) << 8) & 0xFF00) | (((x) >> 8) & 0xFF) + if byteorder == "big": + return x + return ((x << 8) & 0xFF00) | ((x >> 8) & 0xFF) + + +def inet_aton(ip_address: str) -> bytes: + """ + Convert an IPv4 address from dotted-quad string format (for example, "123.45.67.89") + to 32-bit packed binary format, as a bytes object four characters in length. This is + useful when conversing with a program that uses the standard C library and needs + objects of type struct in_addr, which is the C type for the 32-bit packed binary this + function returns. + + :param str ip_address: The IPv4 address to convert. + + :return bytes: The converted IPv4 address. + """ + if not _is_ipv4_string(ip_address): + raise ValueError("The IPv4 address must be a dotted-quad string.") + return _the_interface.unpretty_ip(ip_address) + + +def inet_ntoa(ip_address: Union[bytes, bytearray]) -> str: + """ + Convert a 32-bit packed IPv4 address (a bytes-like object four bytes in length) to + its standard dotted-quad string representation (for example, ‘123.45.67.89’). This is + useful when conversing with a program that uses the standard C library and needs + objects of type struct in_addr, which is the C type for the 32-bit packed binary data + this function takes as an argument. + + :param Union[bytes, bytearray ip_address: The IPv4 address to convert. + + :return str: The converted ip_address: + """ + if len(ip_address) != 4: + raise ValueError("The IPv4 address must be 4 bytes.") + return _the_interface.pretty_ip(ip_address) SOCK_STREAM = const(0x21) # TCP @@ -81,7 +157,7 @@ def getaddrinfo( host: str, port: int, family: int = 0, - socktype: int = 0, + type: int = 0, proto: int = 0, flags: int = 0, ) -> List[Tuple[int, int, int, str, Tuple[str, int]]]: @@ -89,52 +165,42 @@ def getaddrinfo( Translate the host/port argument into a sequence of 5-tuples that contain all the necessary arguments for creating a socket connected to that service. - :param str host: a domain name, a string representation of an IPv4/v6 address or + :param str host: a domain name, a string representation of an IPv4 address or None. :param int port: Port number to connect to (0 - 65536). :param int family: Ignored and hardcoded as 0x03 (the only family implemented) by the function. - :param int socktype: The type of socket, either SOCK_STREAM (0x21) for TCP or SOCK_DGRAM (0x02) - for UDP, defaults to 0x00. + :param int type: The type of socket, either SOCK_STREAM (0x21) for TCP or SOCK_DGRAM (0x02) + for UDP, defaults to 0. :param int proto: Unused in this implementation of socket. :param int flags: Unused in this implementation of socket. - :return List[Tuple[int, int, int, str, Tuple[str, int]]]: Address info entries. + :return List[Tuple[int, int, int, str, Tuple[str, int]]]: Address info entries in the form + (family, type, proto, canonname, sockaddr). In these tuples, family, type, proto are meant + to be passed to the socket() function. canonname will always be an empty string, sockaddr + is a tuple describing a socket address, whose format is (address, port), and is meant to be + passed to the socket.connect() method. """ if not isinstance(port, int): raise ValueError("Port must be an integer") - if is_ipv4(host): - return [(AF_INET, socktype, proto, "", (host, port))] - return [(AF_INET, socktype, proto, "", (gethostbyname(host), port))] + if not _is_ipv4_string(host): + host = gethostbyname(host) + return [(AF_INET, type, proto, "", (host, port))] def gethostbyname(hostname: str) -> str: """ - Lookup a host name's IPv4 address. + Translate a host name to IPv4 address format. The IPv4 address is returned as a string, such + as '100.50.200.5'. If the host name is an IPv4 address itself it is returned unchanged. :param str hostname: Hostname to lookup. - :return str: IPv4 address (a string of the form '255.255.255.255'). + :return str: IPv4 address (a string of the form '0.0.0.0'). """ - addr = _the_interface.get_host_by_name(hostname) - addr = "{}.{}.{}.{}".format(addr[0], addr[1], addr[2], addr[3]) - return addr - - -def is_ipv4(host: str) -> bool: - """ - Check if a hostname is an IPv4 address (a string of the form '255.255.255.255'). - - :param str host: Hostname to check. - - :return bool: - """ - octets = host.split(".", 3) - if len(octets) != 4 or not "".join(octets).isdigit(): - return False - for octet in octets: - if int(octet) > 255: - return False - return True + if _is_ipv4_string(hostname): + return hostname + address = _the_interface.get_host_by_name(hostname) + address = "{}.{}.{}.{}".format(address[0], address[1], address[2], address[3]) + return address # pylint: disable=invalid-name, too-many-public-methods @@ -151,7 +217,6 @@ def __init__( type: int = SOCK_STREAM, proto: int = 0, fileno: Optional[int] = None, - socknum: Optional[int] = None, ) -> None: """ :param int family: Socket address (and protocol) family, defaults to AF_INET. @@ -159,13 +224,12 @@ def __init__( defaults to SOCK_STREAM. :param int proto: Unused, retained for compatibility. :param Optional[int] fileno: Unused, retained for compatibility. - :param Optional[int] socknum: Unused, retained for compatibility. """ if family != AF_INET: raise RuntimeError("Only AF_INET family supported by W5K modules.") self._sock_type = type self._buffer = b"" - self._timeout = 0 + self._timeout = _default_socket_timeout self._listen_port = None self._socknum = _the_interface.get_socket() @@ -177,37 +241,28 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb) -> None: if self._sock_type == SOCK_STREAM: - self.disconnect() + self._disconnect() stamp = time.monotonic() - while self.status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_FIN_WAIT: + while self._status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_FIN_WAIT: if time.monotonic() - stamp > 1000: raise RuntimeError("Failed to disconnect socket") self.close() stamp = time.monotonic() - while self.status != wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED: + while self._status != wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED: if time.monotonic() - stamp > 1000: raise RuntimeError("Failed to close socket") @property - def socknum(self) -> int: - """ - Return the socket object's socket number. - - :return int: Socket number. - """ - return self._socknum - - @property - def status(self) -> int: + def _status(self) -> int: """ Return the status of the socket. :return int: Status of the socket. """ - return _the_interface.socket_status(self.socknum)[0] + return _the_interface.socket_status(self._socknum)[0] @property - def connected(self) -> bool: + def _connected(self) -> bool: """ Return whether connected to the socket. @@ -215,246 +270,214 @@ def connected(self) -> bool: """ # pylint: disable=protected-access - if self.socknum >= _the_interface.max_sockets: + if self._socknum >= _the_interface.max_sockets: return False - status = _the_interface.socket_status(self.socknum)[0] + status = _the_interface.socket_status(self._socknum)[0] if ( - status == wiznet5k.adafruit_wiznet5k._SNSR_SOCK_CLOSE_WAIT - and self.available() == 0 + status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSE_WAIT + and self._available() == 0 ): result = False else: result = status not in ( wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED, wiznet5k.adafruit_wiznet5k.SNSR_SOCK_LISTEN, - wiznet5k.adafruit_wiznet5k._SNSR_SOCK_TIME_WAIT, + wiznet5k.adafruit_wiznet5k.SNSR_SOCK_TIME_WAIT, wiznet5k.adafruit_wiznet5k.SNSR_SOCK_FIN_WAIT, ) if not result and status != wiznet5k.adafruit_wiznet5k.SNSR_SOCK_LISTEN: self.close() return result - def getpeername(self) -> Union[str, bytearray]: + def getpeername(self) -> Tuple[str, int]: """ Return the remote address to which the socket is connected. - :return Union[str, bytearray]: An IPv4 address (a string of the form '255.255.255.255'). - An error may return a bytearray. + :return Tuple[str, int]: IPv4 address and port the socket is connected to. """ - return _the_interface.remote_ip(self.socknum) + return _the_interface.remote_ip(self._socknum), _the_interface.remote_port( + self._socknum + ) - def inet_aton(self, ip_string: str) -> bytearray: + def bind(self, address: Tuple[Optional[str], int]) -> None: """ - Convert an IPv4 address from dotted-quad string format. + Bind the socket to address. The socket must not already be bound. - :param str ip_string: IPv4 address (a string of the form '255.255.255.255'). + The hardware sockets on WIZNET5K systems all share the same IPv4 address. The + address is assigned at startup. Ports can only be bound to this address. - :return bytearray: IPv4 address as a 4 byte bytearray. - """ - self._buffer = b"" - self._buffer = [int(item) for item in ip_string.split(".")] - self._buffer = bytearray(self._buffer) - return self._buffer + :param Tuple[Optional[str], int] address: Address as a (host, port) tuple. - def bind(self, address: Tuple[Optional[str], int]) -> None: - """Bind the socket to the listen port. + :raises ValueError: If the IPv4 address specified is not the address + assigned to the WIZNET5K interface. + """ + # Check to see if the socket is bound. + if self._listen_port: + raise ConnectionError("The socket is already bound.") + self._bind(address) - If the host is specified the interface will be reconfigured to that IP address. + def _bind(self, address: Tuple[Optional[str], int]) -> None: + """ + Helper function to allow bind() to check for an existing connection and for + accept() to generate a new socket connection. - :param Tuple[Optional[str], int] address: Address as a (host, port) tuple. The host - may be an IPv4 address (a string of the form '255.255.255.255'), or None. - The port number is in the range (0 - 65536). + :param Tuple[Optional[str], int] address: Address as a (host, port) tuple. """ - if address[0] is not None: - ip_address = _the_interface.unpretty_ip(address[0]) - current_ip, subnet_mask, gw_addr, dns = _the_interface.ifconfig - if ip_address != current_ip: - _the_interface.ifconfig = (ip_address, subnet_mask, gw_addr, dns) + if address[0]: + if gethostbyname(address[0]) != _the_interface.pretty_ip( + _the_interface.ip_address + ): + raise ValueError( + "The IPv4 address requested must match {}, " + "the one assigned to the WIZNET5K interface.".format( + _the_interface.pretty_ip(_the_interface.ip_address) + ) + ) self._listen_port = address[1] # For UDP servers we need to open the socket here because we won't call # listen if self._sock_type == SOCK_DGRAM: _the_interface.socket_listen( - self.socknum, self._listen_port, wiznet5k.adafruit_wiznet5k.SNMR_UDP + self._socknum, + self._listen_port, + wiznet5k.adafruit_wiznet5k.SNMR_UDP, ) self._buffer = b"" - def listen(self, backlog: Optional[int] = None) -> None: + def listen(self, backlog: int = 0) -> None: """ - Listen on the port specified by bind. + Enable a server to accept connections. - :param Optional[int] backlog: Included for compatibility but ignored. + :param int backlog: Included for compatibility but ignored. """ if self._listen_port is None: raise RuntimeError("Use bind to set the port before listen!") - _the_interface.socket_listen(self.socknum, self._listen_port) + _the_interface.socket_listen(self._socknum, self._listen_port) self._buffer = b"" def accept( self, - ) -> Optional[Tuple[socket, Tuple[Union[str, bytearray], Union[int, bytearray]],]]: - # wiznet5k.adafruit_wiznet5k_socket.socket, + ) -> Tuple[socket, Tuple[str, int]]: """ - Accept a connection. - - The socket must be bound to an address and listening for connections. - - The return value is a pair (conn, address) where conn is a new - socket object to send and receive data on the connection, and address is - the address bound to the socket on the other end of the connection. + Accept a connection. The socket must be bound to an address and listening for connections. - :return Optional[Tuple[socket.socket, Tuple[Union[str, bytearray], Union[int, bytearray]]]: - If successful (socket object, (IP address, port)). If errors occur, the IP address - and / or the port may be returned as bytearrays. + :return Tuple[socket, Tuple[str, int]]: The return value is a pair + (conn, address) where conn is a new socket object to send and receive data on + the connection, and address is the address bound to the socket on the other + end of the connection. """ stamp = time.monotonic() - while self.status not in ( + while self._status not in ( wiznet5k.adafruit_wiznet5k.SNSR_SOCK_SYNRECV, wiznet5k.adafruit_wiznet5k.SNSR_SOCK_ESTABLISHED, ): - if self._timeout > 0 and time.monotonic() - stamp > self._timeout: - return None - if self.status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED: + if self._timeout and 0 < self._timeout < time.monotonic() - stamp: + raise TimeoutError("Failed to accept connection.") + if self._status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED: self.close() self.listen() - new_listen_socknum, addr = _the_interface.socket_accept(self.socknum) - current_socknum = self.socknum + new_listen_socknum, addr = _the_interface.socket_accept(self._socknum) + current_socknum = self._socknum # Create a new socket object and swap socket nums, so we can continue listening client_sock = socket() client_sock._socknum = current_socknum # pylint: disable=protected-access - self._socknum = new_listen_socknum # pylint: disable=protected-access - self.bind((None, self._listen_port)) + self._socknum = new_listen_socknum + self._bind((None, self._listen_port)) self.listen() - while self.status != wiznet5k.adafruit_wiznet5k.SNSR_SOCK_LISTEN: + while self._status != wiznet5k.adafruit_wiznet5k.SNSR_SOCK_LISTEN: raise RuntimeError("Failed to open new listening socket") return client_sock, addr - def connect( - self, - address: Tuple[Union[str, Tuple[int, int, int, int]], int], - conntype: Optional[int] = None, - ) -> None: + def connect(self, address: Tuple[str, int]) -> None: """ - Connect to a remote socket. + Connect to a remote socket at address. - :param Tuple[Union[str, Tuple[int, int, int, int]], int] address: Remote socket as - a (host, port) tuple. The host may be a tuple in the form (0, 0, 0, 0) or a string. - :param Optional[int] conntype: Raises an exception if set to 3, unused otherwise, defaults - to None. + :param Tuple[str, int] address: Remote socket as a (host, port) tuple. """ - if conntype == 0x03: - raise NotImplementedError( - "Error: SSL/TLS is not currently supported by CircuitPython." - ) - host, port = address - - if hasattr(host, "split"): - try: - host = tuple(map(int, host.split("."))) - except ValueError: - host = _the_interface.get_host_by_name(host) 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 + self._socknum, + _the_interface.unpretty_ip(gethostbyname(address[0])), + address[1], + self._sock_type, ) _the_interface.src_port = 0 if not result: - raise RuntimeError("Failed to connect to host", host) + raise RuntimeError("Failed to connect to host ", address[0]) self._buffer = b"" - def send(self, data: Union[bytes, bytearray]) -> None: + def send(self, data: Union[bytes, bytearray]) -> int: """ - Send data to the socket. - - The socket must be connected to a remote socket. + Send data to the socket. The socket must be connected to a remote socket. + Applications are responsible for checking that all data has been sent; if + only some of the data was transmitted, the application needs to attempt + delivery of the remaining data. :param bytearray data: Data to send to the socket. + + :return int: Number of bytes sent. """ - _the_interface.socket_write(self.socknum, data, self._timeout) + bytes_sent = _the_interface.socket_write(self._socknum, data, self._timeout) gc.collect() + return bytes_sent - def sendto(self, data: bytearray, address: [Tuple[str, int]]) -> None: + def sendto(self, data: bytearray, *flags_and_or_address: any) -> int: """ - Connect to a remote socket and send data. + Send data to the socket. The socket should not be connected to a remote socket, since the + destination socket is specified by address. Return the number of bytes sent.. + Either: :param bytearray data: Data to send to the socket. - :param tuple address: Remote socket as a (host, port) tuple. + :param [Tuple[str, int]] address: Remote socket as a (host, port) tuple. + + Or: + :param bytearray data: Data to send to the socket. + :param int flags: Not implemented, kept for compatibility. + :param Tuple[int, Tuple(str, int)] address: Remote socket as a (host, port) tuple """ + # May be called with (data, address) or (data, flags, address) + other_args = list(flags_and_or_address) + if len(other_args) in (1, 2): + address = other_args[-1] + else: + raise ValueError("Incorrect number of arguments, should be 2 or 3.") self.connect(address) return self.send(data) def recv( # pylint: disable=too-many-branches self, - bufsize: int = 0, + bufsize: int, flags: int = 0, ) -> bytes: """ - Read from the connected remote address. + Receive data from the socket. The return value is a bytes object representing the data + received. The maximum amount of data to be received at once is specified by bufsize. :param int bufsize: Maximum number of bytes to receive. :param int flags: ignored, present for compatibility. - :return bytes: Data from the remote address. + :return bytes: Data from the socket. """ - if self.status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED: - return b"" - - if bufsize == 0: - # read everything on the socket - while True: - avail = self.available() - if avail: - if self._sock_type == SOCK_STREAM: - self._buffer += _the_interface.socket_read(self.socknum, avail)[ - 1 - ] - elif self._sock_type == SOCK_DGRAM: - self._buffer += _the_interface.read_udp(self.socknum, avail)[1] - break - else: - break - gc.collect() - ret = self._buffer - self._buffer = b"" - gc.collect() - return ret stamp = time.monotonic() - - to_read = bufsize - len(self._buffer) - received = [] - while to_read > 0: - avail = self.available() - if avail: - stamp = time.monotonic() - if self._sock_type == SOCK_STREAM: - recv = _the_interface.socket_read( - self.socknum, min(to_read, avail) - )[1] - elif self._sock_type == SOCK_DGRAM: - recv = _the_interface.read_udp(self.socknum, min(to_read, avail))[1] - to_read = len(recv) # only get this dgram - recv = bytes(recv) - received.append(recv) - to_read -= len(recv) - gc.collect() - if self._timeout > 0 and time.monotonic() - stamp > self._timeout: + while not self._available(): + if self._timeout and 0 < self._timeout < time.monotonic() - stamp: break - self._buffer += b"".join(received) - - ret = None - if len(self._buffer) == bufsize: - ret = self._buffer - self._buffer = b"" + time.sleep(0.05) + bytes_on_socket = self._available() + if not bytes_on_socket: + return b"" + bytes_to_read = min(bytes_on_socket, bufsize) + if self._sock_type == SOCK_STREAM: + bytes_read = _the_interface.socket_read(self._socknum, bytes_to_read)[1] else: - ret = self._buffer[:bufsize] - self._buffer = self._buffer[bufsize:] + bytes_read = _the_interface.read_udp(self._socknum, bytes_to_read)[1] gc.collect() - return ret + return bytes(bytes_read) - def embed_recv( + def _embed_recv( self, bufsize: int = 0, flags: int = 0 ) -> bytes: # pylint: disable=too-many-branches """ @@ -466,81 +489,84 @@ def embed_recv( :return bytes: All data available from the connection. """ - ret = None - avail = self.available() + avail = self._available() if avail: if self._sock_type == SOCK_STREAM: - self._buffer += _the_interface.socket_read(self.socknum, avail)[1] + self._buffer += _the_interface.socket_read(self._socknum, avail)[1] elif self._sock_type == SOCK_DGRAM: - self._buffer += _the_interface.read_udp(self.socknum, avail)[1] + self._buffer += _the_interface.read_udp(self._socknum, avail)[1] gc.collect() ret = self._buffer self._buffer = b"" gc.collect() return ret - def recvfrom( - self, bufsize: int = 0, flags: int = 0 - ) -> Tuple[bytes, Tuple[str, int]]: + def recvfrom(self, bufsize: int, flags: int = 0) -> Tuple[bytes, Tuple[str, int]]: """ - Read some bytes from the connected remote address. + Receive data from the socket. The return value is a pair (bytes, address) where bytes is + a bytes object representing the data received and address is the address of the socket + sending the data. :param int bufsize: Maximum number of bytes to receive. - :param int flags: ignored, present for compatibility. + :param int flags: Ignored, present for compatibility. :return Tuple[bytes, Tuple[str, int]]: a tuple (bytes, address) - where address is a tuple (ip, port) + where address is a tuple (address, port) """ return ( self.recv(bufsize), ( - _the_interface.pretty_ip(_the_interface.udp_from_ip[self.socknum]), - _the_interface.udp_from_port[self.socknum], + _the_interface.pretty_ip(_the_interface.udp_from_ip[self._socknum]), + _the_interface.udp_from_port[self._socknum], ), ) - def recv_into(self, buf: bytearray, nbytes: int = 0, flags: int = 0) -> int: + def recv_into(self, buffer: bytearray, nbytes: int = 0, flags: int = 0) -> int: """ - Read from the connected remote address into the provided buffer. + Receive up to nbytes bytes from the socket, storing the data into a buffer + rather than creating a new bytestring. - :param bytearray buf: Data buffer - :param nbytes: Maximum number of bytes to receive + :param bytearray buffer: Data buffer to read into. + :param nbytes: Maximum number of bytes to receive (if 0, use length of buffer). :param int flags: ignored, present for compatibility. :return int: the number of bytes received """ if nbytes == 0: - nbytes = len(buf) - ret = self.recv(nbytes) - nbytes = len(ret) - buf[:nbytes] = ret + nbytes = len(buffer) + bytes_received = self.recv(nbytes) + nbytes = len(bytes_received) + buffer[:nbytes] = bytes_received return nbytes def recvfrom_into( - self, buf: bytearray, nbytes: int = 0, flags: int = 0 + self, buffer: bytearray, nbytes: int = 0, flags: int = 0 ) -> Tuple[int, Tuple[str, int]]: """ - Read some bytes from the connected remote address into the provided buffer. + Receive data from the socket, writing it into buffer instead of creating a new bytestring. + The return value is a pair (nbytes, address) where nbytes is the number of bytes received + and address is the address of the socket sending the data. - :param bytearray buf: Data buffer. + :param bytearray buffer: Data buffer. :param int nbytes: Maximum number of bytes to receive. :param int flags: Unused, present for compatibility. - :return Tuple[int, Tuple[str, int]]: A tuple (nbytes, address) where address is a - tuple (ip, port) + :return Tuple[int, Tuple[str, int]]: The number of bytes and address. """ return ( - self.recv_into(buf, nbytes), + self.recv_into(buffer, nbytes), ( - _the_interface.remote_ip(self.socknum), - _the_interface.remote_port(self.socknum), + _the_interface.remote_ip(self._socknum), + _the_interface.remote_port(self._socknum), ), ) - def readline(self) -> bytes: + def _readline(self) -> bytes: """ Read a line from the socket. + Deprecated, will be removed in the future. + Attempt to return as many bytes as we can up to but not including a carriage return and linefeed character pair. @@ -548,55 +574,112 @@ def readline(self) -> bytes: """ stamp = time.monotonic() while b"\r\n" not in self._buffer: - avail = self.available() + avail = self._available() if avail: if self._sock_type == SOCK_STREAM: - self._buffer += _the_interface.socket_read(self.socknum, avail)[1] + self._buffer += _the_interface.socket_read(self._socknum, avail)[1] elif self._sock_type == SOCK_DGRAM: - self._buffer += _the_interface.read_udp(self.socknum, avail)[1] - if ( - not avail - and self._timeout > 0 - and time.monotonic() - stamp > self._timeout - ): - self.close() - raise RuntimeError("Didn't receive response, failing out...") + self._buffer += _the_interface.read_udp(self._socknum, avail)[1] + if ( + self._timeout + and not avail + and 0 < self._timeout < time.monotonic() - stamp + ): + self.close() + raise RuntimeError("Didn't receive response, failing out...") firstline, self._buffer = self._buffer.split(b"\r\n", 1) gc.collect() return firstline - def disconnect(self) -> None: + def _disconnect(self) -> None: """Disconnect a TCP socket.""" if self._sock_type != SOCK_STREAM: raise RuntimeError("Socket must be a TCP socket.") - _the_interface.socket_disconnect(self.socknum) + _the_interface.socket_disconnect(self._socknum) def close(self) -> None: - """Close the socket.""" - _the_interface.socket_close(self.socknum) + """ + Mark the socket closed. Once that happens, all future operations on the socket object + will fail. The remote end will receive no more data. + """ + _the_interface.socket_close(self._socknum) - def available(self) -> int: + def _available(self) -> int: """ Return how many bytes of data are available to be read from the socket. :return int: Number of bytes available. """ - return _the_interface.socket_available(self.socknum, self._sock_type) + return _the_interface.socket_available(self._socknum, self._sock_type) - def settimeout(self, value: float) -> None: + def settimeout(self, value: Optional[float]) -> None: """ - Set the socket read timeout. + Set a timeout on blocking socket operations. The value argument can be a + non-negative floating point number expressing seconds, or None. If a non-zero + value is given, subsequent socket operations will raise a timeout exception + if the timeout period value has elapsed before the operation has completed. + If zero is given, the socket is put in non-blocking mode. If None is given, + the socket is put in blocking mode.. - :param float value: Socket read timeout in seconds. + :param Optional[float] value: Socket read timeout in seconds. """ - if value < 0: - raise Exception("Timeout period should be non-negative.") - self._timeout = value + if value is None or (isinstance(value, (int, float)) and value >= 0): + self._timeout = value + else: + raise ValueError("Timeout must be None, 0.0 or a positive numeric value.") def gettimeout(self) -> Optional[float]: """ - Timeout associated with socket operations. + Return the timeout in seconds (float) associated with socket operations, or None if no + timeout is set. This reflects the last call to setblocking() or settimeout(). :return Optional[float]: Timeout in seconds, or None if no timeout is set. """ return self._timeout + + def setblocking(self, flag: bool) -> None: + """ + Set blocking or non-blocking mode of the socket: if flag is false, the socket is set + to non-blocking, else to blocking mode. + + This method is a shorthand for certain settimeout() calls: + + sock.setblocking(True) is equivalent to sock.settimeout(None) + sock.setblocking(False) is equivalent to sock.settimeout(0.0) + + :param bool flag: The blocking mode of the socket. + + :raises TypeError: If flag is not a bool. + + """ + if flag is True: + self.settimeout(None) + elif flag is False: + self.settimeout(0.0) + else: + raise TypeError("Flag must be a boolean.") + + def getblocking(self) -> bool: + """ + Return True if socket is in blocking mode, False if in non-blocking. + + This is equivalent to checking socket.gettimeout() == 0. + + :return bool: Blocking mode of the socket. + """ + return self.gettimeout() == 0 + + @property + def family(self) -> int: + """Socket family (always 0x03 in this implementation).""" + return 3 + + @property + def type(self): + """Socket type.""" + return self._sock_type + + @property + def proto(self): + """Socket protocol (always 0x00 in this implementation).""" + return 0 diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py b/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py index fd237a7..6309b8f 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py @@ -113,14 +113,17 @@ def update_poll(self) -> None: the application callable will be invoked. """ for sock in self._client_sock: - if sock.available(): + if sock._available(): # pylint: disable=protected-access 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: + if ( + sock._status # pylint: disable=protected-access + == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED + ): self._client_sock.remove(sock) for _ in range(len(self._client_sock), self.MAX_SOCK_NUM): try: @@ -161,7 +164,7 @@ def finish_response(self, result: str, client: socket.socket) -> None: client.send(data_chunk) gc.collect() finally: - client.disconnect() + client._disconnect() # pylint: disable=protected-access client.close() def _start_response( @@ -189,7 +192,7 @@ def _get_environ(self, client: socket.socket) -> Dict: :return Dict: Data for the application callable. """ env = {} - line = str(client.readline(), "utf-8") + line = str(client._readline(), "utf-8") # pylint: disable=protected-access (method, path, ver) = line.rstrip("\r\n").split(None, 2) env["wsgi.version"] = (1, 0) @@ -211,7 +214,9 @@ def _get_environ(self, client: socket.socket) -> Dict: headers = {} while True: - header = str(client.readline(), "utf-8") + header = str( + client._readline(), "utf-8" # pylint: disable=protected-access + ) if header == "": break title, content = header.split(": ", 1) @@ -224,7 +229,7 @@ def _get_environ(self, client: socket.socket) -> Dict: body = client.recv(int(env["CONTENT_LENGTH"])) env["wsgi.input"] = io.StringIO(body) else: - body = client.recv() + body = client.recv(1024) env["wsgi.input"] = io.StringIO(body) for name, value in headers.items(): key = "HTTP_" + name.replace("-", "_").upper() diff --git a/tests/test_dns_server_nonbreaking_changes.py b/tests/test_dns_server_nonbreaking_changes.py index ab920b8..c49b64c 100644 --- a/tests/test_dns_server_nonbreaking_changes.py +++ b/tests/test_dns_server_nonbreaking_changes.py @@ -130,13 +130,13 @@ def test_good_domain_names_give_correct_ipv4( ) # Set up mock server calls. dns_server = wiz_dns.DNS(wiznet, "8.8.8.8", debug=DEFAULT_DEBUG_ON) - dns_server._sock.available.return_value = len(dns_bytes_recv) + dns_server._sock._available.return_value = len(dns_bytes_recv) dns_server._sock.recv.return_value = dns_bytes_recv # Check that the correct IPv4 address was received. assert dns_server.gethostbyname(bytes(domain, "utf-8")) == ipv4 # Check that correct socket calls were made. - dns_server._sock.bind.assert_called_once_with((None, 0x35)) + dns_server._sock.bind.assert_called_once_with(("", 0x35)) dns_server._sock.connect.assert_called_once() dns_server._sock.send.assert_called_once_with(dns_bytes_sent) @@ -169,15 +169,15 @@ def test_bad_response_returns_correct_value( ) # Set up mock server calls. dns_server = wiz_dns.DNS(wiznet, "8.8.8.8", debug=DEFAULT_DEBUG_ON) - dns_server._sock.available.return_value = len(dns_bytes_recv) + dns_server._sock._available.return_value = len(dns_bytes_recv) dns_server._sock.recv.return_value = dns_bytes_recv # Check that the correct response was received. assert dns_server.gethostbyname(bytes("apple.com", "utf-8")) == response # Check that the correct number of calls to _sock.available were made. - dns_server._sock.available.assert_called() - assert len(dns_server._sock.available.call_args_list) == 5 + dns_server._sock._available.assert_called() + assert len(dns_server._sock._available.call_args_list) == 5 @freezegun.freeze_time("2022-3-4", auto_tick_seconds=0.1) def test_retries_with_no_data_on_socket(self, wiznet, wrench): @@ -186,13 +186,13 @@ def test_retries_with_no_data_on_socket(self, wiznet, wrench): # pylint: disable=unused-argument dns_server = wiz_dns.DNS(wiznet, "8.8.8.8", debug=DEFAULT_DEBUG_ON) - dns_server._sock.available.return_value = 0 + dns_server._sock._available.return_value = 0 dns_server._sock.recv.return_value = b"" dns_server.gethostbyname(bytes("domain.name", "utf-8")) # Check how many times the socket was polled for data before giving up. - dns_server._sock.available.assert_called() - assert len(dns_server._sock.available.call_args_list) == 12 + dns_server._sock._available.assert_called() + assert len(dns_server._sock._available.call_args_list) == 12 # Check that no attempt made to read data from the socket. dns_server._sock.recv.assert_not_called() @@ -202,13 +202,13 @@ def test_retries_with_bad_data_on_socket(self, wiznet, wrench): # pylint: disable=unused-argument dns_server = wiz_dns.DNS(wiznet, "8.8.8.8", debug=DEFAULT_DEBUG_ON) - dns_server._sock.available.return_value = 7 + dns_server._sock._available.return_value = 7 dns_server._sock.recv.return_value = b"\x99\x12\x81\x80\x00\x01\x00" dns_server.gethostbyname(bytes("domain.name", "utf-8")) # Check how many times the socket was polled for data before giving up. - dns_server._sock.available.assert_called() - assert len(dns_server._sock.available.call_args_list) == 5 + dns_server._sock._available.assert_called() + assert len(dns_server._sock._available.call_args_list) == 5 # Check how many attempts were made to read data from the socket. dns_server._sock.recv.assert_called() assert len(dns_server._sock.recv.call_args_list) == 5 From 06c2ea159917e3df5ea6f8bd6145a3cbba5ec775 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Mon, 20 Mar 2023 16:37:34 -0500 Subject: [PATCH 72/80] fix constant name --- 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 d824137..b41f7a6 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -1094,7 +1094,7 @@ def socket_write( raise RuntimeError("Socket closed before data was sent.") if timeout and time.monotonic() - stamp > timeout: raise RuntimeError("Operation timed out. No data sent.") - if self.read_snir(socket_num)[0] & _SNIR_TIMEOUT: + if self.read_snir(socket_num)[0] & SNIR_TIMEOUT: raise TimeoutError( "Hardware timeout while sending on socket {}.".format(socket_num) ) From 82277ca356746be4e8e6dca5967d9a78617e25f9 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 22 Mar 2023 22:08:05 +0700 Subject: [PATCH 73/80] Increased buffer to 512 bytes due to long DHCP response. Fixed timeout bug. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 0aff710..20e1de5 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -84,7 +84,7 @@ _OPT_END = const(255) # Packet buffer -_BUFF_LENGTH = 318 +_BUFF_LENGTH = 512 _BUFF = bytearray(_BUFF_LENGTH) @@ -303,7 +303,8 @@ def _receive_dhcp_response(self, timeout: float) -> int: buffer = bytearray(b"") bytes_read = 0 debug_msg("+ Beginning to receive…", self._debug) - while bytes_read <= minimum_packet_length and time.monotonic() < timeout: + timeout += time.monotonic() + while bytes_read < minimum_packet_length and time.monotonic() < timeout: if self._eth.socket_available(self._wiz_sock, _SNMR_UDP): x = self._eth.read_udp(self._wiz_sock, _BUFF_LENGTH - bytes_read)[1] buffer.extend(x) @@ -612,12 +613,18 @@ def option_reader(pointer: int) -> Tuple[int, int, bytes]: :returns Tuple[int, int, bytes]: Pointer to next option, option type, and option data. """ + # debug_msg("initial pointer = {}".format(pointer), self._debug) option_type = _BUFF[pointer] + # debug_msg("option type = {}".format(option_type), self._debug) pointer += 1 data_length = _BUFF[pointer] + # debug_msg("data length = {}".format(data_length), self._debug) pointer += 1 data_end = pointer + data_length + # debug_msg("data end = {}".format(data_end), self._debug) option_data = bytes(_BUFF[pointer:data_end]) + # debug_msg(option_data, self._debug) + # debug_msg("Final pointer = {}".format(pointer), self._debug) return data_end, option_type, option_data debug_msg("Parsing DHCP message.", self._debug) From fafe6e82c9c27ebe7daf7f491ec8bf47d5426905 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Tue, 28 Mar 2023 08:25:11 +0400 Subject: [PATCH 74/80] Fixed bug where timeout was calculated as 2 x time.monotonic + n seconds. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index 20e1de5..ea1a683 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -293,7 +293,7 @@ def _receive_dhcp_response(self, timeout: float) -> int: If the packet is too short, it is discarded and zero is returned. The maximum packet size is limited by the size of the global buffer. - :param float timeout: Seconds to wait for a process to complete. + :param float timeout: time.monotonic at which attempt should timeout. :returns int: The number of bytes stored in the global buffer. """ @@ -303,7 +303,6 @@ def _receive_dhcp_response(self, timeout: float) -> int: buffer = bytearray(b"") bytes_read = 0 debug_msg("+ Beginning to receive…", self._debug) - timeout += time.monotonic() while bytes_read < minimum_packet_length and time.monotonic() < timeout: if self._eth.socket_available(self._wiz_sock, _SNMR_UDP): x = self._eth.read_udp(self._wiz_sock, _BUFF_LENGTH - bytes_read)[1] From da2a88a3bf519e2a28716650a79f010102381126 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 29 Mar 2023 09:41:43 +0300 Subject: [PATCH 75/80] Added debug text that confirms DHCP releases socket. --- adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py index ea1a683..7e53b9c 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -219,6 +219,7 @@ def _socket_release(self) -> None: if self._wiz_sock: self._eth.socket_close(self._wiz_sock) self._wiz_sock = None + debug_msg(" Socket released.", self._debug) def _dhcp_connection_setup(self, timeout: float = 5.0) -> None: """Initialise a UDP socket. From df4efb217ac001db9c877fe41abbf320c28b5c49 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 30 Mar 2023 14:52:59 +0300 Subject: [PATCH 76/80] Fixed potential infinite loop in WIZNET5K.socket_close. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 279cac7..ce39cb7 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -906,11 +906,28 @@ def socket_close(self, socket_num: int) -> None: :param int socket_num: The socket to close. """ debug_msg("*** Closing socket {}".format(socket_num), self._debug) + timeout = time.monotonic() + 5.0 self.write_sncr(socket_num, _CMD_SOCK_CLOSE) + debug_msg(" Waiting for close command to process…", self._debug) while self.read_sncr(socket_num): + if time.monotonic() < timeout: + raise RuntimeError( + "Wiznet5k failed to complete command, status = {}.".format( + self.read_sncr(socket_num) + ) + ) time.sleep(0.0001) + debug_msg(" Waiting for socket to close…", self._debug) + timeout = time.monotonic() + 5.0 while self.read_snsr(socket_num) != SNSR_SOCK_CLOSED: + if time.monotonic() > timeout: + raise RuntimeError( + "Wiznet5k failed to close socket, status = {}.".format( + self.read_snsr(socket_num) + ) + ) time.sleep(0.0001) + debug_msg(" Socket has closed.", self._debug) def socket_disconnect(self, socket_num: int) -> None: """ From 4002d57a360276e32adc533e4ec295128ae8c144 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sat, 1 Apr 2023 12:25:29 +0300 Subject: [PATCH 77/80] Converted read_sncr and read_snsr results to int so that they can actually be False. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index ce39cb7..195d58f 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -909,21 +909,21 @@ def socket_close(self, socket_num: int) -> None: timeout = time.monotonic() + 5.0 self.write_sncr(socket_num, _CMD_SOCK_CLOSE) debug_msg(" Waiting for close command to process…", self._debug) - while self.read_sncr(socket_num): + while self.read_sncr(socket_num)[0]: if time.monotonic() < timeout: raise RuntimeError( "Wiznet5k failed to complete command, status = {}.".format( - self.read_sncr(socket_num) + self.read_sncr(socket_num)[0] ) ) time.sleep(0.0001) debug_msg(" Waiting for socket to close…", self._debug) timeout = time.monotonic() + 5.0 - while self.read_snsr(socket_num) != SNSR_SOCK_CLOSED: + while self.read_snsr(socket_num)[0] != SNSR_SOCK_CLOSED: if time.monotonic() > timeout: raise RuntimeError( "Wiznet5k failed to close socket, status = {}.".format( - self.read_snsr(socket_num) + self.read_snsr(socket_num)[0] ) ) time.sleep(0.0001) From 1e373e4ff671d357d2652d280bed52d468a2c3fc Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Sun, 9 Apr 2023 07:03:37 +0300 Subject: [PATCH 78/80] Added a reset to w5500 chip detection. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 195d58f..3e24502 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -476,6 +476,10 @@ def _detect_and_reset_w5500() -> bool: :return bool: True if a W5500 chip is detected, False if not. """ self._chip_type = "w5500" + self._write_mr(0x80) + if self._read_mr(): + return False + # assert self.sw_reset() == 0, "Chip not reset properly!" self._write_mr(0x08) # assert self._read_mr()[0] == 0x08, "Expected 0x08." From ffb5e32a696f0831bd55efcffbf13620be298203 Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Wed, 26 Apr 2023 08:34:43 +0300 Subject: [PATCH 79/80] Fixed explicit soft reset for w5500. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index 3e24502..fb5c393 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -477,8 +477,8 @@ def _detect_and_reset_w5500() -> bool: """ self._chip_type = "w5500" self._write_mr(0x80) - if self._read_mr(): - return False + while self._read_mr()[0] & 0x80: + pass # assert self.sw_reset() == 0, "Chip not reset properly!" self._write_mr(0x08) From 9231943cbca88e487e44beb9e1f9b87e1ac7003e Mon Sep 17 00:00:00 2001 From: BiffoBear Date: Thu, 27 Apr 2023 03:30:38 +0300 Subject: [PATCH 80/80] Future proofed w5500 initialisation. --- adafruit_wiznet5k/adafruit_wiznet5k.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/adafruit_wiznet5k/adafruit_wiznet5k.py b/adafruit_wiznet5k/adafruit_wiznet5k.py index fb5c393..49b07b9 100644 --- a/adafruit_wiznet5k/adafruit_wiznet5k.py +++ b/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -477,8 +477,9 @@ def _detect_and_reset_w5500() -> bool: """ self._chip_type = "w5500" self._write_mr(0x80) - while self._read_mr()[0] & 0x80: - pass + time.sleep(0.05) + if self._read_mr()[0] & 0x80: + return False # assert self.sw_reset() == 0, "Chip not reset properly!" self._write_mr(0x08)