From 5b281189d5a52cef50f38deda40d66fca9cdeec6 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sat, 13 Jul 2019 09:45:47 -0400 Subject: [PATCH 01/25] separate server code from AP code --- adafruit_esp32spi/adafruit_esp32spi.py | 26 ++++++ adafruit_esp32spi/adafruit_esp32spi_socket.py | 5 ++ .../adafruit_esp32spi_wifimanager.py | 4 +- examples/esp32spi_server.py | 81 +++++++++++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 examples/esp32spi_server.py diff --git a/adafruit_esp32spi/adafruit_esp32spi.py b/adafruit_esp32spi/adafruit_esp32spi.py index a72bbb9..4065635 100644 --- a/adafruit_esp32spi/adafruit_esp32spi.py +++ b/adafruit_esp32spi/adafruit_esp32spi.py @@ -54,6 +54,7 @@ # pylint: disable=bad-whitespace _SET_NET_CMD = const(0x10) _SET_PASSPHRASE_CMD = const(0x11) +_SET_AP_PASSPHRASE_CMD = const(0x19) _SET_DEBUG_CMD = const(0x1A) _GET_CONN_STATUS_CMD = const(0x20) @@ -64,6 +65,7 @@ _GET_CURR_ENCT_CMD = const(0x26) _SCAN_NETWORKS = const(0x27) +_START_SERVER_TCP_CMD = const(0x28) _GET_SOCKET_CMD = const(0x3F) _GET_STATE_TCP_CMD = const(0x29) _DATA_SENT_TCP_CMD = const(0x2A) @@ -622,6 +624,30 @@ def socket_close(self, socket_num): if resp[0][0] != 1: raise RuntimeError("Failed to close socket") + def start_server(self, port, socket_num, conn_mode=TCP_MODE, ip=None): + if self._debug: + print("*** starting server") + self._socknum_ll[0][0] = socket_num + port_param = struct.pack('>H', port) + if ip: # use the 4 arg version + resp = self._send_command_get_response(_START_SERVER_TCP_CMD, + (ip, + port_param, + self._socknum_ll[0], + (conn_mode,))) + else: # use the 3 arg version + resp = self._send_command_get_response(_START_SERVER_TCP_CMD, + (port_param, + self._socknum_ll[0], + (conn_mode,))) + if resp[0][0] != 1: + raise RuntimeError("Could not start server") + + def get_server_state(self, socket_num): + self._socknum_ll[0][0] = socket_num + resp = self._send_command_get_response(_GET_STATE_TCP_CMD, self._socknum_ll) + return resp[0][0] + def set_esp_debug(self, enabled): """Enable/disable debug mode on the ESP32. Debug messages will be written to the ESP32's UART.""" diff --git a/adafruit_esp32spi/adafruit_esp32spi_socket.py b/adafruit_esp32spi/adafruit_esp32spi_socket.py index ccf5b4f..3acb63d 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socket.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socket.py @@ -148,6 +148,11 @@ def settimeout(self, value): """Set the read timeout for sockets, if value is 0 it will block""" self._timeout = value + def get_sock_num(self): + return self._socknum + + def set_sock_num(self, sock_num): + self._socknum = sock_num def close(self): """Close the socket, after reading whatever remains""" _the_interface.socket_close(self._socknum) diff --git a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py index 6b889d2..03cb54c 100755 --- a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py +++ b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py @@ -38,7 +38,7 @@ class ESPSPI_WiFiManager: """ A class to help manage the Wifi connection """ - def __init__(self, esp, secrets, status_pixel=None, attempts=2): + def __init__(self, esp, secrets, status_pixel=None, attempts=2, debug=False): """ :param ESP_SPIcontrol esp: The ESP object we are using :param dict secrets: The WiFi and Adafruit IO secrets dict (See examples) @@ -49,7 +49,7 @@ def __init__(self, esp, secrets, status_pixel=None, attempts=2): """ # Read the settings self.esp = esp - self.debug = False + self.debug = debug self.ssid = secrets['ssid'] self.password = secrets['password'] self.attempts = attempts diff --git a/examples/esp32spi_server.py b/examples/esp32spi_server.py new file mode 100644 index 0000000..fe07e2a --- /dev/null +++ b/examples/esp32spi_server.py @@ -0,0 +1,81 @@ +import board +import busio +from digitalio import DigitalInOut + +from adafruit_esp32spi import adafruit_esp32spi +import adafruit_esp32spi.adafruit_esp32spi_wifimanager as wifimanager +import adafruit_esp32spi.adafruit_esp32spi_socket as socket + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +print("ESP32 SPI simple web server test!") + +esp32_cs = DigitalInOut(board.D10) +esp32_ready = DigitalInOut(board.D9) +esp32_reset = DigitalInOut(board.D7) +esp32_gpio0 = DigitalInOut(board.D12) + + +spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0, debug=False) + +## Connect to wifi with secrets +wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, debug=True) +wifi.connect() + +socket.set_interface(esp) +sock = socket.socket() # Request a socket for the server +curr_sock = sock +sockNum = sock.get_sock_num() +print("server status: ", esp.get_server_state(sockNum)) + +# Start the server on port 80 with the socket number we just requested for it. +esp.start_server(80, sockNum) + +print("socket num: ", sockNum) +print("server status: ", esp.get_server_state(sockNum)) +print("IP addr: ", esp.pretty_ip(esp.ip_address)) +print("info: ", esp.network_data) +print("done!") + + +status = 0 +last_sock = 255 +def server_avail(): # TODO: make a server helper class + global last_sock + sock = 255; + + if (curr_sock != 255): + # if (last_sock != 255): + # TODO: if last sock, check that last_sock is still connected and available + # sock = last_sock + if (sock == 255): + sock = esp.socket_available(sockNum) + if (sock != 255): + last_sock = sock + return sock + + return 255 + +while True: + + avail = server_avail() + if (avail != 255): + sock.set_sock_num(avail) # TODO: Server class should return a new client socket + data = sock.read() + if len(data): + print(data) + sock.write(b"HTTP/1.1 200 OK\r\n") + sock.write(b"Content-type:text/html\r\n") + sock.write(b"\r\n") + + sock.write(b"Click here turn the LED on!!!
\r\n") + sock.write(b"Click here turn the LED off!!!!
\r\n") + + sock.write(b"\r\n") + sock.close() From 5e09b7b8676ce1ad487d5bc53f3d1a43c1f1d29c Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sat, 13 Jul 2019 12:52:20 -0400 Subject: [PATCH 02/25] Add server class to handle server start and get connected clients --- adafruit_esp32spi/adafruit_esp32spi_server.py | 94 +++++++++++++++++++ adafruit_esp32spi/adafruit_esp32spi_socket.py | 36 ++++++- examples/esp32spi_server.py | 57 +++-------- 3 files changed, 140 insertions(+), 47 deletions(-) create mode 100644 adafruit_esp32spi/adafruit_esp32spi_server.py diff --git a/adafruit_esp32spi/adafruit_esp32spi_server.py b/adafruit_esp32spi/adafruit_esp32spi_server.py new file mode 100644 index 0000000..8931369 --- /dev/null +++ b/adafruit_esp32spi/adafruit_esp32spi_server.py @@ -0,0 +1,94 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 ladyada for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`adafruit_esp32spi_server` +================================================================================ + +TODO: better description? +Server management lib to make handling and responding to incoming requests much easier + +* Author(s): Matt Costi +""" + +from micropython import const +import adafruit_esp32spi_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) + +NO_SOCK_AVAIL = const(255) + + +# pylint: disable=unused-argument, redefined-builtin, invalid-name +class server: + """ TODO: class docs """ + def __init__(self, port=80, debug=False): + self.port = port + self._server_sock = socket.socket(socknum=NO_SOCK_AVAIL) + self._client_sock = socket.socket(socknum=NO_SOCK_AVAIL) + self._debug = debug + + + def start(self): + """ start the server """ + self._server_sock = socket.socket() + _the_interface.start_server(self.port, self._server_sock.socknum) + if self._debug: + ip = _the_interface.pretty_ip(_the_interface.ip_address) + print("Server available at {0}:{1}".format(ip, self.port)) + print("Sever status: ", _the_interface.get_server_state(self._server_sock.socknum)) + + def client_available(self): + """ + returns a client socket connection if available.otherwise, returns a non available socket + :return the client + :rtype Socket + """ + sock = None + if self._server_sock.socknum != NO_SOCK_AVAIL: + if self._client_sock.socknum != NO_SOCK_AVAIL: + # check previous received client socket + if self._debug: + print("checking if last client sock still valid") + if self._client_sock.connected() and self._client_sock.available(): + sock = self._client_sock + if not sock: + # check for new client sock + if self._debug: + print("checking for new client sock") + client_sock_num = _the_interface.socket_available(self._server_sock.socknum) + sock = socket.socket(socknum=client_sock_num) + else: + print("Server has not been started, cannot check for clients!") + + if sock and sock.socknum != NO_SOCK_AVAIL: + if self._debug: + print("client sock num is: ", sock.socknum) + self._client_sock = sock + return self._client_sock + + return socket.socket(socknum=NO_SOCK_AVAIL) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socket.py b/adafruit_esp32spi/adafruit_esp32spi_socket.py index 3acb63d..bbeeb04 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socket.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socket.py @@ -32,6 +32,7 @@ import time import gc +import adafruit_esp32spi as esp from micropython import const _the_interface = None # pylint: disable=invalid-name @@ -42,6 +43,7 @@ def set_interface(iface): SOCK_STREAM = const(1) AF_INET = const(2) +NO_SOCKET_AVAIL = const(255) MAX_PACKET = const(4000) @@ -59,13 +61,13 @@ def getaddrinfo(host, port, family=0, socktype=0, proto=0, flags=0): class socket: """A simplified implementation of the Python 'socket' class, for connecting through an interface to a remote device""" - def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None): + 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") if type != SOCK_STREAM: raise RuntimeError("Only SOCK_STREAM type supported") self._buffer = b'' - self._socknum = _the_interface.get_socket() + self._socknum = socknum if socknum else _the_interface.get_socket() self.settimeout(0) def connect(self, address, conntype=None): @@ -148,11 +150,35 @@ def settimeout(self, value): """Set the read timeout for sockets, if value is 0 it will block""" self._timeout = value - def get_sock_num(self): + def available(self): + if self.socknum != NO_SOCKET_AVAIL: + return min(_the_interface.socket_available(self._socknum), MAX_PACKET) + return 0 + + def connected(self): + if self.socknum == NO_SOCKET_AVAIL: + return False + elif self.available(): + return True + else: + status = _the_interface.socket_status(self.socknum) + result = status not in (esp.SOCKET_LISTEN, + esp.SOCKET_CLOSED, + esp.SOCKET_FIN_WAIT_1, + esp.SOCKET_FIN_WAIT_2, + esp.SOCKET_TIME_WAIT, + esp.SOCKET_SYN_SENT, + esp.SOCKET_SYN_RCVD, + esp.SOCKET_CLOSE_WAIT) + if not result: + self.close() + self._socknum = NO_SOCKET_AVAIL + return result + + @property + def socknum(self): return self._socknum - def set_sock_num(self, sock_num): - self._socknum = sock_num def close(self): """Close the socket, after reading whatever remains""" _the_interface.socket_close(self._socknum) diff --git a/examples/esp32spi_server.py b/examples/esp32spi_server.py index fe07e2a..936850f 100644 --- a/examples/esp32spi_server.py +++ b/examples/esp32spi_server.py @@ -4,7 +4,7 @@ from adafruit_esp32spi import adafruit_esp32spi import adafruit_esp32spi.adafruit_esp32spi_wifimanager as wifimanager -import adafruit_esp32spi.adafruit_esp32spi_socket as socket +import adafruit_esp32spi.adafruit_esp32spi_server as server # Get wifi details and more from a secrets.py file try: @@ -28,54 +28,27 @@ wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, debug=True) wifi.connect() -socket.set_interface(esp) -sock = socket.socket() # Request a socket for the server -curr_sock = sock -sockNum = sock.get_sock_num() -print("server status: ", esp.get_server_state(sockNum)) +server.set_interface(esp) +server = server.server(80, True) +server.start() -# Start the server on port 80 with the socket number we just requested for it. -esp.start_server(80, sockNum) - -print("socket num: ", sockNum) -print("server status: ", esp.get_server_state(sockNum)) print("IP addr: ", esp.pretty_ip(esp.ip_address)) -print("info: ", esp.network_data) -print("done!") - - -status = 0 -last_sock = 255 -def server_avail(): # TODO: make a server helper class - global last_sock - sock = 255; - - if (curr_sock != 255): - # if (last_sock != 255): - # TODO: if last sock, check that last_sock is still connected and available - # sock = last_sock - if (sock == 255): - sock = esp.socket_available(sockNum) - if (sock != 255): - last_sock = sock - return sock +print("server started!") - return 255 while True: - avail = server_avail() - if (avail != 255): - sock.set_sock_num(avail) # TODO: Server class should return a new client socket - data = sock.read() + client = server.client_available() + if (client.available()): + data = client.read() if len(data): print(data) - sock.write(b"HTTP/1.1 200 OK\r\n") - sock.write(b"Content-type:text/html\r\n") - sock.write(b"\r\n") + client.write(b"HTTP/1.1 200 OK\r\n") + client.write(b"Content-type:text/html\r\n") + client.write(b"\r\n") - sock.write(b"Click here turn the LED on!!!
\r\n") - sock.write(b"Click here turn the LED off!!!!
\r\n") + client.write(b"Click here turn the LED on!!!
\r\n") + client.write(b"Click here turn the LED off!!!!
\r\n") - sock.write(b"\r\n") - sock.close() + client.write(b"\r\n") + client.close() From faf031acd7702ff98dbc46f42426705fe44f3025 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sat, 13 Jul 2019 20:54:48 -0400 Subject: [PATCH 03/25] add simple request method handling - can register callbacks on specific HTTP methods and paths - Can call update_poll in event loop to check for new incoming requests - update server example script --- .../adafruit_esp32spi_requests.py | 43 +++++++++------ adafruit_esp32spi/adafruit_esp32spi_server.py | 47 ++++++++++++++-- adafruit_esp32spi/adafruit_esp32spi_socket.py | 17 +++--- examples/esp32spi_server.py | 54 ++++++++++++------- 4 files changed, 113 insertions(+), 48 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_requests.py b/adafruit_esp32spi/adafruit_esp32spi_requests.py index 3857061..1d16da7 100755 --- a/adafruit_esp32spi/adafruit_esp32spi_requests.py +++ b/adafruit_esp32spi/adafruit_esp32spi_requests.py @@ -204,23 +204,11 @@ def request(method, url, data=None, json=None, headers=None, stream=False, timeo reason = "" if len(line) > 2: reason = line[2].rstrip() - while True: - line = sock.readline() - if not line or line == b"\r\n": - break - - #print("**line: ", line) - title, content = line.split(b': ', 1) - if title and content: - title = str(title.lower(), 'utf-8') - content = str(content, 'utf-8') - resp.headers[title] = content - - if line.startswith(b"Transfer-Encoding:"): - if b"chunked" in line: - raise ValueError("Unsupported " + line) - elif line.startswith(b"Location:") and not 200 <= status <= 299: - raise NotImplementedError("Redirects not yet supported") + resp.headers = self.parse_headers(sock) + if "chunked" in resp.headers.get("transfer-encoding"): + raise ValueError("Unsupported " + line) + elif resp.headers.get("location") and not 200 <= status <= 299: + raise NotImplementedError("Redirects not yet supported") except: sock.close() @@ -232,6 +220,27 @@ def request(method, url, data=None, json=None, headers=None, stream=False, timeo # pylint: enable=too-many-branches, too-many-statements, unused-argument # pylint: enable=too-many-arguments, too-many-locals +def parse_headers(sock): + """ + Parses the header portion of an HTTP request/response from the socket. + Expects first line of HTTP request/response to have been read already + return: header dictionary + rtype: Dict + """ + headers = {} + while True: + line = sock.readline() + if not line or line == b"\r\n": + break + + #print("**line: ", line) + title, content = line.split(b': ', 1) + if title and content: + title = str(title.lower(), 'utf-8') + content = str(content, 'utf-8') + headers[title] = content + return headers + def head(url, **kw): """Send HTTP HEAD request""" return request("HEAD", url, **kw) diff --git a/adafruit_esp32spi/adafruit_esp32spi_server.py b/adafruit_esp32spi/adafruit_esp32spi_server.py index 8931369..5b61701 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_server.py +++ b/adafruit_esp32spi/adafruit_esp32spi_server.py @@ -31,7 +31,8 @@ """ from micropython import const -import adafruit_esp32spi_socket as socket +import adafruit_esp32spi.adafruit_esp32spi_socket as socket +from adafruit_esp32spi.adafruit_esp32spi_requests import parse_headers _the_interface = None # pylint: disable=invalid-name def set_interface(iface): @@ -51,6 +52,7 @@ def __init__(self, port=80, debug=False): self._server_sock = socket.socket(socknum=NO_SOCK_AVAIL) self._client_sock = socket.socket(socknum=NO_SOCK_AVAIL) self._debug = debug + self._listeners = {} def start(self): @@ -62,11 +64,43 @@ def start(self): print("Server available at {0}:{1}".format(ip, self.port)) print("Sever status: ", _the_interface.get_server_state(self._server_sock.socknum)) + def on(self, method, path, request_handler): + """ + Register a Request Handler for a particular HTTP method and path. + request_handler will be called whenever a matching HTTP request is received. + + request_handler should accept the following args: + (Dict headers, bytes body, Socket client) + :param str method: the method of the HTTP request + :param str path: the path of the HTTP request + :param func request_handler: the function to call + """ + self._listeners[self._get_listener_key(method, path)] = request_handler + + def update_poll(self): + client = self.client_available() + if (client and client.available()): + line = client.readline() + method, path, ver = line.split(None, 2) + key = self._get_listener_key(method, path) + if key in self._listeners: + headers = parse_headers(client) + body = client.read() + print("headers: ", headers) + print("body: ", body) + self._listeners[key](headers, body, client) + else: + # TODO: support optional custom 404 callback? + client.write(b"HTTP/1.1 404 NotFound\r\n") + client.close() + + def client_available(self): """ - returns a client socket connection if available.otherwise, returns a non available socket - :return the client - :rtype Socket + returns a client socket connection if available. + Otherwise, returns None + :return: the client + :rtype: Socket """ sock = None if self._server_sock.socknum != NO_SOCK_AVAIL: @@ -91,4 +125,7 @@ def client_available(self): self._client_sock = sock return self._client_sock - return socket.socket(socknum=NO_SOCK_AVAIL) + return None + + def _get_listener_key(self, method, path): + return "{0}|{1}".format(str(method.lower(), 'utf-8'), str(path, 'utf-8')) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socket.py b/adafruit_esp32spi/adafruit_esp32spi_socket.py index bbeeb04..9088ad0 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socket.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socket.py @@ -162,14 +162,15 @@ def connected(self): return True else: status = _the_interface.socket_status(self.socknum) - result = status not in (esp.SOCKET_LISTEN, - esp.SOCKET_CLOSED, - esp.SOCKET_FIN_WAIT_1, - esp.SOCKET_FIN_WAIT_2, - esp.SOCKET_TIME_WAIT, - esp.SOCKET_SYN_SENT, - esp.SOCKET_SYN_RCVD, - esp.SOCKET_CLOSE_WAIT) + # TODO: why is esp. not defined? using magic numbers in mean time + result = status not in (1, + 0, + 5, + 6, + 10, + 2, + 3, + 7) if not result: self.close() self._socknum = NO_SOCKET_AVAIL diff --git a/examples/esp32spi_server.py b/examples/esp32spi_server.py index 936850f..950cf93 100644 --- a/examples/esp32spi_server.py +++ b/examples/esp32spi_server.py @@ -20,35 +20,53 @@ esp32_reset = DigitalInOut(board.D7) esp32_gpio0 = DigitalInOut(board.D12) +"""Use below for Most Boards""" +# status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +"""Uncomment below for ItsyBitsy M4""" +import adafruit_dotstar as dotstar +status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) + spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0, debug=False) ## Connect to wifi with secrets -wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, debug=True) +wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light, debug=True) wifi.connect() server.set_interface(esp) -server = server.server(80, True) -server.start() +server = server.server(80, debug=False) -print("IP addr: ", esp.pretty_ip(esp.ip_address)) -print("server started!") +def onLedHigh(headers, body, client): + print("led on!") + status_light.fill((0, 0, 100)) + respond(headers, body, client) -while True: +def onLedLow(headers, body, client): + print("led off!") + status_light.fill(0) + respond(headers, body, client) + +def respond(headers, body, client): + client.write(b"HTTP/1.1 200 OK\r\n") + client.write(b"Content-type:text/html\r\n") + client.write(b"\r\n") + + client.write(b"Click here turn the LED on!!!
\r\n") + client.write(b"Click here turn the LED off!!!!
\r\n") - client = server.client_available() - if (client.available()): - data = client.read() - if len(data): - print(data) - client.write(b"HTTP/1.1 200 OK\r\n") - client.write(b"Content-type:text/html\r\n") - client.write(b"\r\n") + client.write(b"\r\n") + client.close() - client.write(b"Click here turn the LED on!!!
\r\n") - client.write(b"Click here turn the LED off!!!!
\r\n") +server.on("GET", "/", respond) +server.on("GET", "/H", onLedHigh) +server.on("GET", "/L", onLedLow) - client.write(b"\r\n") - client.close() + +print("IP addr: ", esp.pretty_ip(esp.ip_address)) + +server.start() +print("server started!") +while True: + server.update_poll() From 8c4035d80df2040cb1f76f25fb66302d51311acc Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 14 Jul 2019 11:44:13 -0400 Subject: [PATCH 04/25] py linting --- adafruit_esp32spi/adafruit_esp32spi.py | 12 +++++++----- adafruit_esp32spi/adafruit_esp32spi_requests.py | 2 +- adafruit_esp32spi/adafruit_esp32spi_server.py | 17 ++++++++++++++--- adafruit_esp32spi/adafruit_esp32spi_socket.py | 10 ++++++---- .../adafruit_esp32spi_wifimanager.py | 2 ++ 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi.py b/adafruit_esp32spi/adafruit_esp32spi.py index 4065635..3ad6b67 100644 --- a/adafruit_esp32spi/adafruit_esp32spi.py +++ b/adafruit_esp32spi/adafruit_esp32spi.py @@ -624,26 +624,28 @@ def socket_close(self, socket_num): if resp[0][0] != 1: raise RuntimeError("Failed to close socket") - def start_server(self, port, socket_num, conn_mode=TCP_MODE, ip=None): + def start_server(self, port, socket_num, conn_mode=TCP_MODE, ip=None): # pylint: disable=invalid-name + """Opens a server on the specified port, using the ESP32's internal reference number""" if self._debug: print("*** starting server") self._socknum_ll[0][0] = socket_num port_param = struct.pack('>H', port) if ip: # use the 4 arg version resp = self._send_command_get_response(_START_SERVER_TCP_CMD, - (ip, + (ip, port_param, self._socknum_ll[0], (conn_mode,))) else: # use the 3 arg version resp = self._send_command_get_response(_START_SERVER_TCP_CMD, - (port_param, - self._socknum_ll[0], - (conn_mode,))) + (port_param, + self._socknum_ll[0], + (conn_mode,))) if resp[0][0] != 1: raise RuntimeError("Could not start server") def get_server_state(self, socket_num): + """Get the state of the ESP32's internal reference server socket number""" self._socknum_ll[0][0] = socket_num resp = self._send_command_get_response(_GET_STATE_TCP_CMD, self._socknum_ll) return resp[0][0] diff --git a/adafruit_esp32spi/adafruit_esp32spi_requests.py b/adafruit_esp32spi/adafruit_esp32spi_requests.py index 1d16da7..994af94 100755 --- a/adafruit_esp32spi/adafruit_esp32spi_requests.py +++ b/adafruit_esp32spi/adafruit_esp32spi_requests.py @@ -204,7 +204,7 @@ def request(method, url, data=None, json=None, headers=None, stream=False, timeo reason = "" if len(line) > 2: reason = line[2].rstrip() - resp.headers = self.parse_headers(sock) + resp.headers = parse_headers(sock) if "chunked" in resp.headers.get("transfer-encoding"): raise ValueError("Unsupported " + line) elif resp.headers.get("location") and not 200 <= status <= 299: diff --git a/adafruit_esp32spi/adafruit_esp32spi_server.py b/adafruit_esp32spi/adafruit_esp32spi_server.py index 5b61701..5c3f586 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_server.py +++ b/adafruit_esp32spi/adafruit_esp32spi_server.py @@ -29,6 +29,7 @@ * Author(s): Matt Costi """ +# pylint: disable=no-name-in-module from micropython import const import adafruit_esp32spi.adafruit_esp32spi_socket as socket @@ -78,10 +79,20 @@ def on(self, method, path, request_handler): self._listeners[self._get_listener_key(method, path)] = request_handler 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 for + which a request handler has been registered with 'on' method, that + request handler will be invoked. + + Unrecognized requests will be automatically be responded to with a 404. + """ client = self.client_available() if (client and client.available()): line = client.readline() - method, path, ver = line.split(None, 2) + line = line.split(None, 2) + method = line[0] + path = line[1] key = self._get_listener_key(method, path) if key in self._listeners: headers = parse_headers(client) @@ -90,7 +101,7 @@ def update_poll(self): print("body: ", body) self._listeners[key](headers, body, client) else: - # TODO: support optional custom 404 callback? + # TODO: support optional custom 404 handler? client.write(b"HTTP/1.1 404 NotFound\r\n") client.close() @@ -127,5 +138,5 @@ def client_available(self): return None - def _get_listener_key(self, method, path): + def _get_listener_key(self, method, path): # pylint: disable=no-self-use return "{0}|{1}".format(str(method.lower(), 'utf-8'), str(path, 'utf-8')) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socket.py b/adafruit_esp32spi/adafruit_esp32spi_socket.py index 9088ad0..0ef838a 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socket.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socket.py @@ -32,7 +32,6 @@ import time import gc -import adafruit_esp32spi as esp from micropython import const _the_interface = None # pylint: disable=invalid-name @@ -92,7 +91,7 @@ def readline(self): stamp = time.monotonic() while b'\r\n' not in self._buffer: # there's no line already in there, read some more - avail = min(_the_interface.socket_available(self._socknum), MAX_PACKET) + avail = self.available() if avail: self._buffer += _the_interface.socket_read(self._socknum, avail) elif self._timeout > 0 and time.monotonic() - stamp > self._timeout: @@ -108,7 +107,7 @@ def read(self, size=0): #print("Socket read", size) if size == 0: # read as much as we can at the moment while True: - avail = min(_the_interface.socket_available(self._socknum), MAX_PACKET) + avail = self.available() if avail: self._buffer += _the_interface.socket_read(self._socknum, avail) else: @@ -124,7 +123,7 @@ def read(self, size=0): received = [] while to_read > 0: #print("Bytes to read:", to_read) - avail = min(_the_interface.socket_available(self._socknum), MAX_PACKET) + avail = self.available() if avail: stamp = time.monotonic() recv = _the_interface.socket_read(self._socknum, min(to_read, avail)) @@ -151,11 +150,13 @@ def settimeout(self, value): self._timeout = value def available(self): + """Returns how many bytes of data are available to be read (up to the MAX_PACKET length)""" if self.socknum != NO_SOCKET_AVAIL: return min(_the_interface.socket_available(self._socknum), MAX_PACKET) return 0 def connected(self): + """Whether or not we are connected to the socket""" if self.socknum == NO_SOCKET_AVAIL: return False elif self.available(): @@ -178,6 +179,7 @@ def connected(self): @property def socknum(self): + """The socket number""" return self._socknum def close(self): diff --git a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py index 03cb54c..8b445a2 100755 --- a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py +++ b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py @@ -38,6 +38,7 @@ class ESPSPI_WiFiManager: """ A class to help manage the Wifi connection """ + # pylint: disable=too-many-arguments def __init__(self, esp, secrets, status_pixel=None, attempts=2, debug=False): """ :param ESP_SPIcontrol esp: The ESP object we are using @@ -56,6 +57,7 @@ def __init__(self, esp, secrets, status_pixel=None, attempts=2, debug=False): requests.set_interface(self.esp) self.statuspix = status_pixel self.pixel_status(0) + # pylint: enable=too-many-arguments def reset(self): """ From b8fd02eaecb7c670cf276092caa1217a5d9ef4a3 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 14 Jul 2019 12:03:21 -0400 Subject: [PATCH 05/25] more linting --- adafruit_esp32spi/adafruit_esp32spi_server.py | 2 -- adafruit_esp32spi/adafruit_esp32spi_socket.py | 2 ++ examples/esp32spi_server.py | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_server.py b/adafruit_esp32spi/adafruit_esp32spi_server.py index 5c3f586..647d025 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_server.py +++ b/adafruit_esp32spi/adafruit_esp32spi_server.py @@ -97,8 +97,6 @@ def update_poll(self): if key in self._listeners: headers = parse_headers(client) body = client.read() - print("headers: ", headers) - print("body: ", body) self._listeners[key](headers, body, client) else: # TODO: support optional custom 404 handler? diff --git a/adafruit_esp32spi/adafruit_esp32spi_socket.py b/adafruit_esp32spi/adafruit_esp32spi_socket.py index 0ef838a..6737d14 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socket.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socket.py @@ -60,6 +60,7 @@ def getaddrinfo(host, port, family=0, socktype=0, proto=0, flags=0): class socket: """A simplified implementation of the Python 'socket' class, for connecting through an interface to a remote device""" + # pylint: disable=too-many-arguments 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") @@ -68,6 +69,7 @@ def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None, sockn self._buffer = b'' self._socknum = socknum if socknum else _the_interface.get_socket() self.settimeout(0) + # pylint: enable=too-many-arguments def connect(self, address, conntype=None): """Connect the socket to the 'address' (which can be 32bit packed IP or diff --git a/examples/esp32spi_server.py b/examples/esp32spi_server.py index 950cf93..faed539 100644 --- a/examples/esp32spi_server.py +++ b/examples/esp32spi_server.py @@ -6,6 +6,8 @@ import adafruit_esp32spi.adafruit_esp32spi_wifimanager as wifimanager import adafruit_esp32spi.adafruit_esp32spi_server as server +import neopixel + # Get wifi details and more from a secrets.py file try: from secrets import secrets @@ -21,14 +23,15 @@ esp32_gpio0 = DigitalInOut(board.D12) """Use below for Most Boards""" -# status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards """Uncomment below for ItsyBitsy M4""" -import adafruit_dotstar as dotstar -status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) +# import adafruit_dotstar as dotstar +# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) spi = busio.SPI(board.SCK, board.MOSI, board.MISO) -esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0, debug=False) +esp = adafruit_esp32spi.ESP_SPIcontrol( + spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0, debug=False) ## Connect to wifi with secrets wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light, debug=True) @@ -40,15 +43,22 @@ def onLedHigh(headers, body, client): print("led on!") + print("headers: ", headers) + print("body: ", body) status_light.fill((0, 0, 100)) respond(headers, body, client) def onLedLow(headers, body, client): print("led off!") + print("headers: ", headers) + print("body: ", body) status_light.fill(0) respond(headers, body, client) def respond(headers, body, client): + print("headers: ", headers) + print("body: ", body) + client.write(b"HTTP/1.1 200 OK\r\n") client.write(b"Content-type:text/html\r\n") client.write(b"\r\n") From 5bd6c86f184333ec95e18a8cbacce41e14252101 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 14 Jul 2019 17:53:26 -0400 Subject: [PATCH 06/25] add in support for automatically serving files from a specified directory --- adafruit_esp32spi/adafruit_esp32spi_server.py | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_server.py b/adafruit_esp32spi/adafruit_esp32spi_server.py index 647d025..8ab2696 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_server.py +++ b/adafruit_esp32spi/adafruit_esp32spi_server.py @@ -31,6 +31,7 @@ """ # pylint: disable=no-name-in-module +import os from micropython import const import adafruit_esp32spi.adafruit_esp32spi_socket as socket from adafruit_esp32spi.adafruit_esp32spi_requests import parse_headers @@ -43,6 +44,7 @@ def set_interface(iface): socket.set_interface(iface) NO_SOCK_AVAIL = const(255) +INDEX_HTML = "/index.html" # pylint: disable=unused-argument, redefined-builtin, invalid-name @@ -54,6 +56,8 @@ def __init__(self, port=80, debug=False): self._client_sock = socket.socket(socknum=NO_SOCK_AVAIL) self._debug = debug self._listeners = {} + self._static_dir = None + self._static_files = [] def start(self): @@ -72,12 +76,43 @@ def on(self, method, path, request_handler): request_handler should accept the following args: (Dict headers, bytes body, Socket client) + :param str method: the method of the HTTP request :param str path: the path of the HTTP request :param func request_handler: the function to call """ self._listeners[self._get_listener_key(method, path)] = request_handler + def set_static_dir(self, directory_path): + """ + allows for setting a directory of static files that will be auto-served + when that file is GET requested at'/' + index.html will also be made available at root path '/' + + Note: does not support serving files in child folders at this time + """ + self._static_dir = directory_path + self._static_files = ["/" + file for file in os.listdir(self._static_dir)] + print(self._static_files) + + def serve_file(self, file_path, dir=None): + """ + writes a file from the file system as a response to the client. + + :param string file_path: path to the image to write to client. + if dir is not present, it is treated as an absolute path + :param string dir: path to directory that file is located in (optional) + """ + self._client_sock.write(b"HTTP/1.1 200 OK\r\n") + self._client_sock.write(b"Content-Type:" + self._get_content_type(file_path) + b"\r\n") + self._client_sock.write(b"\r\n") + full_path = file_path if not dir else dir + file_path + with open(full_path, 'rb') as fp: + for line in fp: + self._client_sock.write(line) + self._client_sock.write(b"\r\n") + self._client_sock.close() + def update_poll(self): """ Call this method inside your main event loop to get the server @@ -91,16 +126,22 @@ def update_poll(self): if (client and client.available()): line = client.readline() line = line.split(None, 2) - method = line[0] - path = line[1] + method = str(line[0], "utf-8") + path = str(line[1], "utf-8") key = self._get_listener_key(method, path) if key in self._listeners: headers = parse_headers(client) body = client.read() self._listeners[key](headers, body, client) + elif method.lower() == "get": + client.read() + if path in self._static_files: + self.serve_file(path, dir=self._static_dir) + elif path == "/" and INDEX_HTML in self._static_files: + self.serve_file(INDEX_HTML, dir=self._static_dir) else: # TODO: support optional custom 404 handler? - client.write(b"HTTP/1.1 404 NotFound\r\n") + self._client_sock.write(b"HTTP/1.1 404 NotFound\r\n") client.close() @@ -137,4 +178,16 @@ def client_available(self): return None def _get_listener_key(self, method, path): # pylint: disable=no-self-use - return "{0}|{1}".format(str(method.lower(), 'utf-8'), str(path, 'utf-8')) + return "{0}|{1}".format(method.lower(), path) + + + def _get_content_type(self, file): # pylint: disable=no-self-use + ext = file.split('.')[-1] + if ext in ("html", "htm"): + return b"text/html" + if ext == "js": + return b"application/javascript" + if ext == "css": + return b"text/css" + # TODO: test adding in support for image types as well + return b"text/plain" From 83d15258a4d917ba2a4714294faab9c9c95d3f56 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 14 Jul 2019 18:03:35 -0400 Subject: [PATCH 07/25] add in example static asset serving with ajax call LED color changing --- examples/esp32spi_server.py | 47 +++++++++++---------- examples/static/index.html | 14 ++++++ examples/static/led_color_picker_example.js | 23 ++++++++++ 3 files changed, 61 insertions(+), 23 deletions(-) create mode 100755 examples/static/index.html create mode 100755 examples/static/led_color_picker_example.js diff --git a/examples/esp32spi_server.py b/examples/esp32spi_server.py index faed539..390c768 100644 --- a/examples/esp32spi_server.py +++ b/examples/esp32spi_server.py @@ -8,6 +8,12 @@ import neopixel + +# This example depends on the 'static' folder in the examples folder +# being copied to the root of the circuitpython filesystem. +# This is where our static assets like html, js, and css live. + + # Get wifi details and more from a secrets.py file try: from secrets import secrets @@ -15,6 +21,11 @@ print("WiFi secrets are kept in secrets.py, please add them there!") raise +try: + import json as json_module +except ImportError: + import ujson as json_module + print("ESP32 SPI simple web server test!") esp32_cs = DigitalInOut(board.D10) @@ -41,42 +52,32 @@ server = server.server(80, debug=False) -def onLedHigh(headers, body, client): +def onLedHigh(headers, body, client): # pylint: disable=unused-argument print("led on!") - print("headers: ", headers) - print("body: ", body) status_light.fill((0, 0, 100)) - respond(headers, body, client) + server.serve_file("static/index.html") -def onLedLow(headers, body, client): +def onLedLow(headers, body, client): # pylint: disable=unused-argument print("led off!") - print("headers: ", headers) - print("body: ", body) status_light.fill(0) - respond(headers, body, client) - -def respond(headers, body, client): - print("headers: ", headers) - print("body: ", body) - - client.write(b"HTTP/1.1 200 OK\r\n") - client.write(b"Content-type:text/html\r\n") - client.write(b"\r\n") + server.serve_file("static/index.html") - client.write(b"Click here turn the LED on!!!
\r\n") - client.write(b"Click here turn the LED off!!!!
\r\n") - - client.write(b"\r\n") +def onLedColor(headers, body, client): # pylint: disable=unused-argument + rgb = json_module.loads(body) + print("led color: " + rgb) + status_light.fill((rgb.get("r"), rgb.get("g"), rgb.get("b"))) + client.write(b"HTTP/1.1 200 OK") client.close() -server.on("GET", "/", respond) +server.set_static_dir("/static") server.on("GET", "/H", onLedHigh) server.on("GET", "/L", onLedLow) +server.on("POST", "/ajax/ledcolor", onLedColor) -print("IP addr: ", esp.pretty_ip(esp.ip_address)) +print("open this IP in your browser: ", esp.pretty_ip(esp.ip_address)) server.start() -print("server started!") + while True: server.update_poll() diff --git a/examples/static/index.html b/examples/static/index.html new file mode 100755 index 0000000..460a37b --- /dev/null +++ b/examples/static/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + +

LED color picker demo!

+ + + \ No newline at end of file diff --git a/examples/static/led_color_picker_example.js b/examples/static/led_color_picker_example.js new file mode 100755 index 0000000..d988944 --- /dev/null +++ b/examples/static/led_color_picker_example.js @@ -0,0 +1,23 @@ +console.log("initializing color picker") +var colorPicker = $('input#colorPicker'); +colorPicker.minicolors({ + format: "rgb", + changeDelay: 200, + change: function (value, opacity) { + rgbObject = colorPicker.minicolors("rgbObject"); + console.log(rgbObject); + $.ajax({ + type: "POST", + url: "/ajax/ledcolor", + data: JSON.stringify(rgbObject), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: function(data){ + console.log("success!"); + }, + failure: function(errMsg) { + console.log("error! " + errMsg); + } + }); + } +}); \ No newline at end of file From f6d941510322710fce8f9d6f2f956fdd95f35c6b Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 14 Jul 2019 18:07:00 -0400 Subject: [PATCH 08/25] fix spacing --- examples/esp32spi_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/esp32spi_server.py b/examples/esp32spi_server.py index 390c768..37b7058 100644 --- a/examples/esp32spi_server.py +++ b/examples/esp32spi_server.py @@ -10,7 +10,7 @@ # This example depends on the 'static' folder in the examples folder -# being copied to the root of the circuitpython filesystem. +# being copied to the root of the circuitpython filesystem. # This is where our static assets like html, js, and css live. From d469ab99a561b6c842dd9305dd6b354dc563fb9c Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sat, 20 Jul 2019 00:50:08 -0400 Subject: [PATCH 09/25] untested first draft of simple WSGI server --- .../adafruite_esp32spi_wsgiserver.py | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py diff --git a/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py b/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py new file mode 100644 index 0000000..45b7cef --- /dev/null +++ b/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py @@ -0,0 +1,210 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Matt Costi for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`adafruit_esp32spi_wsgiserver` +================================================================================ + +A simple WSGI (Web Server Gateway Interface) server that interfaces with the ESP32 over SPI. +Opens a specified port on the ESP32 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 a single string 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 +""" +# pylint: disable=no-name-in-module + +import io +from micropython import const +import adafruit_esp32spi.adafruit_esp32spi_socket as socket +from adafruit_esp32spi.adafruit_esp32spi_requests import parse_headers + +_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) + +NO_SOCK_AVAIL = const(255) + +# 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._server_sock = socket.socket(socknum=NO_SOCK_AVAIL) + self._client_sock = socket.socket(socknum=NO_SOCK_AVAIL) + self._debug = debug + + self._response_status = None + self._response_headers = [] + + 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. + """ + self._server_sock = socket.socket() + _the_interface.start_server(self.port, self._server_sock.socknum) + if self._debug: + ip = _the_interface.pretty_ip(_the_interface.ip_address) + print("Server available at {0}:{1}".format(ip, self.port)) + print("Sever status: ", _the_interface.get_server_state(self._server_sock.socknum)) + + 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. + """ + self.client_available() + if (self._client_sock and self._client_sock.available()): + environ = self._get_environ(self._client_sock) + result = self.application(environ, self._start_response) + self.finish_response(result) + + def finish_response(self, result): + """ + Called after the application callbile 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. + """ + 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" + for data in result: + response += data + self._client_sock.write(response.encode("utf-8")) + finally: + self._client_sock.close() + + def client_available(self): + """ + returns a client socket connection if available. + Otherwise, returns None + :return: the client + :rtype: Socket + """ + sock = None + if self._server_sock.socknum != NO_SOCK_AVAIL: + if self._client_sock.socknum != NO_SOCK_AVAIL: + # check previous received client socket + if self._debug > 2: + print("checking if last client sock still valid") + if self._client_sock.connected() and self._client_sock.available(): + sock = self._client_sock + if not sock: + # check for new client sock + if self._debug > 2: + print("checking for new client sock") + client_sock_num = _the_interface.socket_available(self._server_sock.socknum) + sock = socket.socket(socknum=client_sock_num) + else: + print("Server has not been started, cannot check for clients!") + + if sock and sock.socknum != NO_SOCK_AVAIL: + if self._debug > 2: + print("client sock num is: ", sock.socknum) + self._client_sock = sock + return self._client_sock + + return None + + 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", "esp32WSGIServer")] + 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["PATH_INFO"] = path + env["SERVER_NAME"] = _the_interface.pretty_ip(_the_interface.ip_address) + env["SERVER_PROTOCOL"] = ver + env["SERVER_PORT"] = self.port + if line.find("?"): + env["QUERY_STRING"] = line.split("?")[1] + + headers = parse_headers(client) + 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.read(env["CONTENT_LENGTH"]) + env["wsgi.input"] = io.StringIO(body) + else: + body = client.read() + env["wsgi.input"] = io.StringIO(body) + for name, value in headers: + key = "HTTP_" + name.replace('-', '_').upper() + if key in env: + value = "{0},{1}".format(env[key], value) + env[key] = value + + return env From 94fce446aa735ea7ac1497d36c23e2b67e384b6a Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sat, 20 Jul 2019 22:08:27 -0400 Subject: [PATCH 10/25] fix a couple wsgi server bugs --- adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py b/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py index 45b7cef..bf229b7 100644 --- a/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py +++ b/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py @@ -188,20 +188,20 @@ def _get_environ(self, client): env["SERVER_NAME"] = _the_interface.pretty_ip(_the_interface.ip_address) env["SERVER_PROTOCOL"] = ver env["SERVER_PORT"] = self.port - if line.find("?"): - env["QUERY_STRING"] = line.split("?")[1] + if path.find("?") >= 0: + env["QUERY_STRING"] = path.split("?")[1] headers = parse_headers(client) 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.read(env["CONTENT_LENGTH"]) + body = client.read(int(env["CONTENT_LENGTH"])) env["wsgi.input"] = io.StringIO(body) else: body = client.read() env["wsgi.input"] = io.StringIO(body) - for name, value in headers: + for name, value in headers.items(): key = "HTTP_" + name.replace('-', '_').upper() if key in env: value = "{0},{1}".format(env[key], value) From c429455f57f39993c0bb7ea2c016ffcc5eddb2ff Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 13:24:13 -0400 Subject: [PATCH 11/25] send response headers separately before body to support chunking --- adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py b/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py index bf229b7..6784aaf 100644 --- a/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py +++ b/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py @@ -47,6 +47,7 @@ # pylint: disable=no-name-in-module import io +import gc from micropython import const import adafruit_esp32spi.adafruit_esp32spi_socket as socket from adafruit_esp32spi.adafruit_esp32spi_requests import parse_headers @@ -114,10 +115,15 @@ def finish_response(self, result): for header in self._response_headers: response += "{0}: {1}\r\n".format(*header) response += "\r\n" - for data in result: - response += data self._client_sock.write(response.encode("utf-8")) + for data in result: + if isinstance(data, bytes): + self._client_sock.write(data) + else: + self._client_sock.write(data.encode("utf-8")) + gc.collect() finally: + print("closing") self._client_sock.close() def client_available(self): From 92d0c6669fdd8a6c9f56eeb86c1268679697095d Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 13:34:06 -0400 Subject: [PATCH 12/25] new example script for using WsgiServer Includes an simple example of a wsgi complient application class --- examples/esp32spi_server.py | 83 -------------- examples/esp32spi_wsgiserver.py | 188 ++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 83 deletions(-) delete mode 100644 examples/esp32spi_server.py create mode 100644 examples/esp32spi_wsgiserver.py diff --git a/examples/esp32spi_server.py b/examples/esp32spi_server.py deleted file mode 100644 index 37b7058..0000000 --- a/examples/esp32spi_server.py +++ /dev/null @@ -1,83 +0,0 @@ -import board -import busio -from digitalio import DigitalInOut - -from adafruit_esp32spi import adafruit_esp32spi -import adafruit_esp32spi.adafruit_esp32spi_wifimanager as wifimanager -import adafruit_esp32spi.adafruit_esp32spi_server as server - -import neopixel - - -# This example depends on the 'static' folder in the examples folder -# being copied to the root of the circuitpython filesystem. -# This is where our static assets like html, js, and css live. - - -# Get wifi details and more from a secrets.py file -try: - from secrets import secrets -except ImportError: - print("WiFi secrets are kept in secrets.py, please add them there!") - raise - -try: - import json as json_module -except ImportError: - import ujson as json_module - -print("ESP32 SPI simple web server test!") - -esp32_cs = DigitalInOut(board.D10) -esp32_ready = DigitalInOut(board.D9) -esp32_reset = DigitalInOut(board.D7) -esp32_gpio0 = DigitalInOut(board.D12) - -"""Use below for Most Boards""" -status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards -"""Uncomment below for ItsyBitsy M4""" -# import adafruit_dotstar as dotstar -# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) - - -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) -esp = adafruit_esp32spi.ESP_SPIcontrol( - spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0, debug=False) - -## Connect to wifi with secrets -wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light, debug=True) -wifi.connect() - -server.set_interface(esp) -server = server.server(80, debug=False) - - -def onLedHigh(headers, body, client): # pylint: disable=unused-argument - print("led on!") - status_light.fill((0, 0, 100)) - server.serve_file("static/index.html") - -def onLedLow(headers, body, client): # pylint: disable=unused-argument - print("led off!") - status_light.fill(0) - server.serve_file("static/index.html") - -def onLedColor(headers, body, client): # pylint: disable=unused-argument - rgb = json_module.loads(body) - print("led color: " + rgb) - status_light.fill((rgb.get("r"), rgb.get("g"), rgb.get("b"))) - client.write(b"HTTP/1.1 200 OK") - client.close() - -server.set_static_dir("/static") -server.on("GET", "/H", onLedHigh) -server.on("GET", "/L", onLedLow) -server.on("POST", "/ajax/ledcolor", onLedColor) - - -print("open this IP in your browser: ", esp.pretty_ip(esp.ip_address)) - -server.start() - -while True: - server.update_poll() diff --git a/examples/esp32spi_wsgiserver.py b/examples/esp32spi_wsgiserver.py new file mode 100644 index 0000000..efc4f50 --- /dev/null +++ b/examples/esp32spi_wsgiserver.py @@ -0,0 +1,188 @@ +import board +import busio +import os +from digitalio import DigitalInOut + +from adafruit_esp32spi import adafruit_esp32spi +import adafruit_esp32spi.adafruit_esp32spi_wifimanager as wifimanager +import adafruit_esp32spi.adafruit_esp32spi_wsgiserver as server + +# This example depends on the 'static' folder in the examples folder +# being copied to the root of the circuitpython filesystem. +# This is where our static assets like html, js, and css live. + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +try: + import json as json_module +except ImportError: + import ujson as json_module + +"""Use below for Most Boards""" +import neopixel +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +"""Uncomment below for ItsyBitsy M4""" +# import adafruit_dotstar as dotstar +# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) + +print("ESP32 SPI simple web server test!") + +esp32_cs = DigitalInOut(board.D10) +esp32_ready = DigitalInOut(board.D9) +esp32_reset = DigitalInOut(board.D7) +esp32_gpio0 = DigitalInOut(board.D12) + +spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0, debug=False) + +## Connect to wifi with secrets +wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light, debug=True) +wifi.connect() + +class SimpleWSGIApplication: + """ + An example of a simple WSGI Application that supports + basic route handling and static asset file serving for common file types + """ + + INDEX = "/index.html" + CHUNK_SIZE = 8912 # max number of bytes to read at once when reading files + + def __init__(self, static_dir=None, debug=False): + self._debug = debug + self._listeners = {} + self._start_response = None + self._static = static_dir + if self._static: + self._static_files = ["/" + file for file in os.listdir(self._static)] + + def __call__(self, environ, start_response): + """ + Called whenever the server gets a request. + The environ dict has details about the request per wsgi specification. + Call start_response with the response status string and headers as a list of tuples. + Return a single item list with the item being your response data string. + """ + if self._debug: + self._log_environ(environ) + + self._start_response = start_response + status = "" + headers = [] + resp_data = [] + + key = self._get_listener_key(environ["REQUEST_METHOD"].lower(), environ["PATH_INFO"]) + if key in self._listeners: + status, headers, resp_data = self._listeners[key](environ) + if environ["REQUEST_METHOD"].lower() == "get" and self._static: + path = environ["PATH_INFO"] + if path in self._static_files: + status, headers, resp_data = self.serve_file(path, directory=self._static) + elif path == "/" and self.INDEX in self._static_files: + status, headers, resp_data = self.serve_file(self.INDEX, directory=self._static) + + self._start_response(status, headers) + return resp_data + + def on(self, method, path, request_handler): + """ + Register a Request Handler for a particular HTTP method and path. + request_handler will be called whenever a matching HTTP request is received. + + request_handler should accept the following args: + (Dict environ) + request_handler should return a tuple in the shape of: + (status, header_list, data_iterable) + + :param str method: the method of the HTTP request + :param str path: the path of the HTTP request + :param func request_handler: the function to call + """ + self._listeners[self._get_listener_key(method, path)] = request_handler + + def serve_file(self, file_path, directory=None): + status = "200 OK" + headers = [("Content-Type", self._get_content_type(file_path))] + + full_path = file_path if not directory else directory + file_path + def resp_iter(): + with open(full_path, 'rb') as file: + while True: + chunk = file.read(self.CHUNK_SIZE) + if chunk: + yield chunk + else: + break + + return (status, headers, resp_iter()) + + def _log_environ(self, environ): # pylint: disable=no-self-use + print("environ map:") + for name, value in environ.items(): + print(name, value) + + def _get_listener_key(self, method, path): # pylint: disable=no-self-use + return "{0}|{1}".format(method.lower(), path) + + def _get_content_type(self, file): # pylint: disable=no-self-use + ext = file.split('.')[-1] + if ext in ("html", "htm"): + return "text/html" + if ext == "js": + return "application/javascript" + if ext == "css": + return "text/css" + if ext in ("jpg", "jpeg"): + return "image/jpeg" + if ext == "png": + return "image/png" + return "text/plain" + +# Our HTTP Request handlers +def led_on(environ): # pylint: disable=unused-argument + print("led on!") + status_light.fill(0, 0, 100) + return web_app.serve_file("static/index.html") + +def led_off(environ): # pylint: disable=unused-argument + print("led off!") + status_light.fill(0) + return web_app.serve_file("static/index.html") + +def led_color(environ): # pylint: disable=unused-argument + json = json_module.loads(environ["wsgi.input"].getvalue()) + print(json) + rgb_tuple = (json.get("r"), json.get("g"), json.get("b")) + status_light.fill(rgb_tuple) + return ("200 OK", [], []) + +# Here we create our application, setting the static directory location +# and registering the above request_handlers for specific HTTP requests +# we want to listen and respond to. +web_app = SimpleWSGIApplication(static_dir="/static") +web_app.on("GET", "/led_on", led_on) +web_app.on("GET", "/led_off", led_off) +web_app.on("POST", "/ajax/ledcolor", led_color) + +# Here we setup our server, passing in our web_app as the application +server.set_interface(esp) +wsgiServer = server.WSGIServer(80, application=web_app) + +print("open this IP in your browser: ", esp.pretty_ip(esp.ip_address)) + +# Start the server +wsgiServer.start() +while True: + # Our main loop where we have the server poll for incoming requests + try: + wsgiServer.update_poll() + # Could do any other background tasks here, like reading sensors + except (ValueError, RuntimeError) as e: + print("Failed to update server, restarting ESP32\n", e) + wifi.reset() + continue From d1fe7774ad50f9dd788ad50a71115ef8b1e491ce Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 13:54:56 -0400 Subject: [PATCH 13/25] fix led_on to pass rgb tuple --- examples/esp32spi_wsgiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/esp32spi_wsgiserver.py b/examples/esp32spi_wsgiserver.py index efc4f50..59b7b9b 100644 --- a/examples/esp32spi_wsgiserver.py +++ b/examples/esp32spi_wsgiserver.py @@ -146,7 +146,7 @@ def _get_content_type(self, file): # pylint: disable=no-self-use # Our HTTP Request handlers def led_on(environ): # pylint: disable=unused-argument print("led on!") - status_light.fill(0, 0, 100) + status_light.fill((0, 0, 100)) return web_app.serve_file("static/index.html") def led_off(environ): # pylint: disable=unused-argument From 842ac464e1920c2646ec587fb1610df3f62f6773 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 14:11:10 -0400 Subject: [PATCH 14/25] Remove server.py in favor of new wsgiserver.py --- adafruit_esp32spi/adafruit_esp32spi_server.py | 193 ------------------ 1 file changed, 193 deletions(-) delete mode 100644 adafruit_esp32spi/adafruit_esp32spi_server.py diff --git a/adafruit_esp32spi/adafruit_esp32spi_server.py b/adafruit_esp32spi/adafruit_esp32spi_server.py deleted file mode 100644 index 8ab2696..0000000 --- a/adafruit_esp32spi/adafruit_esp32spi_server.py +++ /dev/null @@ -1,193 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2019 ladyada for Adafruit Industries -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -""" -`adafruit_esp32spi_server` -================================================================================ - -TODO: better description? -Server management lib to make handling and responding to incoming requests much easier - -* Author(s): Matt Costi -""" -# pylint: disable=no-name-in-module - -import os -from micropython import const -import adafruit_esp32spi.adafruit_esp32spi_socket as socket -from adafruit_esp32spi.adafruit_esp32spi_requests import parse_headers - -_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) - -NO_SOCK_AVAIL = const(255) -INDEX_HTML = "/index.html" - - -# pylint: disable=unused-argument, redefined-builtin, invalid-name -class server: - """ TODO: class docs """ - def __init__(self, port=80, debug=False): - self.port = port - self._server_sock = socket.socket(socknum=NO_SOCK_AVAIL) - self._client_sock = socket.socket(socknum=NO_SOCK_AVAIL) - self._debug = debug - self._listeners = {} - self._static_dir = None - self._static_files = [] - - - def start(self): - """ start the server """ - self._server_sock = socket.socket() - _the_interface.start_server(self.port, self._server_sock.socknum) - if self._debug: - ip = _the_interface.pretty_ip(_the_interface.ip_address) - print("Server available at {0}:{1}".format(ip, self.port)) - print("Sever status: ", _the_interface.get_server_state(self._server_sock.socknum)) - - def on(self, method, path, request_handler): - """ - Register a Request Handler for a particular HTTP method and path. - request_handler will be called whenever a matching HTTP request is received. - - request_handler should accept the following args: - (Dict headers, bytes body, Socket client) - - :param str method: the method of the HTTP request - :param str path: the path of the HTTP request - :param func request_handler: the function to call - """ - self._listeners[self._get_listener_key(method, path)] = request_handler - - def set_static_dir(self, directory_path): - """ - allows for setting a directory of static files that will be auto-served - when that file is GET requested at'/' - index.html will also be made available at root path '/' - - Note: does not support serving files in child folders at this time - """ - self._static_dir = directory_path - self._static_files = ["/" + file for file in os.listdir(self._static_dir)] - print(self._static_files) - - def serve_file(self, file_path, dir=None): - """ - writes a file from the file system as a response to the client. - - :param string file_path: path to the image to write to client. - if dir is not present, it is treated as an absolute path - :param string dir: path to directory that file is located in (optional) - """ - self._client_sock.write(b"HTTP/1.1 200 OK\r\n") - self._client_sock.write(b"Content-Type:" + self._get_content_type(file_path) + b"\r\n") - self._client_sock.write(b"\r\n") - full_path = file_path if not dir else dir + file_path - with open(full_path, 'rb') as fp: - for line in fp: - self._client_sock.write(line) - self._client_sock.write(b"\r\n") - self._client_sock.close() - - 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 for - which a request handler has been registered with 'on' method, that - request handler will be invoked. - - Unrecognized requests will be automatically be responded to with a 404. - """ - client = self.client_available() - if (client and client.available()): - line = client.readline() - line = line.split(None, 2) - method = str(line[0], "utf-8") - path = str(line[1], "utf-8") - key = self._get_listener_key(method, path) - if key in self._listeners: - headers = parse_headers(client) - body = client.read() - self._listeners[key](headers, body, client) - elif method.lower() == "get": - client.read() - if path in self._static_files: - self.serve_file(path, dir=self._static_dir) - elif path == "/" and INDEX_HTML in self._static_files: - self.serve_file(INDEX_HTML, dir=self._static_dir) - else: - # TODO: support optional custom 404 handler? - self._client_sock.write(b"HTTP/1.1 404 NotFound\r\n") - client.close() - - - def client_available(self): - """ - returns a client socket connection if available. - Otherwise, returns None - :return: the client - :rtype: Socket - """ - sock = None - if self._server_sock.socknum != NO_SOCK_AVAIL: - if self._client_sock.socknum != NO_SOCK_AVAIL: - # check previous received client socket - if self._debug: - print("checking if last client sock still valid") - if self._client_sock.connected() and self._client_sock.available(): - sock = self._client_sock - if not sock: - # check for new client sock - if self._debug: - print("checking for new client sock") - client_sock_num = _the_interface.socket_available(self._server_sock.socknum) - sock = socket.socket(socknum=client_sock_num) - else: - print("Server has not been started, cannot check for clients!") - - if sock and sock.socknum != NO_SOCK_AVAIL: - if self._debug: - print("client sock num is: ", sock.socknum) - self._client_sock = sock - return self._client_sock - - return None - - def _get_listener_key(self, method, path): # pylint: disable=no-self-use - return "{0}|{1}".format(method.lower(), path) - - - def _get_content_type(self, file): # pylint: disable=no-self-use - ext = file.split('.')[-1] - if ext in ("html", "htm"): - return b"text/html" - if ext == "js": - return b"application/javascript" - if ext == "css": - return b"text/css" - # TODO: test adding in support for image types as well - return b"text/plain" From c2b7cb065f9695da7cfc39ff70e8fa6efbda1708 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 14:11:43 -0400 Subject: [PATCH 15/25] Address PR comments for adafruit_esp32spi.py --- adafruit_esp32spi/adafruit_esp32spi.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi.py b/adafruit_esp32spi/adafruit_esp32spi.py index 3ad6b67..96bc200 100644 --- a/adafruit_esp32spi/adafruit_esp32spi.py +++ b/adafruit_esp32spi/adafruit_esp32spi.py @@ -54,7 +54,6 @@ # pylint: disable=bad-whitespace _SET_NET_CMD = const(0x10) _SET_PASSPHRASE_CMD = const(0x11) -_SET_AP_PASSPHRASE_CMD = const(0x19) _SET_DEBUG_CMD = const(0x1A) _GET_CONN_STATUS_CMD = const(0x20) @@ -629,22 +628,15 @@ def start_server(self, port, socket_num, conn_mode=TCP_MODE, ip=None): # pylint: if self._debug: print("*** starting server") self._socknum_ll[0][0] = socket_num - port_param = struct.pack('>H', port) - if ip: # use the 4 arg version - resp = self._send_command_get_response(_START_SERVER_TCP_CMD, - (ip, - port_param, - self._socknum_ll[0], - (conn_mode,))) - else: # use the 3 arg version - resp = self._send_command_get_response(_START_SERVER_TCP_CMD, - (port_param, - self._socknum_ll[0], - (conn_mode,))) + params = [struct.pack('>H', port), self._socknum_ll[0], (conn_mode,)] + if ip: + params.insert(0, ip) + resp = self._send_command_get_response(_START_SERVER_TCP_CMD, params) + if resp[0][0] != 1: raise RuntimeError("Could not start server") - def get_server_state(self, socket_num): + def server_state(self, socket_num): """Get the state of the ESP32's internal reference server socket number""" self._socknum_ll[0][0] = socket_num resp = self._send_command_get_response(_GET_STATE_TCP_CMD, self._socknum_ll) From 4f3d07f43d37e7531cb8fc05558dcd0209c07018 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 14:31:06 -0400 Subject: [PATCH 16/25] fix file name. adafruite -> adafruit --- ...ite_esp32spi_wsgiserver.py => adafruit_esp32spi_wsgiserver.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename adafruit_esp32spi/{adafruite_esp32spi_wsgiserver.py => adafruit_esp32spi_wsgiserver.py} (100%) diff --git a/adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py b/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py similarity index 100% rename from adafruit_esp32spi/adafruite_esp32spi_wsgiserver.py rename to adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py From fc97dacf8299a10945296b96b5c3e841e8757b57 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 14:36:55 -0400 Subject: [PATCH 17/25] fix linting of example file --- examples/esp32spi_wsgiserver.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/esp32spi_wsgiserver.py b/examples/esp32spi_wsgiserver.py index 59b7b9b..a18df83 100644 --- a/examples/esp32spi_wsgiserver.py +++ b/examples/esp32spi_wsgiserver.py @@ -1,7 +1,8 @@ +import os import board import busio -import os from digitalio import DigitalInOut +import neopixel from adafruit_esp32spi import adafruit_esp32spi import adafruit_esp32spi.adafruit_esp32spi_wifimanager as wifimanager @@ -23,13 +24,6 @@ except ImportError: import ujson as json_module -"""Use below for Most Boards""" -import neopixel -status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards -"""Uncomment below for ItsyBitsy M4""" -# import adafruit_dotstar as dotstar -# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) - print("ESP32 SPI simple web server test!") esp32_cs = DigitalInOut(board.D10) @@ -38,7 +32,13 @@ esp32_gpio0 = DigitalInOut(board.D12) spi = busio.SPI(board.SCK, board.MOSI, board.MISO) -esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0, debug=False) +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0) # pylint: disable=line-too-long + +"""Use below for Most Boards""" +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +"""Uncomment below for ItsyBitsy M4""" +# import adafruit_dotstar as dotstar +# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) ## Connect to wifi with secrets wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light, debug=True) From 41eab1cccfc9e1bf30143114445b08652bfd0dfe Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 14:45:17 -0400 Subject: [PATCH 18/25] PATH_INFO shouldn't contain query params --- adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py b/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py index 6784aaf..bdb7e8a 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py +++ b/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py @@ -190,12 +190,14 @@ def _get_environ(self, client): env["REQUEST_METHOD"] = method env["SCRIPT_NAME"] = "" - env["PATH_INFO"] = path 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 = parse_headers(client) if "content-type" in headers: From e6bfd362a34d7191a867bec223d7e052bfb2f9e4 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 22:31:54 -0400 Subject: [PATCH 19/25] remove wifimanager debug arg - Already in ap_mode branch --- adafruit_esp32spi/adafruit_esp32spi_wifimanager.py | 8 ++++---- examples/esp32spi_wsgiserver.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py index 0a00103..28b08b0 100755 --- a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py +++ b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py @@ -43,8 +43,8 @@ class ESPSPI_WiFiManager: NORMAL = const(1) ENTERPRISE = const(2) - # pylint: disable=too-many-arguments, line-too-long - def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type=NORMAL, debug=False): +# pylint: disable=too-many-arguments + def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type=NORMAL): """ :param ESP_SPIcontrol esp: The ESP object we are using :param dict secrets: The WiFi and Adafruit IO secrets dict (See examples) @@ -56,7 +56,7 @@ def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type= """ # Read the settings self.esp = esp - self.debug = debug + self.debug = False self.ssid = secrets['ssid'] self.password = secrets['password'] self.attempts = attempts @@ -78,7 +78,7 @@ def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type= self.ent_user = secrets['ent_user'] if secrets.get('ent_password'): self.ent_password = secrets['ent_password'] - # pylint: enable=too-many-arguments, line-too-long +# pylint: enable=too-many-arguments def reset(self): """ diff --git a/examples/esp32spi_wsgiserver.py b/examples/esp32spi_wsgiserver.py index a18df83..f83a319 100644 --- a/examples/esp32spi_wsgiserver.py +++ b/examples/esp32spi_wsgiserver.py @@ -41,7 +41,7 @@ # status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) ## Connect to wifi with secrets -wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light, debug=True) +wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) wifi.connect() class SimpleWSGIApplication: From 473f8508587eed4c9c86d762b88c4bc6ec866965 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Sun, 21 Jul 2019 22:37:23 -0400 Subject: [PATCH 20/25] fix PATH_INFO when there are query params --- adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py b/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py index bdb7e8a..0d538e8 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py +++ b/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py @@ -194,7 +194,7 @@ def _get_environ(self, client): env["SERVER_PROTOCOL"] = ver env["SERVER_PORT"] = self.port if path.find("?") >= 0: - env["PATH_INFO"] = path.split()[0] + env["PATH_INFO"] = path.split("?")[0] env["QUERY_STRING"] = path.split("?")[1] else: env["PATH_INFO"] = path From a93c0998a9afb961454b9aa5d2a3110420ba26ce Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Mon, 22 Jul 2019 21:48:32 -0400 Subject: [PATCH 21/25] Address PR comments --- examples/esp32spi_wsgiserver.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/examples/esp32spi_wsgiserver.py b/examples/esp32spi_wsgiserver.py index f83a319..a1ec4a1 100644 --- a/examples/esp32spi_wsgiserver.py +++ b/examples/esp32spi_wsgiserver.py @@ -26,10 +26,15 @@ print("ESP32 SPI simple web server test!") -esp32_cs = DigitalInOut(board.D10) -esp32_ready = DigitalInOut(board.D9) -esp32_reset = DigitalInOut(board.D7) -esp32_gpio0 = DigitalInOut(board.D12) +# If you are using a board with pre-defined ESP32 Pins: +esp32_cs = DigitalInOut(board.ESP_CS) +esp32_ready = DigitalInOut(board.ESP_BUSY) +esp32_reset = DigitalInOut(board.ESP_RESET) + +# If you have an externally connected ESP32: +# esp32_cs = DigitalInOut(board.D9) +# esp32_ready = DigitalInOut(board.D10) +# esp32_reset = DigitalInOut(board.D5) spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0) # pylint: disable=line-too-long @@ -164,7 +169,19 @@ def led_color(environ): # pylint: disable=unused-argument # Here we create our application, setting the static directory location # and registering the above request_handlers for specific HTTP requests # we want to listen and respond to. -web_app = SimpleWSGIApplication(static_dir="/static") +static_dir = "/static" +try: + static_files = os.listdir(static_dir) + if "index.html" not in static_files: + raise RuntimeError(""" + This example depends on an index.html, but it isn't present. + Please add it to the {0} directory""".format(static_dir)) +except (OSError) as e: + raise RuntimeError(""" + This example depends on a static asset directory. + Please create one named {0} in the root of the device filesystem.""".format(static_dir)) + +web_app = SimpleWSGIApplication(static_dir=static_dir) web_app.on("GET", "/led_on", led_on) web_app.on("GET", "/led_off", led_off) web_app.on("POST", "/ajax/ledcolor", led_color) From fd4e191573cdc65d9d10c0a555b98b35d1511c91 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Mon, 22 Jul 2019 22:06:51 -0400 Subject: [PATCH 22/25] Use constants instead of magic numbers --- adafruit_esp32spi/adafruit_esp32spi_socket.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socket.py b/adafruit_esp32spi/adafruit_esp32spi_socket.py index 6737d14..40e518f 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socket.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socket.py @@ -33,6 +33,7 @@ import time import gc from micropython import const +from adafruit_esp32spi import adafruit_esp32spi _the_interface = None # pylint: disable=invalid-name def set_interface(iface): @@ -165,15 +166,14 @@ def connected(self): return True else: status = _the_interface.socket_status(self.socknum) - # TODO: why is esp. not defined? using magic numbers in mean time - result = status not in (1, - 0, - 5, - 6, - 10, - 2, - 3, - 7) + result = status not in (adafruit_esp32spi.SOCKET_LISTEN, + adafruit_esp32spi.SOCKET_CLOSED, + adafruit_esp32spi.SOCKET_FIN_WAIT_1, + adafruit_esp32spi.SOCKET_FIN_WAIT_2, + adafruit_esp32spi.SOCKET_TIME_WAIT, + adafruit_esp32spi.SOCKET_SYN_SENT, + adafruit_esp32spi.SOCKET_SYN_RCVD, + adafruit_esp32spi.SOCKET_CLOSE_WAIT) if not result: self.close() self._socknum = NO_SOCKET_AVAIL From 6951f8ce018e850afd829e75e2b62067c42e1fff Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Mon, 22 Jul 2019 22:11:30 -0400 Subject: [PATCH 23/25] fix import? --- adafruit_esp32spi/adafruit_esp32spi_socket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socket.py b/adafruit_esp32spi/adafruit_esp32spi_socket.py index 40e518f..35c82de 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socket.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socket.py @@ -33,7 +33,7 @@ import time import gc from micropython import const -from adafruit_esp32spi import adafruit_esp32spi +import adafruit_esp32spi _the_interface = None # pylint: disable=invalid-name def set_interface(iface): From a5d59f8fb41b19720deea1c21c92cb022daaf8d3 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Mon, 22 Jul 2019 22:30:10 -0400 Subject: [PATCH 24/25] fix linting --- adafruit_esp32spi/adafruit_esp32spi_socket.py | 3 ++- examples/esp32spi_wsgiserver.py | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/adafruit_esp32spi/adafruit_esp32spi_socket.py b/adafruit_esp32spi/adafruit_esp32spi_socket.py index 35c82de..f5c288b 100644 --- a/adafruit_esp32spi/adafruit_esp32spi_socket.py +++ b/adafruit_esp32spi/adafruit_esp32spi_socket.py @@ -29,11 +29,12 @@ * Author(s): ladyada """ +# pylint: disable=no-name-in-module import time import gc from micropython import const -import adafruit_esp32spi +from adafruit_esp32spi import adafruit_esp32spi _the_interface = None # pylint: disable=invalid-name def set_interface(iface): diff --git a/examples/esp32spi_wsgiserver.py b/examples/esp32spi_wsgiserver.py index a1ec4a1..c971300 100644 --- a/examples/esp32spi_wsgiserver.py +++ b/examples/esp32spi_wsgiserver.py @@ -37,7 +37,7 @@ # esp32_reset = DigitalInOut(board.D5) spi = busio.SPI(board.SCK, board.MOSI, board.MISO) -esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, gpio0_pin=esp32_gpio0) # pylint: disable=line-too-long +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) # pylint: disable=line-too-long """Use below for Most Boards""" status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards @@ -169,19 +169,19 @@ def led_color(environ): # pylint: disable=unused-argument # Here we create our application, setting the static directory location # and registering the above request_handlers for specific HTTP requests # we want to listen and respond to. -static_dir = "/static" +static = "/static" try: - static_files = os.listdir(static_dir) + static_files = os.listdir(static) if "index.html" not in static_files: raise RuntimeError(""" This example depends on an index.html, but it isn't present. - Please add it to the {0} directory""".format(static_dir)) + Please add it to the {0} directory""".format(static)) except (OSError) as e: raise RuntimeError(""" This example depends on a static asset directory. - Please create one named {0} in the root of the device filesystem.""".format(static_dir)) + Please create one named {0} in the root of the device filesystem.""".format(static)) -web_app = SimpleWSGIApplication(static_dir=static_dir) +web_app = SimpleWSGIApplication(static_dir=static) web_app.on("GET", "/led_on", led_on) web_app.on("GET", "/led_off", led_off) web_app.on("POST", "/ajax/ledcolor", led_color) From 8dbeef02125e4f27d49fe468d78912afc366a200 Mon Sep 17 00:00:00 2001 From: Matt Costi Date: Tue, 23 Jul 2019 19:01:43 -0400 Subject: [PATCH 25/25] move all server example assets into examples/server --- examples/{ => server}/esp32spi_wsgiserver.py | 0 examples/{ => server}/static/index.html | 0 examples/{ => server}/static/led_color_picker_example.js | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename examples/{ => server}/esp32spi_wsgiserver.py (100%) rename examples/{ => server}/static/index.html (100%) rename examples/{ => server}/static/led_color_picker_example.js (100%) diff --git a/examples/esp32spi_wsgiserver.py b/examples/server/esp32spi_wsgiserver.py similarity index 100% rename from examples/esp32spi_wsgiserver.py rename to examples/server/esp32spi_wsgiserver.py diff --git a/examples/static/index.html b/examples/server/static/index.html similarity index 100% rename from examples/static/index.html rename to examples/server/static/index.html diff --git a/examples/static/led_color_picker_example.js b/examples/server/static/led_color_picker_example.js similarity index 100% rename from examples/static/led_color_picker_example.js rename to examples/server/static/led_color_picker_example.js