From d8323bb9a3d1061e926303587c02e2da0a122549 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Thu, 26 Jun 2025 19:26:41 +1200 Subject: [PATCH] Inital ethernet to ws2812 --- adapter/code.py | 117 +++ adapter/lib/adafruit_wiznet5k/__init__.py | 0 .../adafruit_wiznet5k/adafruit_wiznet5k.py | 957 ++++++++++++++++++ .../adafruit_wiznet5k_dhcp.py | 497 +++++++++ .../adafruit_wiznet5k_dns.py | 254 +++++ .../adafruit_wiznet5k_socket.py | 442 ++++++++ .../adafruit_wiznet5k_wsgiserver.py | 203 ++++ adapter/lib/neopixel.py | 167 +++ 8 files changed, 2637 insertions(+) create mode 100644 adapter/code.py create mode 100644 adapter/lib/adafruit_wiznet5k/__init__.py create mode 100644 adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k.py create mode 100644 adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py create mode 100644 adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_dns.py create mode 100644 adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_socket.py create mode 100644 adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py create mode 100644 adapter/lib/neopixel.py diff --git a/adapter/code.py b/adapter/code.py new file mode 100644 index 0000000..b8b2866 --- /dev/null +++ b/adapter/code.py @@ -0,0 +1,117 @@ +import board +import busio +import digitalio +import time +from adafruit_wiznet5k.adafruit_wiznet5k import * +import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket +import neopixel + +SPI1_SCK = board.GP10 +SPI1_TX = board.GP11 +SPI1_RX = board.GP12 +SPI1_CSn = board.GP9 +W5500_RSTn = board.GP13 + +pixel_pin = board.GP28 +num_pixels = 2 + +print("Wiznet5k SimpleServer Test (DHCP)") +# Setup your network configuration below +# random MAC, later should change this value on your vendor ID +MY_MAC = (0x00, 0x01, 0x02, 0x03, 0x04, 0x05) +IP_ADDRESS = (192, 168, 2, 111) +SUBNET_MASK = (255, 255, 0, 0) +GATEWAY_ADDRESS = (192, 168, 2, 1) +DNS_SERVER = (8, 8, 8, 8) + +RED = (255, 0, 0) +YELLOW = (255, 150, 0) +GREEN = (0, 255, 0) +CYAN = (0, 255, 255) +BLUE = (0, 0, 255) +PURPLE = (180, 0, 255) + +led = digitalio.DigitalInOut(board.GP25) +led.direction = digitalio.Direction.OUTPUT + +ethernetRst = digitalio.DigitalInOut(W5500_RSTn) +ethernetRst.direction = digitalio.Direction.OUTPUT + +# For Adafruit Ethernet FeatherWing +cs = digitalio.DigitalInOut(SPI1_CSn) +# For Particle Ethernet FeatherWing +# cs = digitalio.DigitalInOut(board.D5) + +spi_bus = busio.SPI(SPI1_SCK, MOSI=SPI1_TX, MISO=SPI1_RX) + +# Reset W5500 first +ethernetRst.value = False +time.sleep(1) +ethernetRst.value = True + +# # Initialize ethernet interface without DHCP +eth = WIZNET5K(spi_bus, cs, is_dhcp=False, mac=MY_MAC, debug=False) +# # Set network configuration +eth.ifconfig = (IP_ADDRESS, SUBNET_MASK, GATEWAY_ADDRESS, DNS_SERVER) + +# Initialize ethernet interface with DHCP +eth = WIZNET5K(spi_bus, cs, is_dhcp=True, mac=MY_MAC, debug=False) + +print("Chip Version:", eth.chip) +print("MAC Address:", [hex(i) for i in eth.mac_address]) +print("My IP address is:", eth.pretty_ip(eth.ip_address)) + +pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.1, auto_write=False) + +# Initialize a socket for our server +socket.set_interface(eth) +server = socket.socket() # Allocate socket for the server +server_ip = None # IP address of server +server_port = 50007 # Port to listen on +server.bind((server_ip, server_port)) # Bind to IP and Port +server.listen() # Begin listening for incoming clients +print("server listen") + +conn = None + +while True: + if conn is None: + conn, addr = server.accept() # Wait for a connection from a client. + print("socket connected") + print(conn, addr) + else : + if conn.status in ( + SNSR_SOCK_FIN_WAIT, + SNSR_SOCK_CLOSE_WAIT, + ): + print("socket closed") + conn.close() + conn = None + else : + # print("socket established", conn.status) + data = conn.recv() + if data: + print(data) + conn.send(data) # Echo message back to client + # expect data format RED example: 255,0,0 + recvStr = data.decode('UTF-8') + print(recvStr) + color = recvStr.split(",", 2) + print(color) + if len(color) != 3 or not "".join(color).isdigit(): + pass + try: + red = int(color[0]) + green = int(color[1]) + blue = int(color[2]) + pixels.fill((red,green,blue)) + pixels.show() + except: + pass + + + led.value = not led.value + time.sleep(1) + +print("Done!") + diff --git a/adapter/lib/adafruit_wiznet5k/__init__.py b/adapter/lib/adafruit_wiznet5k/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k.py b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k.py new file mode 100644 index 0000000..08a1f01 --- /dev/null +++ b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k.py @@ -0,0 +1,957 @@ +# SPDX-FileCopyrightText: 2010 WIZnet +# SPDX-FileCopyrightText: 2010 Arduino LLC +# SPDX-FileCopyrightText: 2008 Bjoern Hartmann +# SPDX-FileCopyrightText: 2018 Paul Stoffregen +# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries +# SPDX-FileCopyrightText: 2021 Patrick Van Oosterwijck +# SPDX-FileCopyrightText: 2021 Adam Cummick +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_wiznet5k` +================================================================================ + +Pure-Python interface for WIZNET 5k ethernet modules. + +* Author(s): WIZnet, Arduino LLC, Bjoern Hartmann, Paul Stoffregen, Brent Rubell, + Patrick Van Oosterwijck + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +""" +from random import randint +import time +from micropython import const + +from adafruit_bus_device.spi_device import SPIDevice +import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as dhcp +import adafruit_wiznet5k.adafruit_wiznet5k_dns as dns + + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Wiznet5k.git" + + +# Wiznet5k Registers +REG_MR = const(0x0000) # Mode +REG_GAR = const(0x0001) # Gateway IP Address +REG_SUBR = const(0x0005) # Subnet Mask Address +REG_VERSIONR_W5500 = const(0x0039) # W5500 Silicon Version +REG_VERSIONR_W5100S = const(0x0080) # W5100S Silicon Version +REG_SHAR = const(0x0009) # Source Hardware Address +REG_SIPR = const(0x000F) # Source IP Address +REG_PHYCFGR = const(0x002E) # W5500 PHY Configuration +REG_PHYCFGR_W5100S = const(0x003C) # W5100S PHY Configuration + +# Wiznet5k Socket Registers +REG_SNMR = const(0x0000) # Socket n Mode +REG_SNCR = const(0x0001) # Socket n Command +REG_SNIR = const(0x0002) # Socket n Interrupt +REG_SNSR = const(0x0003) # Socket n Status +REG_SNPORT = const(0x0004) # Socket n Source Port +REG_SNDIPR = const(0x000C) # Destination IP Address +REG_SNDPORT = const(0x0010) # Destination Port +REG_SNRX_RSR = const(0x0026) # RX Free Size +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 + +# SNSR Commands +SNSR_SOCK_CLOSED = const(0x00) +SNSR_SOCK_INIT = const(0x13) +SNSR_SOCK_LISTEN = const(0x14) +SNSR_SOCK_SYNSENT = const(0x15) +SNSR_SOCK_SYNRECV = const(0x16) +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_LAST_ACK = const(0x1D) +SNSR_SOCK_UDP = const(0x22) +SNSR_SOCK_IPRAW = const(0x32) +SNSR_SOCK_MACRAW = const(0x42) +SNSR_SOCK_PPPOE = const(0x5F) + +# Sock Commands (CMD) +CMD_SOCK_OPEN = const(0x01) +CMD_SOCK_LISTEN = const(0x02) +CMD_SOCK_CONNECT = const(0x04) +CMD_SOCK_DISCON = const(0x08) +CMD_SOCK_CLOSE = const(0x10) +CMD_SOCK_SEND = const(0x20) +CMD_SOCK_SEND_MAC = const(0x21) +CMD_SOCK_SEND_KEEP = const(0x22) +CMD_SOCK_RECV = const(0x40) + +# Socket n Interrupt Register +SNIR_SEND_OK = const(0x10) +SNIR_TIMEOUT = const(0x08) +SNIR_RECV = const(0x04) +SNIR_DISCON = const(0x02) +SNIR_CON = const(0x01) + +CH_SIZE = const(0x100) +SOCK_SIZE = const(0x800) # MAX W5k socket size +SOCK_MASK = const(0x7FF) +# Register commands +MR_RST = const(0x80) # Mode Register RST +# Socket mode register +SNMR_CLOSE = const(0x00) +SNMR_TCP = const(0x21) +SNMR_UDP = const(0x02) +SNMR_IPRAW = const(0x03) +SNMR_MACRAW = const(0x04) +SNMR_PPPOE = const(0x05) + +MAX_PACKET = const(4000) +LOCAL_PORT = const(0x400) +# Default hardware MAC address +DEFAULT_MAC = (0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED) + +# Maximum number of sockets to support, differs between chip versions. +W5200_W5500_MAX_SOCK_NUM = const(0x08) +W5100_MAX_SOCK_NUM = const(0x04) +SOCKET_INVALID = const(255) + +# UDP socket struct. +UDP_SOCK = {"bytes_remaining": 0, "remote_ip": 0, "remote_port": 0} + +# Source ports in use +SRC_PORTS = [0] * W5200_W5500_MAX_SOCK_NUM + + +class WIZNET5K: # pylint: disable=too-many-public-methods + """Interface for WIZNET5K module. + :param ~busio.SPI spi_bus: The SPI bus the Wiznet module is connected to. + :param ~digitalio.DigitalInOut cs: Chip select pin. + :param ~digitalio.DigitalInOut rst: Optional reset pin. + :param bool is_dhcp: Whether to start DHCP automatically or not. + :param list mac: The Wiznet's MAC Address. + :param str hostname: The desired hostname, with optional {} to fill in MAC. + :param int dhcp_timeout: Timeout in seconds for DHCP response. + :param bool debug: Enable debugging output. + + """ + + TCP_MODE = const(0x21) + UDP_MODE = const(0x02) + TLS_MODE = const(0x03) # This is NOT currently implemented + + # pylint: disable=too-many-arguments + def __init__( + self, + spi_bus, + cs, + reset=None, + is_dhcp=True, + mac=DEFAULT_MAC, + hostname=None, + dhcp_timeout=30, + debug=False, + ): + self._debug = debug + self._chip_type = None + self._device = SPIDevice(spi_bus, cs, baudrate=8000000, polarity=0, phase=0) + # init c.s. + self._cs = cs + + # reset wiznet module prior to initialization + if reset: + reset.value = True + time.sleep(0.1) + reset.value = False + time.sleep(0.1) + + # Buffer for reading params from module + self._pbuff = bytearray(8) + self._rxbuf = bytearray(MAX_PACKET) + + # attempt to initialize the module + self._ch_base_msb = 0 + assert self._w5100_init() == 1, "Failed to initialize WIZnet module." + # Set MAC address + self.mac_address = mac + self.src_port = 0 + self._dns = 0 + + # First, wait link status is on + # to avoid the code during DHCP, socket listen, connect ... - assert self.link_status, "Ethernet cable disconnected!" + start_time = time.monotonic() + while True: + if self.link_status or ((time.monotonic() - start_time) > 5): + break + time.sleep(1) + if self._debug: + print("My Link is:", self.link_status) + self._dhcp_client = None + + # Set DHCP + if is_dhcp: + ret = self.set_dhcp(hostname, dhcp_timeout) + if ret != 0: + self._dhcp_client = None + assert ret == 0, "Failed to configure DHCP Server!" + + def set_dhcp(self, hostname=None, response_timeout=30): + """Initializes the DHCP client and attempts to retrieve + and set network configuration from the DHCP server. + Returns 0 if DHCP configured, -1 otherwise. + :param str hostname: The desired hostname, with optional {} to fill in MAC. + :param int response_timeout: Time to wait for server to return packet, in seconds. + + """ + if self._debug: + print("* Initializing DHCP") + + # 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 + ) + ) + return 0 + return -1 + + def maintain_dhcp_lease(self): + """Maintain DHCP lease""" + if self._dhcp_client is not None: + self._dhcp_client.maintain_dhcp_lease() + + def get_host_by_name(self, hostname): + """Convert a hostname to a packed 4-byte IP Address. + Returns a 4 bytearray. + """ + if self._debug: + print("* Get host by name") + 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) + assert ret != -1, "Failed to resolve hostname!" + return ret + + @property + def max_sockets(self): + """Returns max number of sockets supported by chip.""" + if self._chip_type == "w5500": + return W5200_W5500_MAX_SOCK_NUM + elif self._chip_type == "w5100s": + return W5100_MAX_SOCK_NUM + return -1 + + @property + def chip(self): + """Returns the chip type.""" + return self._chip_type + + @property + def ip_address(self): + """Returns the configured IP address.""" + return self.read(REG_SIPR, 0x00, 4) + + def pretty_ip(self, ip): # pylint: disable=no-self-use, invalid-name + """Converts a bytearray IP address to a + dotted-quad string for printing + + """ + return "%d.%d.%d.%d" % (ip[0], ip[1], ip[2], ip[3]) + + def unpretty_ip(self, ip): # pylint: disable=no-self-use, invalid-name + """Converts a dotted-quad string to a bytearray IP address""" + octets = [int(x) for x in ip.split(".")] + return bytes(octets) + + @property + def mac_address(self): + """Returns the hardware's MAC address.""" + return self.read(REG_SHAR, 0x00, 6) + + @mac_address.setter + def mac_address(self, address): + """Sets the hardware MAC address. + :param tuple address: Hardware MAC address. + + """ + self.write(REG_SHAR, 0x04, address) + + def pretty_mac(self, mac): # pylint: disable=no-self-use, invalid-name + """Converts a bytearray MAC address to a + dotted-quad string for printing + + """ + return "%s:%s:%s:%s:%s:%s" % ( + hex(mac[0]), + hex(mac[1]), + hex(mac[2]), + hex(mac[3]), + hex(mac[4]), + hex(mac[5]), + ) + + def remote_ip(self, socket_num): + """Returns the IP address of the host who sent the current incoming packet. + :param int socket num: Desired socket. + + """ + if socket_num >= self.max_sockets: + return self._pbuff + for octet in range(0, 4): + self._pbuff[octet] = self._read_socket(socket_num, REG_SNDIPR + octet)[0] + return self.pretty_ip(self._pbuff) + + @property + def link_status(self): + """ "Returns if the PHY is connected.""" + if self._chip_type == "w5500": + data = self.read(REG_PHYCFGR, 0x00) + return data[0] & 0x01 + elif self._chip_type == "w5100s": + data = self.read(REG_PHYCFGR_W5100S, 0x00) + return data[0] & 0x01 + return 0 + + def remote_port(self, socket_num): + """Returns the port of the host who sent the current incoming packet.""" + if socket_num >= self.max_sockets: + return self._pbuff + for octet in range(0, 2): + self._pbuff[octet] = self._read_socket(socket_num, REG_SNDPORT + octet)[0] + return int((self._pbuff[0] << 8) | self._pbuff[0]) + + @property + def ifconfig(self): + """Returns the network configuration as a tuple.""" + return ( + self.ip_address, + self.read(REG_SUBR, 0x00, 4), + self.read(REG_GAR, 0x00, 4), + self._dns, + ) + + @ifconfig.setter + def ifconfig(self, params): + """Sets network configuration to provided tuple in format: + (ip_address, subnet_mask, gateway_address, dns_server). + + """ + ip_address, subnet_mask, gateway_address, dns_server = params + + self.write(REG_SIPR, 0x04, ip_address) + self.write(REG_SUBR, 0x04, subnet_mask) + self.write(REG_GAR, 0x04, gateway_address) + + self._dns = dns_server + + def _w5100_init(self): + """Initializes and detects a wiznet5k module.""" + time.sleep(1) + self._cs.switch_to_output() + self._cs.value = 1 + + # Detect if chip is Wiznet W5500 + if self.detect_w5500() == 1: + # perform w5500 initialization + for i in range(0, W5200_W5500_MAX_SOCK_NUM): + ctrl_byte = 0x0C + (i << 5) + self.write(0x1E, ctrl_byte, 2) + self.write(0x1F, ctrl_byte, 2) + else: + # Detect if chip is Wiznet W5100S + if self.detect_w5100s() == 1: + pass + else: + return 0 + return 1 + + def detect_w5500(self): + """Detects W5500 chip.""" + self._chip_type = "w5500" + assert self.sw_reset() == 0, "Chip not reset properly!" + self._write_mr(0x08) + # assert self._read_mr()[0] == 0x08, "Expected 0x08." + if self._read_mr()[0] != 0x08 : return -1 + + self._write_mr(0x10) + # assert self._read_mr()[0] == 0x10, "Expected 0x10." + if self._read_mr()[0] != 0x10 : return -1 + + self._write_mr(0x00) + # assert self._read_mr()[0] == 0x00, "Expected 0x00." + if self._read_mr()[0] != 0x00 : return -1 + + if self.read(REG_VERSIONR_W5500, 0x00)[0] != 0x04: + return -1 + # self._chip_type = "w5500" + # self._ch_base_msb = 0x10 + return 1 + + def detect_w5100s(self): + """Detects W5100S chip.""" + self._chip_type = "w5100s" + # sw reset + assert self.sw_reset() == 0, "Chip not reset properly!" + if self.read(REG_VERSIONR_W5100S, 0x00)[0] != 0x51: + return -1 + + self._ch_base_msb = 0x0400 + return 1 + + def sw_reset(self): + """Performs a soft-reset on a Wiznet chip + by writing to its MR register reset bit. + + """ + mode_reg = self._read_mr() + self._write_mr(0x80) + mode_reg = self._read_mr() + + # W5100S case => 0x03 + if (mode_reg[0] != 0x00) and (mode_reg[0] != 0x03): + return -1 + return 0 + + def _read_mr(self): + """Reads from the Mode Register (MR).""" + res = self.read(REG_MR, 0x00) + return res + + def _write_mr(self, data): + """Writes to the mode register (MR). + :param int data: Data to write to the mode register. + + """ + self.write(REG_MR, 0x04, data) + + def read(self, addr, callback, length=1, buffer=None): + """Reads data from a register address. + :param int addr: Register address. + + """ + with self._device as bus_device: + if self._chip_type == "w5500": + bus_device.write(bytes([addr >> 8])) # pylint: disable=no-member + bus_device.write(bytes([addr & 0xFF])) # pylint: disable=no-member + bus_device.write(bytes([callback])) # pylint: disable=no-member + else : + #if self._chip_type == "w5100s": + bus_device.write(bytes([0x0F])) # pylint: disable=no-member + 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 + + def write(self, addr, callback, data): + """Write data to a register address. + :param int addr: Destination address. + :param int callback: Callback reference. + :param int data: Data to write, as an integer. + :param bytearray data: Data to write, as a bytearray. + + """ + with self._device as bus_device: + if self._chip_type == "w5500": + bus_device.write(bytes([addr >> 8])) # pylint: disable=no-member + bus_device.write(bytes([addr & 0xFF])) # pylint: disable=no-member + bus_device.write(bytes([callback])) # pylint: disable=no-member + else : + #if self._chip_type == "w5100s": + bus_device.write(bytes([0xF0])) # pylint: disable=no-member + bus_device.write(bytes([addr >> 8])) # pylint: disable=no-member + bus_device.write(bytes([addr & 0xFF])) # pylint: disable=no-member + + if hasattr(data, "from_bytes"): + bus_device.write(bytes([data])) # pylint: disable=no-member + else: + for i, _ in enumerate(data): + bus_device.write(bytes([data[i]])) # pylint: disable=no-member + + # Socket-Register API + def udp_remaining(self): + """Returns amount of bytes remaining in a udp socket.""" + if self._debug: + print("* UDP Bytes Remaining: ", UDP_SOCK["bytes_remaining"]) + return UDP_SOCK["bytes_remaining"] + + def socket_available(self, socket_num, sock_type=SNMR_TCP): + """Returns the amount of bytes to be read from the socket. + + :param int socket_num: Desired socket to return bytes from. + :param int sock_type: Socket type, defaults to TCP. + """ + if self._debug: + print( + "* socket_available called on socket {}, protocol {}".format( + socket_num, sock_type + ) + ) + assert socket_num <= self.max_sockets, "Provided socket exceeds max_sockets." + + res = self._get_rx_rcv_size(socket_num) + + if sock_type == SNMR_TCP: + return res + if res > 0: + if UDP_SOCK["bytes_remaining"]: + return UDP_SOCK["bytes_remaining"] + # parse the udp rx packet + # read the first 8 header bytes + ret, self._pbuff = self.socket_read(socket_num, 8) + if ret > 0: + UDP_SOCK["remote_ip"] = self._pbuff[:4] + UDP_SOCK["remote_port"] = (self._pbuff[4] << 8) + self._pbuff[5] + UDP_SOCK["bytes_remaining"] = (self._pbuff[6] << 8) + self._pbuff[7] + ret = UDP_SOCK["bytes_remaining"] + return ret + return 0 + + def socket_status(self, socket_num): + """Returns the socket connection status. Can be: SNSR_SOCK_CLOSED, + SNSR_SOCK_INIT, SNSR_SOCK_LISTEN, SNSR_SOCK_SYNSENT, SNSR_SOCK_SYNRECV, + SNSR_SYN_SOCK_ESTABLISHED, SNSR_SOCK_FIN_WAIT, SNSR_SOCK_CLOSING, + SNSR_SOCK_TIME_WAIT, SNSR_SOCK_CLOSE_WAIT, SNSR_LAST_ACK, + SNSR_SOCK_UDP, SNSR_SOCK_IPRAW, SNSR_SOCK_MACRAW, SNSR_SOCK_PPOE. + """ + return self._read_snsr(socket_num) + + def socket_connect(self, socket_num, dest, port, conn_mode=SNMR_TCP): + """Open and verify we've connected a socket to a dest IP address + or hostname. By default, we use 'conn_mode'= SNMR_TCP but we + may also use SNMR_UDP. + """ + assert self.link_status, "Ethernet cable disconnected!" + if self._debug: + print( + "* w5k socket connect, protocol={}, port={}, ip={}".format( + conn_mode, port, self.pretty_ip(dest) + ) + ) + # initialize a socket and set the mode + res = self.socket_open(socket_num, conn_mode=conn_mode) + if res == 1: + raise RuntimeError("Failed to initalize a connection with the socket.") + + # set socket destination IP and 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: + # 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]) + if self.socket_status(socket_num)[0] == SNSR_SOCK_CLOSED: + raise RuntimeError("Failed to establish connection.") + elif conn_mode == SNMR_UDP: + UDP_SOCK["bytes_remaining"] = 0 + return 1 + + def _send_socket_cmd(self, socket, cmd): + self._write_sncr(socket, cmd) + while self._read_sncr(socket) != b"\x00": + if self._debug: + print("waiting for sncr to clear...") + + def get_socket(self): + """Requests, allocates and returns a socket from the W5k + chip. Returned socket number may not exceed max_sockets. + """ + if self._debug: + print("*** Get socket") + + sock = SOCKET_INVALID + for _sock in range(self.max_sockets): + status = self.socket_status(_sock)[0] + if status == SNSR_SOCK_CLOSED: + sock = _sock + break + + if self._debug: + print("Allocated socket #{}".format(sock)) + return sock + + def socket_listen(self, socket_num, port, conn_mode=SNMR_TCP): + """Start listening on a socket (default TCP mode). + :parm int socket_num: socket number + :parm int port: port to listen on + :parm int conn_mode: connection mode SNMR_TCP (default) or SNMR_UDP + """ + assert self.link_status, "Ethernet cable disconnected!" + if self._debug: + print( + "* Listening on port={}, ip={}".format( + port, self.pretty_ip(self.ip_address) + ) + ) + # Initialize a socket and set the mode + self.src_port = port + res = self.socket_open(socket_num, conn_mode=conn_mode) + self.src_port = 0 + if res == 1: + raise RuntimeError("Failed to initalize the socket.") + # Send listen command + self._send_socket_cmd(socket_num, CMD_SOCK_LISTEN) + # Wait until ready + status = [SNSR_SOCK_CLOSED] + while status[0] not in (SNSR_SOCK_LISTEN, SNSR_SOCK_ESTABLISHED, SNSR_SOCK_UDP): + status = self._read_snsr(socket_num) + if status[0] == SNSR_SOCK_CLOSED: + raise RuntimeError("Listening socket closed.") + + def socket_accept(self, socket_num): + """Gets the dest IP and port from an incoming connection. + Returns the next socket number so listening can continue + :parm int socket_num: socket number + """ + 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 + ) + ) + return next_socknum, (dest_ip, dest_port) + + def socket_open(self, socket_num, conn_mode=SNMR_TCP): + """Opens a TCP or UDP socket. By default, we use + 'conn_mode'=SNMR_TCP but we may also use SNMR_UDP. + """ + assert self.link_status, "Ethernet cable disconnected!" + if self._debug: + print("*** Opening socket %d" % socket_num) + status = self._read_snsr(socket_num)[0] + if status in ( + SNSR_SOCK_CLOSED, + SNSR_SOCK_TIME_WAIT, + SNSR_SOCK_FIN_WAIT, + SNSR_SOCK_CLOSE_WAIT, + SNSR_SOCK_CLOSING, + SNSR_SOCK_UDP, + ): + if self._debug: + print("* Opening W5k Socket, protocol={}".format(conn_mode)) + time.sleep(0.00025) + + self._write_snmr(socket_num, conn_mode) + self._write_snir(socket_num, 0xFF) + + if self.src_port > 0: + # write to socket source 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) + SRC_PORTS[socket_num] = s_port + + # open socket + 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 + ), "Could not open socket in TCP or UDP mode." + return 0 + return 1 + + def socket_close(self, socket_num): + """Closes a socket.""" + if self._debug: + print("*** Closing socket #%d" % socket_num) + self._write_sncr(socket_num, CMD_SOCK_CLOSE) + self._read_sncr(socket_num) + + def socket_disconnect(self, socket_num): + """Disconnect a TCP connection.""" + if self._debug: + print("*** Disconnecting socket #%d" % socket_num) + self._write_sncr(socket_num, CMD_SOCK_DISCON) + self._read_sncr(socket_num) + + def socket_read(self, socket_num, length): + """Reads data from a socket into a buffer. + Returns buffer. + + """ + assert self.link_status, "Ethernet cable disconnected!" + assert socket_num <= self.max_sockets, "Provided socket exceeds max_sockets." + + # Check if there is data available on the socket + 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): + # remote end closed its side of the connection, EOF state + ret = 0 + resp = 0 + else: + # connection is alive, no data waiting to be read + ret = -1 + resp = -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)) + # Read the starting save address of the received data + ptr = self._read_snrx_rd(socket_num) + + if self._chip_type == "w5500": + # Read data from the starting address of snrx_rd + ctrl_byte = 0x18 + (socket_num << 5) + + resp = self.read(ptr, ctrl_byte, ret) + else : + #if self._chip_type == "w5100s": + offset = ptr & SOCK_MASK + src_addr = offset + (socket_num * SOCK_SIZE + 0x6000) + if (offset + ret > SOCK_SIZE) : + size = SOCK_SIZE - offset + resp1 = self.read(src_addr, 0x00, size) + size = ret - size + src_addr = (socket_num * SOCK_SIZE + 0x6000) + resp2 = self.read(src_addr, 0x00, size) + resp = resp1 + resp2 + else : + resp = self.read(src_addr, 0x00, ret) + + # After reading the received data, update Sn_RX_RD to the increased + # value as many as the reading size. + ptr = (ptr + ret) & 0xFFFF + 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) + return ret, resp + + def read_udp(self, socket_num, length): + """Read UDP socket's remaining bytes.""" + if UDP_SOCK["bytes_remaining"] > 0: + if UDP_SOCK["bytes_remaining"] <= length: + ret, resp = self.socket_read(socket_num, UDP_SOCK["bytes_remaining"]) + else: + ret, resp = self.socket_read(socket_num, length) + if ret > 0: + UDP_SOCK["bytes_remaining"] -= ret + return ret, resp + return -1 + + def socket_write(self, socket_num, buffer, timeout=0): + """Writes a bytearray to a provided socket.""" + 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: + ret = len(buffer) + stamp = time.monotonic() + + # 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) + status = self.socket_status(socket_num)[0] + if status not in (SNSR_SOCK_ESTABLISHED, SNSR_SOCK_CLOSE_WAIT) or ( + timeout and time.monotonic() - stamp > timeout + ): + ret = 0 + break + + # Read the starting address for saving the transmitting data. + ptr = self._read_sntx_wr(socket_num) + 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) + + else : + #if self._chip_type == "w5100s": + dst_addr = offset + (socket_num * SOCK_SIZE + 0x4000) + + if (offset + ret > SOCK_SIZE) : + size = SOCK_SIZE - offset + 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 + ptr = (ptr + ret) & 0xFFFF + self._write_sntx_wr(socket_num, ptr) + + self._write_sncr(socket_num, CMD_SOCK_SEND) + 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: + if ( + self.socket_status(socket_num)[0] + in ( + SNSR_SOCK_CLOSED, + SNSR_SOCK_TIME_WAIT, + 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 + time.sleep(0.01) + + self._write_snir(socket_num, SNIR_SEND_OK) + return ret + + # Socket-Register Methods + + def _get_rx_rcv_size(self, sock): + """Get size of recieved and saved in socket buffer.""" + val = 0 + val_1 = self._read_snrx_rsr(sock) + while val != val_1: + val_1 = self._read_snrx_rsr(sock) + if val_1 != 0: + val = self._read_snrx_rsr(sock) + return int.from_bytes(val, "b") + + def _get_tx_free_size(self, sock): + """Get free size of sock's tx buffer block.""" + val = 0 + val_1 = self._read_sntx_fsr(sock) + while val != val_1: + val_1 = self._read_sntx_fsr(sock) + if val_1 != 0: + val = self._read_sntx_fsr(sock) + return int.from_bytes(val, "b") + + def _read_snrx_rd(self, sock): + 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, data): + 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, data): + 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): + 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): + 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): + data = self._read_socket(sock, REG_SNRX_RSR) + data += self._read_socket(sock, REG_SNRX_RSR + 1) + return data + + def _write_sndipr(self, sock, ip_addr): + """Writes to socket destination IP Address.""" + for octet in range(0, 4): + self._write_socket(sock, REG_SNDIPR + octet, ip_addr[octet]) + + def _write_sndport(self, sock, port): + """Writes to socket destination port.""" + self._write_socket(sock, REG_SNDPORT, port >> 8) + self._write_socket(sock, REG_SNDPORT + 1, port & 0xFF) + + def _read_snsr(self, sock): + """Reads Socket n Status Register.""" + return self._read_socket(sock, REG_SNSR) + + def _write_snmr(self, sock, protocol): + """Write to Socket n Mode Register.""" + self._write_socket(sock, REG_SNMR, protocol) + + def _write_snir(self, sock, data): + """Write to Socket n Interrupt Register.""" + self._write_socket(sock, REG_SNIR, data) + + def _write_sock_port(self, sock, port): + """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, data): + self._write_socket(sock, REG_SNCR, data) + + def _read_sncr(self, sock): + return self._read_socket(sock, REG_SNCR) + + def _read_snmr(self, sock): + return self._read_socket(sock, REG_SNMR) + + def _write_socket(self, sock, address, data): + """Write to a W5k socket register.""" + if self._chip_type == "w5500": + cntl_byte = (sock << 5) + 0x0C + return self.write(address, cntl_byte, data) + else : + #if self._chip_type == "w5100s": + cntl_byte = 0 + return self.write(self._ch_base_msb + sock * CH_SIZE + address, cntl_byte, data) + + def _read_socket(self, sock, address): + """Read a W5k socket register.""" + if self._chip_type == "w5500": + cntl_byte = (sock << 5) + 0x08 + return self.read(address, cntl_byte) + else : + #if self._chip_type == "w5100s": + cntl_byte = 0 + return self.read(self._ch_base_msb + sock * CH_SIZE + address, cntl_byte) diff --git a/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py new file mode 100644 index 0000000..b5768fb --- /dev/null +++ b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py @@ -0,0 +1,497 @@ +# SPDX-FileCopyrightText: 2009 Jordan Terell (blog.jordanterrell.com) +# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries +# SPDX-FileCopyrightText: 2021 Patrick Van Oosterwijck @ Silicognition LLC +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_wiznet5k_dhcp` +================================================================================ + +Pure-Python implementation of Jordan Terrell's DHCP library v0.3 + +* Author(s): Jordan Terrell, Brent Rubell + +""" +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, htons + + +# 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) + +# DHCP wait time between attempts +DHCP_WAIT_TIME = const(60) + +# DHCP Message Types +DHCP_DISCOVER = const(1) +DHCP_OFFER = const(2) +DHCP_REQUEST = const(3) +DHCP_DECLINE = const(4) +DHCP_ACK = const(5) +DHCP_NAK = const(6) +DHCP_RELEASE = const(7) +DHCP_INFORM = const(8) + +# DHCP Message OP Codes +DHCP_BOOT_REQUEST = const(0x01) +DHCP_BOOT_REPLY = const(0x02) + +DHCP_HTYPE10MB = const(0x01) +DHCP_HTYPE100MB = const(0x02) + +DHCP_HLENETHERNET = const(0x06) +DHCP_HOPS = const(0x00) + +MAGIC_COOKIE = const(0x63825363) +MAX_DHCP_OPT = const(0x10) + +# Default DHCP Server port +DHCP_SERVER_PORT = const(67) +# DHCP Lease Time, in seconds +DEFAULT_LEASE_TIME = const(900) +BROADCAST_SERVER_ADDR = (255, 255, 255, 255) + +# 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 + +# Packet buffer +_BUFF = bytearray(318) + + +class DHCP: + """W5k DHCP Client implementation. + :param eth: Wiznet 5k object + :param list mac_address: Hardware MAC. + :param str hostname: The desired hostname, with optional {} to fill in MAC. + :param int response_timeout: DHCP Response timeout. + :param bool debug: Enable debugging output. + + """ + + # pylint: disable=too-many-arguments, too-many-instance-attributes, invalid-name + def __init__( + self, eth, mac_address, hostname=None, response_timeout=30, debug=False + ): + self._debug = debug + self._response_timeout = response_timeout + self._mac_address = mac_address + + # Set socket interface + socket.set_interface(eth) + self._eth = eth + self._sock = None + + # DHCP state machine + self._dhcp_state = STATE_DHCP_START + self._initial_xid = 0 + self._transaction_id = 0 + self._start_time = 0 + + # DHCP server configuration + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + self.local_ip = 0 + self.gateway_ip = 0 + self.subnet_mask = 0 + self.dns_server_ip = 0 + + # Lease configuration + self._lease_time = 0 + self._last_lease_time = 0 + self._renew_in_sec = 0 + self._rebind_in_sec = 0 + self._t1 = 0 + self._t2 = 0 + + # Select an initial transaction id + self._transaction_id = randint(1, 0x7FFFFFFF) + + # Host name + mac_string = "".join("{:02X}".format(o) for o in mac_address) + self._hostname = bytes( + (hostname or "WIZnet{}").split(".")[0].format(mac_string)[:42], "utf-8" + ) + + # pylint: disable=too-many-statements + def send_dhcp_message(self, state, time_elapsed, renew=False): + """Assemble and send a DHCP message packet to a socket. + :param int state: DHCP Message state. + :param float time_elapsed: Number of seconds elapsed since DHCP process started + :param bool renew: Set True for renew and rebind + """ + _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, "l") + _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, "b") + _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 in range(0, len(self._mac_address)): + _BUFF[246 + mac] = self._mac_address[mac] + + # 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): + """Parse DHCP response from DHCP server. + Returns 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], "l") != 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], "l") + 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], "l") + 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], "l") + 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): + """DHCP state machine without wait loops to enable cooperative multi tasking + This state machine is used both by the initial blocking lease request and + the non-blocking DHCP maintenance function""" + if self._eth.link_status: + if self._dhcp_state == STATE_DHCP_DISCONN: + self._dhcp_state = STATE_DHCP_START + else: + if self._dhcp_state != STATE_DHCP_DISCONN: + self._dhcp_state = STATE_DHCP_DISCONN + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + self._last_lease_time = 0 + reset_ip = (0, 0, 0, 0) + self._eth.ifconfig = (reset_ip, reset_ip, reset_ip, reset_ip) + if self._sock is not None: + self._sock.close() + self._sock = None + + if self._dhcp_state == STATE_DHCP_START: + self._start_time = time.monotonic() + self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF + try: + self._sock = socket.socket(type=socket.SOCK_DGRAM) + except RuntimeError: + if self._debug: + print("* DHCP: 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 = self.parse_dhcp_response() + if msg_type == DHCP_OFFER: + # Check if transaction ID matches, otherwise it may be an offer + # for another device + if htonl(self._transaction_id) == int.from_bytes(xid, "l"): + if self._debug: + print( + "* DHCP: Send request to {}".format(self.dhcp_server_ip) + ) + self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF + self.send_dhcp_message( + DHCP_REQUEST, (time.monotonic() - self._start_time) + ) + self._dhcp_state = STATE_DHCP_REQUEST + else: + if self._debug: + print("* DHCP: Received OFFER with non-matching xid") + else: + if self._debug: + print("* DHCP: Received DHCP Message is not OFFER") + + elif self._dhcp_state == STATE_DHCP_REQUEST: + if self._sock.available(): + if self._debug: + print("* DHCP: Parsing ACK") + msg_type, xid = self.parse_dhcp_response() + # Check if transaction ID matches, otherwise it may be + # for another device + if htonl(self._transaction_id) == int.from_bytes(xid, "l"): + if msg_type == DHCP_ACK: + if self._debug: + print("* DHCP: Successful lease") + self._sock.close() + self._sock = None + self._dhcp_state = STATE_DHCP_LEASED + self._last_lease_time = self._start_time + if self._lease_time == 0: + self._lease_time = DEFAULT_LEASE_TIME + if self._t1 == 0: + # T1 is 50% of _lease_time + self._t1 = self._lease_time >> 1 + if self._t2 == 0: + # T2 is 87.5% of _lease_time + self._t2 = self._lease_time - (self._lease_time >> 3) + self._renew_in_sec = self._t1 + self._rebind_in_sec = self._t2 + self._eth.ifconfig = ( + self.local_ip, + self.subnet_mask, + self.gateway_ip, + self.dns_server_ip, + ) + gc.collect() + else: + if self._debug: + print("* DHCP: Received DHCP Message is not ACK") + else: + if self._debug: + print("* DHCP: Received non-matching xid") + + elif self._dhcp_state == STATE_DHCP_WAIT: + if time.monotonic() > (self._start_time + DHCP_WAIT_TIME): + if self._debug: + print("* DHCP: Begin retry") + self._dhcp_state = STATE_DHCP_START + if time.monotonic() > (self._last_lease_time + self._rebind_in_sec): + self.dhcp_server_ip = BROADCAST_SERVER_ADDR + if time.monotonic() > (self._last_lease_time + self._lease_time): + reset_ip = (0, 0, 0, 0) + self._eth.ifconfig = (reset_ip, reset_ip, reset_ip, reset_ip) + + elif self._dhcp_state == STATE_DHCP_LEASED: + if time.monotonic() > (self._last_lease_time + self._renew_in_sec): + self._dhcp_state = STATE_DHCP_START + if self._debug: + print("* DHCP: Time to renew lease") + + if ( + self._dhcp_state == STATE_DHCP_DISCOVER + or self._dhcp_state == STATE_DHCP_REQUEST + ) and time.monotonic() > (self._start_time + self._response_timeout): + self._dhcp_state = STATE_DHCP_WAIT + if self._sock is not None: + self._sock.close() + self._sock = None + + def request_dhcp_lease(self): + """Request to renew or acquire a DHCP lease.""" + if self._dhcp_state == STATE_DHCP_LEASED or self._dhcp_state == STATE_DHCP_WAIT: + self._dhcp_state = STATE_DHCP_START + + while ( + self._dhcp_state != STATE_DHCP_LEASED + and self._dhcp_state != STATE_DHCP_WAIT + ): + self._dhcp_state_machine() + + return self._dhcp_state == STATE_DHCP_LEASED + + def maintain_dhcp_lease(self): + """Maintain DHCP lease""" + self._dhcp_state_machine() diff --git a/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_dns.py b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_dns.py new file mode 100644 index 0000000..84a8791 --- /dev/null +++ b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_dns.py @@ -0,0 +1,254 @@ +# SPDX-FileCopyrightText: 2009-2010 MCQN Ltd +# SPDX-FileCopyrightText: Brent Rubell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_wiznet5k_dns` +================================================================================ + +Pure-Python implementation of the Arduino DNS client for WIZnet 5k-based +ethernet modules. + +* Author(s): MCQN Ltd, Brent Rubell + +""" +import time +from random import getrandbits +from micropython import const +import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket +from adafruit_wiznet5k.adafruit_wiznet5k_socket import htons + + +QUERY_FLAG = const(0x00) +OPCODE_STANDARD_QUERY = const(0x00) +RECURSION_DESIRED_FLAG = 1 << 8 + +TYPE_A = const(0x0001) +CLASS_IN = const(0x0001) +DATA_LEN = const(0x0004) + +# Return codes for gethostbyname +SUCCESS = const(1) +TIMED_OUT = const(-1) +INVALID_SERVER = const(-2) +TRUNCATED = const(-3) +INVALID_RESPONSE = const(-4) + +DNS_PORT = const(0x35) # port used for DNS request + + +class DNS: + """W5K DNS implementation. + + :param iface: Network interface + """ + + def __init__(self, iface, dns_address, debug=False): + self._debug = debug + self._iface = iface + socket.set_interface(iface) + self._sock = socket.socket(type=socket.SOCK_DGRAM) + self._sock.settimeout(1) + + self._dns_server = dns_address + self._host = 0 + self._request_id = 0 # request identifier + self._pkt_buf = bytearray() + + def gethostbyname(self, hostname): + """Translate a host name to IPv4 address format. + :param str hostname: Desired host name to connect to. + + Returns the IPv4 address as a bytearray if successful, -1 otherwise. + """ + if self._dns_server is None: + return INVALID_SERVER + self._host = hostname + # build DNS request packet + self._build_dns_header() + self._build_dns_question() + + # Send DNS request packet + self._sock.bind((None, DNS_PORT)) + self._sock.connect((self._dns_server, DNS_PORT)) + if self._debug: + print("* DNS: Sending request packet...") + self._sock.send(self._pkt_buf) + + # wait and retry 3 times for a response + retries = 0 + addr = -1 + while (retries < 5) and (addr == -1): + addr = self._parse_dns_response() + if addr == -1 and self._debug: + print("* DNS ERROR: Failed to resolve DNS response, retrying...") + retries += 1 + + self._sock.close() + return addr + + def _parse_dns_response( + self, + ): # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements, too-many-locals + """Receives and parses DNS query response. + Returns desired hostname address if obtained, -1 otherwise. + + """ + # wait for a response + start_time = time.monotonic() + packet_sz = self._sock.available() + while packet_sz <= 0: + packet_sz = self._sock.available() + if (time.monotonic() - start_time) > 1.0: + if self._debug: + print("* DNS ERROR: Did not receive DNS response!") + return -1 + time.sleep(0.05) + # recv packet into buf + self._pkt_buf = self._sock.recv() + + if self._debug: + print("DNS Packet Received: ", self._pkt_buf) + + # Validate request identifier + xid = int.from_bytes(self._pkt_buf[0:2], "l") + if not xid == self._request_id: + if self._debug: + print( + "* DNS ERROR: Received request identifer {} \ + does not match expected {}".format( + xid, self._request_id + ) + ) + return -1 + # Validate flags + flags = int.from_bytes(self._pkt_buf[2:4], "l") + if not flags in (0x8180, 0x8580): + if self._debug: + print("* DNS ERROR: Invalid flags, ", flags) + return -1 + # Number of questions + qr_count = int.from_bytes(self._pkt_buf[4:6], "l") + if not qr_count >= 1: + if self._debug: + print("* DNS ERROR: Question count >=1, ", qr_count) + return -1 + # Number of answers + an_count = int.from_bytes(self._pkt_buf[6:8], "l") + if self._debug: + print("* DNS Answer Count: ", an_count) + if not an_count >= 1: + return -1 + + # Parse query + ptr = 12 + name_len = 1 + while name_len > 0: + # read the length of the name + name_len = self._pkt_buf[ptr] + if name_len == 0x00: + # we reached the end of this name + ptr += 1 # inc. pointer by 0x00 + break + # advance pointer + ptr += name_len + 1 + + # Validate Query is Type A + q_type = int.from_bytes(self._pkt_buf[ptr : ptr + 2], "l") + if not q_type == TYPE_A: + if self._debug: + print("* DNS ERROR: Incorrect Query Type: ", q_type) + return -1 + ptr += 2 + + # Validate Query is Type A + q_class = int.from_bytes(self._pkt_buf[ptr : ptr + 2], "l") + if not q_class == TYPE_A: + if self._debug: + print("* DNS ERROR: Incorrect Query Class: ", q_class) + return -1 + ptr += 2 + + # Let's take the first type-a answer + if self._pkt_buf[ptr] != 0xC0: + return -1 + ptr += 1 + + if self._pkt_buf[ptr] != 0xC: + return -1 + ptr += 1 + + # Validate Answer Type A + ans_type = int.from_bytes(self._pkt_buf[ptr : ptr + 2], "l") + if not ans_type == TYPE_A: + if self._debug: + print("* DNS ERROR: Incorrect Answer Type: ", ans_type) + return -1 + ptr += 2 + + # Validate Answer Class IN + ans_class = int.from_bytes(self._pkt_buf[ptr : ptr + 2], "l") + if not ans_class == TYPE_A: + if self._debug: + print("* DNS ERROR: Incorrect Answer Class: ", ans_class) + return -1 + ptr += 2 + + # skip over TTL + ptr += 4 + + # Validate addr is IPv4 + data_len = int.from_bytes(self._pkt_buf[ptr : ptr + 2], "l") + if not data_len == DATA_LEN: + if self._debug: + print("* DNS ERROR: Unexpected Data Length: ", data_len) + return -1 + ptr += 2 + # Return address + return self._pkt_buf[ptr : ptr + 4] + + def _build_dns_header(self): + """Builds DNS header.""" + # generate a random, 16-bit, request identifier + self._request_id = getrandbits(16) + + # ID, 16-bit identifier + self._pkt_buf.append(self._request_id >> 8) + self._pkt_buf.append(self._request_id & 0xFF) + + # Flags (0x0100) + self._pkt_buf.append(0x01) + self._pkt_buf.append(0x00) + + # QDCOUNT + self._pkt_buf.append(0x00) + self._pkt_buf.append(0x01) + # ANCOUNT + self._pkt_buf.append(0x00) + self._pkt_buf.append(0x00) + # NSCOUNT + self._pkt_buf.append(0x00) + self._pkt_buf.append(0x00) + # ARCOUNT + self._pkt_buf.append(0x00) + self._pkt_buf.append(0x00) + + def _build_dns_question(self): + """Build DNS question""" + host = self._host.decode("utf-8") + host = host.split(".") + # write out each section of host + for i, _ in enumerate(host): + # append the sz of the section + self._pkt_buf.append(len(host[i])) + # append the section data + self._pkt_buf += host[i] + # end of the name + self._pkt_buf.append(0x00) + # Type A record + self._pkt_buf.append(htons(TYPE_A) & 0xFF) + self._pkt_buf.append(htons(TYPE_A) >> 8) + # Class IN + self._pkt_buf.append(htons(CLASS_IN) & 0xFF) + self._pkt_buf.append(htons(CLASS_IN) >> 8) diff --git a/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_socket.py b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_socket.py new file mode 100644 index 0000000..b55c881 --- /dev/null +++ b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_socket.py @@ -0,0 +1,442 @@ +# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_wiznet5k_socket` +================================================================================ + +A socket compatible interface with the Wiznet5k module. + +* Author(s): ladyada, Brent Rubell, Patrick Van Oosterwijck, Adam Cummick + +""" +import gc +import time +from micropython import const +import adafruit_wiznet5k as wiznet5k + +_the_interface = None # pylint: disable=invalid-name + + +def set_interface(iface): + """Helper to set the global internet interface.""" + global _the_interface # pylint: disable=global-statement, invalid-name + _the_interface = iface + + +def htonl(x): + """Convert 32-bit positive integers from host to network byte order.""" + return ( + ((x) << 24 & 0xFF000000) + | ((x) << 8 & 0x00FF0000) + | ((x) >> 8 & 0x0000FF00) + | ((x) >> 24 & 0x000000FF) + ) + + +def htons(x): + """Convert 16-bit positive integers from host to network byte order.""" + return (((x) << 8) & 0xFF00) | (((x) >> 8) & 0xFF) + + +SOCK_STREAM = const(0x21) # TCP +TCP_MODE = 80 +SOCK_DGRAM = const(0x02) # UDP +AF_INET = const(3) +SOCKET_INVALID = const(255) + + +# pylint: disable=too-many-arguments, unused-argument +def getaddrinfo(host, port, family=0, socktype=0, proto=0, flags=0): + """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. + + """ + if not isinstance(port, int): + raise RuntimeError("Port must be an integer") + if is_ipv4(host): + return [(AF_INET, socktype, proto, "", (host, port))] + return [(AF_INET, socktype, proto, "", (gethostbyname(host), port))] + + +def gethostbyname(hostname): + """Translate a host name to IPv4 address format. The IPv4 address + is returned as a string. + :param str hostname: Desired hostname. + """ + addr = _the_interface.get_host_by_name(hostname) + addr = "{}.{}.{}.{}".format(addr[0], addr[1], addr[2], addr[3]) + return addr + + +def is_ipv4(host): + """Checks if a host string is an IPv4 address. + :param str host: host's name or ip + """ + 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 + + +# pylint: disable=invalid-name, too-many-public-methods +class socket: + """A simplified implementation of the Python 'socket' class + for connecting to a Wiznet5k module. + :param int family: Socket address (and protocol) family. + :param int type: Socket type. + + """ + + # pylint: disable=redefined-builtin,unused-argument + def __init__( + self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None, socknum=None + ): + 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._listen_port = None + + self._socknum = _the_interface.get_socket() + if self._socknum == SOCKET_INVALID: + raise RuntimeError("Failed to allocate socket.") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._sock_type == SOCK_STREAM: + self.disconnect() + stamp = time.monotonic() + 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: + if time.monotonic() - stamp > 1000: + raise RuntimeError("Failed to close socket") + + @property + def socknum(self): + """Returns the socket object's socket number.""" + return self._socknum + + @property + def status(self): + """Returns the status of the socket""" + return _the_interface.socket_status(self.socknum)[0] + + @property + def connected(self): + """Returns whether or not we are connected to the socket.""" + if self.socknum >= _the_interface.max_sockets: + return False + status = _the_interface.socket_status(self.socknum)[0] + if ( + 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_FIN_WAIT, + ) + if not result and status != wiznet5k.adafruit_wiznet5k.SNSR_SOCK_LISTEN: + self.close() + return result + + def getpeername(self): + """Return the remote address to which the socket is connected.""" + return _the_interface.remote_ip(self.socknum) + + def inet_aton(self, ip_string): + """Convert an IPv4 address from dotted-quad string format. + :param str ip_string: IP Address, as a dotted-quad string. + + """ + self._buffer = b"" + self._buffer = [int(item) for item in ip_string.split(".")] + self._buffer = bytearray(self._buffer) + return self._buffer + + def bind(self, address): + """Bind the socket to the listen port, if host is specified the interface + will be reconfigured to that IP. + :param tuple address: local socket 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) + 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._buffer = b"" + + def listen(self, backlog=None): + """Listen on the port specified by bind. + :param backlog: For compatibility but ignored. + """ + assert self._listen_port is not None, "Use bind to set the port before listen!" + _the_interface.socket_listen(self.socknum, self._listen_port) + self._buffer = b"" + + def accept(self): + """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 usable 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 ( + 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: + self.close() + self.listen() + + 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.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, conntype=None): + """Connect to a remote socket at address. + :param tuple address: Remote socket as a (host, port) tuple. + """ + assert ( + conntype != 0x03 + ), "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 + ) + _the_interface.src_port = 0 + if not result: + raise RuntimeError("Failed to connect to host", host) + self._buffer = b"" + + def send(self, data): + """Send data to the socket. The socket must be connected to + a remote socket. + :param bytearray data: Desired data to send to the socket. + """ + _the_interface.socket_write(self.socknum, data, self._timeout) + gc.collect() + + def sendto(self, data, address): + """Send data to the socket. The socket must be connected to + a remote socket. + :param bytearray data: Desired data to send to the socket. + :param tuple address: Remote socket as a (host, port) tuple. + """ + self.connect(address) + return self.send(data) + + def recv(self, bufsize=0, flags=0): # pylint: disable=too-many-branches + """Reads some bytes from the connected remote address. + :param int bufsize: Maximum number of bytes to receive. + :param int flags: ignored, present for compatibility. + """ + 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] + 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] + recv = bytes(recv) + received.append(recv) + to_read -= len(recv) + gc.collect() + if self._timeout > 0 and time.monotonic() - stamp > self._timeout: + break + self._buffer += b"".join(received) + + ret = None + if len(self._buffer) == bufsize: + ret = self._buffer + self._buffer = b"" + else: + ret = self._buffer[:bufsize] + self._buffer = self._buffer[bufsize:] + gc.collect() + return ret + + def embed_recv(self, bufsize=0, flags=0): # pylint: disable=too-many-branches + """Reads some bytes from the connected remote address and then return recv(). + :param int bufsize: Maximum number of bytes to receive. + :param int flags: ignored, present for compatibility. + """ + # print("Socket read", bufsize) + ret = None + 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] + gc.collect() + ret = self._buffer + # print("RET ptr:", id(ret), id(self._buffer)) + self._buffer = b"" + gc.collect() + return ret + + def recvfrom(self, bufsize=0, flags=0): + """Reads some bytes from the connected remote address. + :param int bufsize: Maximum number of bytes to receive. + :param int flags: ignored, present for compatibility. + :returns: a tuple (bytes, address) where address is a tuple (ip, port) + """ + return ( + self.recv(bufsize), + ( + _the_interface.remote_ip(self.socknum), + _the_interface.remote_port(self.socknum), + ), + ) + + def recv_into(self, buf, nbytes=0, flags=0): + """Reads some bytes from the connected remote address info the provided buffer. + :param bytearray buf: Data buffer + :param nbytes: Maximum number of bytes to receive + :param int flags: ignored, present for compatibility. + :returns: the number of bytes received + """ + if nbytes == 0: + nbytes = len(buf) + ret = self.recv(nbytes) + nbytes = len(ret) + buf[:nbytes] = ret + return nbytes + + def recvfrom_into(self, buf, nbytes=0, flags=0): + """Reads some bytes from the connected remote address info the provided buffer. + :param bytearray buf: Data buffer + :param nbytes: Maximum number of bytes to receive + :param int flags: ignored, present for compatibility. + :returns a tuple (nbytes, address) where address is a tuple (ip, port) + """ + return ( + self.recv_into(buf, nbytes), + ( + _the_interface.remote_ip(self.socknum), + _the_interface.remote_port(self.socknum), + ), + ) + + def readline(self): + """Attempt to return as many bytes as we can up to \ + but not including '\r\n'. + + """ + stamp = time.monotonic() + while b"\r\n" not in self._buffer: + 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] + if ( + not avail + and self._timeout > 0 + and time.monotonic() - stamp > self._timeout + ): + 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): + """Disconnects a TCP socket.""" + assert self._sock_type == SOCK_STREAM, "Socket must be a TCP socket." + _the_interface.socket_disconnect(self.socknum) + + def close(self): + """Closes the socket.""" + _the_interface.socket_close(self.socknum) + + def available(self): + """Returns how many bytes of data are available to be read from the socket.""" + return _the_interface.socket_available(self.socknum, self._sock_type) + + def settimeout(self, value): + """Sets socket read timeout. + :param int value: Socket read timeout, in seconds. + + """ + if value < 0: + raise Exception("Timeout period should be non-negative.") + self._timeout = value + + def gettimeout(self): + """Return the timeout in seconds (float) associated + with socket operations, or None if no timeout is set. + + """ + return self._timeout diff --git a/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py new file mode 100644 index 0000000..ac4beea --- /dev/null +++ b/adapter/lib/adafruit_wiznet5k/adafruit_wiznet5k_wsgiserver.py @@ -0,0 +1,203 @@ +# Based on ESP32 code Copyright (c) 2019 Matt Costi for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Patrick Van Oosterwijck +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_wiznet5k_wsgiserver` +================================================================================ + +A simple WSGI (Web Server Gateway Interface) server that interfaces with the W5500. +Opens a listening port on the W5500 to listen for incoming HTTP Requests and +Accepts an Application object that must be callable, which gets called +whenever a new HTTP Request has been received. + +The Application MUST accept 2 ordered parameters: + 1. environ object (incoming request data) + 2. start_response function. Must be called before the Application + callable returns, in order to set the response status and headers. + +The Application MUST return strings in a list, which is the response data + +Requires update_poll being called in the applications main event loop. + +For more details about Python WSGI see: +https://www.python.org/dev/peps/pep-0333/ + +* Author(s): Matt Costi, Patrick Van Oosterwijck +""" +# pylint: disable=no-name-in-module + +import io +import gc +from micropython import const +import adafruit_wiznet5k as wiznet5k +import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket + +_the_interface = None # pylint: disable=invalid-name + + +def set_interface(iface): + """Helper to set the global internet interface""" + global _the_interface # pylint: disable=global-statement, invalid-name + _the_interface = iface + socket.set_interface(iface) + + +# # Maximum number of sockets for the web server (number of connections we can hold) +# MAX_SOCK_NUM = const(6) + +# pylint: disable=invalid-name +class WSGIServer: + """ + A simple server that implements the WSGI interface + """ + + def __init__(self, port=80, debug=False, application=None): + self.application = application + self.port = port + self._timeout = 20 + self._client_sock = [] + self._debug = debug + + self._response_status = None + self._response_headers = [] + if _the_interface.chip == "w5100s" : + self.MAX_SOCK_NUM = const(2) + print("MAX_SOCK_NUM is 2") + else: + self.MAX_SOCK_NUM = const(6) + print("MAX_SOCK_NUM is 6") + + def start(self): + """ + Starts the server and begins listening for incoming connections. + Call update_poll in the main loop for the application callable to be + invoked on receiving an incoming request. + """ + for _ in range(self.MAX_SOCK_NUM): + new_sock = socket.socket() + new_sock.settimeout(self._timeout) + new_sock.bind((None, self.port)) + new_sock.listen() + self._client_sock.append(new_sock) + if self._debug: + ip = _the_interface.pretty_ip(_the_interface.ip_address) + print("Server available at {0}:{1}".format(ip, self.port)) + + def update_poll(self): + """ + Call this method inside your main event loop to get the server + check for new incoming client requests. When a request comes in, + the application callable will be invoked. + """ + for sock in self._client_sock: + if sock.available(): + environ = self._get_environ(sock) + result = self.application(environ, self._start_response) + self.finish_response(result, sock) + self._client_sock.remove(sock) + break + for sock in self._client_sock: + if sock.status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED: + self._client_sock.remove(sock) + for _ in range(len(self._client_sock), self.MAX_SOCK_NUM): + try: + new_sock = socket.socket() + new_sock.settimeout(self._timeout) + new_sock.bind((None, self.port)) + new_sock.listen() + self._client_sock.append(new_sock) + except RuntimeError: + pass + + def finish_response(self, result, client): + """ + Called after the application callable returns result data to respond with. + Creates the HTTP Response payload from the response_headers and results data, + and sends it back to client. + + :param string result: the data string to send back in the response to the client. + :param Socket client: the socket to send the response to. + """ + try: + response = "HTTP/1.1 {0}\r\n".format(self._response_status) + for header in self._response_headers: + response += "{0}: {1}\r\n".format(*header) + response += "\r\n" + client.send(response.encode("utf-8")) + for data in result: + if isinstance(data, bytes): + client.send(data) + else: + client.send(data.encode("utf-8")) + gc.collect() + finally: + client.disconnect() + client.close() + + def _start_response(self, status, response_headers): + """ + The application callable will be given this method as the second param + This is to be called before the application callable returns, to signify + the response can be started with the given status and headers. + + :param string status: a status string including the code and reason. ex: "200 OK" + :param list response_headers: a list of tuples to represent the headers. + ex ("header-name", "header value") + """ + self._response_status = status + self._response_headers = [("Server", "w5kWSGIServer")] + response_headers + + def _get_environ(self, client): + """ + The application callable will be given the resulting environ dictionary. + It contains metadata about the incoming request and the request body ("wsgi.input") + + :param Socket client: socket to read the request from + """ + env = {} + line = str(client.readline(), "utf-8") + (method, path, ver) = line.rstrip("\r\n").split(None, 2) + + env["wsgi.version"] = (1, 0) + env["wsgi.url_scheme"] = "http" + env["wsgi.multithread"] = False + env["wsgi.multiprocess"] = False + env["wsgi.run_once"] = False + + env["REQUEST_METHOD"] = method + env["SCRIPT_NAME"] = "" + env["SERVER_NAME"] = _the_interface.pretty_ip(_the_interface.ip_address) + env["SERVER_PROTOCOL"] = ver + env["SERVER_PORT"] = self.port + if path.find("?") >= 0: + env["PATH_INFO"] = path.split("?")[0] + env["QUERY_STRING"] = path.split("?")[1] + else: + env["PATH_INFO"] = path + + headers = {} + while True: + header = str(client.readline(), "utf-8") + if header == "": + break + title, content = header.split(": ", 1) + headers[title.lower()] = content + + if "content-type" in headers: + env["CONTENT_TYPE"] = headers.get("content-type") + if "content-length" in headers: + env["CONTENT_LENGTH"] = headers.get("content-length") + body = client.recv(int(env["CONTENT_LENGTH"])) + env["wsgi.input"] = io.StringIO(body) + else: + body = client.recv() + env["wsgi.input"] = io.StringIO(body) + for name, value in headers.items(): + key = "HTTP_" + name.replace("-", "_").upper() + if key in env: + value = "{0},{1}".format(env[key], value) + env[key] = value + + return env diff --git a/adapter/lib/neopixel.py b/adapter/lib/neopixel.py new file mode 100644 index 0000000..9b62818 --- /dev/null +++ b/adapter/lib/neopixel.py @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: 2016 Damien P. George +# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries +# SPDX-FileCopyrightText: 2019 Carter Nelson +# SPDX-FileCopyrightText: 2019 Roy Hooper +# +# SPDX-License-Identifier: MIT + +""" +`neopixel` - NeoPixel strip driver +==================================================== + +* Author(s): Damien P. George, Scott Shawcroft, Carter Nelson, Rose Hooper +""" + +# pylint: disable=ungrouped-imports +import sys +import board +import digitalio +from neopixel_write import neopixel_write + +try: + import adafruit_pixelbuf +except ImportError: + try: + import _pixelbuf as adafruit_pixelbuf + except ImportError: + import adafruit_pypixelbuf as adafruit_pixelbuf + + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel.git" + + +# Pixel color order constants +RGB = "RGB" +"""Red Green Blue""" +GRB = "GRB" +"""Green Red Blue""" +RGBW = "RGBW" +"""Red Green Blue White""" +GRBW = "GRBW" +"""Green Red Blue White""" + + +class NeoPixel(adafruit_pixelbuf.PixelBuf): + """ + A sequence of neopixels. + + :param ~microcontroller.Pin pin: The pin to output neopixel data on. + :param int n: The number of neopixels in the chain + :param int bpp: Bytes per pixel. 3 for RGB and 4 for RGBW pixels. + :param float brightness: Brightness of the pixels between 0.0 and 1.0 where 1.0 is full + brightness + :param bool auto_write: True if the neopixels should immediately change when set. If False, + `show` must be called explicitly. + :param str pixel_order: Set the pixel color channel order. GRBW is set by default. + + Example for Circuit Playground Express: + + .. code-block:: python + + import neopixel + from board import * + + RED = 0x100000 # (0x10, 0, 0) also works + + pixels = neopixel.NeoPixel(NEOPIXEL, 10) + for i in range(len(pixels)): + pixels[i] = RED + + Example for Circuit Playground Express setting every other pixel red using a slice: + + .. code-block:: python + + import neopixel + from board import * + import time + + RED = 0x100000 # (0x10, 0, 0) also works + + # Using ``with`` ensures pixels are cleared after we're done. + with neopixel.NeoPixel(NEOPIXEL, 10) as pixels: + pixels[::2] = [RED] * (len(pixels) // 2) + time.sleep(2) + + .. py:method:: NeoPixel.show() + + Shows the new colors on the pixels themselves if they haven't already + been autowritten. + + The colors may or may not be showing after this function returns because + it may be done asynchronously. + + .. py:method:: NeoPixel.fill(color) + + Colors all pixels the given ***color***. + + .. py:attribute:: brightness + + Overall brightness of the pixel (0 to 1.0) + + """ + + def __init__( + self, pin, n, *, bpp=3, brightness=1.0, auto_write=True, pixel_order=None + ): + if not pixel_order: + pixel_order = GRB if bpp == 3 else GRBW + elif isinstance(pixel_order, tuple): + order_list = [RGBW[order] for order in pixel_order] + pixel_order = "".join(order_list) + + self._power = None + if ( + sys.implementation.version[0] >= 7 + and getattr(board, "NEOPIXEL", None) == pin + ): + power = getattr(board, "NEOPIXEL_POWER_INVERTED", None) + polarity = power is None + if not power: + power = getattr(board, "NEOPIXEL_POWER", None) + if power: + try: + self._power = digitalio.DigitalInOut(power) + self._power.switch_to_output(value=polarity) + except ValueError: + pass + + super().__init__( + n, brightness=brightness, byteorder=pixel_order, auto_write=auto_write + ) + + self.pin = digitalio.DigitalInOut(pin) + self.pin.direction = digitalio.Direction.OUTPUT + + def deinit(self): + """Blank out the NeoPixels and release the pin.""" + self.fill(0) + self.show() + self.pin.deinit() + if self._power: + self._power.deinit() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.deinit() + + def __repr__(self): + return "[" + ", ".join([str(x) for x in self]) + "]" + + @property + def n(self): + """ + The number of neopixels in the chain (read-only) + """ + return len(self) + + def write(self): + """.. deprecated: 1.0.0 + + Use ``show`` instead. It matches Micro:Bit and Arduino APIs.""" + self.show() + + def _transmit(self, buffer): + neopixel_write(self.pin, buffer)