-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from FoamyGuy/compatibility_refactor
Move esp32spi_wsgi server code to here
- Loading branch information
Showing
5 changed files
with
647 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
# SPDX-FileCopyrightText: Copyright (c) 2019 Matt Costi for Adafruit Industries | ||
# | ||
# SPDX-License-Identifier: MIT | ||
|
||
""" | ||
`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, protected-access | ||
|
||
import io | ||
import gc | ||
import time | ||
|
||
from micropython import const | ||
import adafruit_esp32spi.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) | ||
|
||
|
||
def parse_headers(client): | ||
""" | ||
Parses the header portion of an HTTP request from the socket. | ||
Expects first line of HTTP request to have been read already. | ||
""" | ||
headers = {} | ||
while True: | ||
# line = str(client.readline(), "utf-8") | ||
line = str(socket_readline(client), "utf-8") | ||
if not line: | ||
break | ||
title, content = line.split(":", 1) | ||
headers[title.strip().lower()] = content.strip() | ||
return headers | ||
|
||
|
||
def socket_readline(_socket, eol=b"\r\n"): | ||
"""Attempt to return as many bytes as we can up to but not including | ||
end-of-line character (default is '\\r\\n')""" | ||
|
||
# print("Socket readline") | ||
stamp = time.monotonic() | ||
while eol not in _socket._buffer: | ||
# there's no line already in there, read some more | ||
avail = _socket._available() | ||
if avail: | ||
_socket._buffer += _the_interface.socket_read(_socket._socknum, avail) | ||
elif _socket._timeout > 0 and time.monotonic() - stamp > _socket._timeout: | ||
_socket.close() # Make sure to close socket so that we don't exhaust sockets. | ||
raise OSError("Didn't receive full response, failing out") | ||
firstline, _socket._buffer = _socket._buffer.split(eol, 1) | ||
gc.collect() | ||
return firstline | ||
|
||
|
||
# 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( | ||
"Server status: ", | ||
_the_interface.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 or "500 ISE") | ||
for header in self._response_headers: | ||
response += "{0}: {1}\r\n".format(*header) | ||
response += "\r\n" | ||
self._client_sock.send(response.encode("utf-8")) | ||
if isinstance(result, bytes): # send whole response if possible (see #174) | ||
self._client_sock.send(result) | ||
elif isinstance(result, str): | ||
self._client_sock.send(result.encode("utf-8")) | ||
else: # fall back to sending byte-by-byte | ||
for data in result: | ||
if isinstance(data, bytes): | ||
self._client_sock.send(data) | ||
else: | ||
self._client_sock.send(data.encode("utf-8")) | ||
gc.collect() | ||
finally: | ||
if self._debug > 2: | ||
print("closing") | ||
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"), | ||
("Connection", "close"), | ||
] + 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") | ||
line = str(socket_readline(client), "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 = 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.recv(int(env["CONTENT_LENGTH"])) | ||
env["wsgi.input"] = io.StringIO(body) | ||
else: | ||
body = client.recv(0) | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<!-- | ||
SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries | ||
SPDX-License-Identifier: MIT | ||
--> | ||
|
||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<script async src="led_color_picker_example.js"></script> | ||
</head> | ||
<body> | ||
<h1>LED color picker demo!</h1> | ||
<canvas id="colorPicker" height="300px" width="300px"></canvas> | ||
</body> | ||
</html> |
Oops, something went wrong.