Skip to content

Commit 63ede55

Browse files
authored
Merge pull request #20 from FoamyGuy/compatibility_refactor
Move esp32spi_wsgi server code to here
2 parents 7c55ba5 + 7e8391d commit 63ede55

File tree

5 files changed

+647
-1
lines changed

5 files changed

+647
-1
lines changed

adafruit_wsgi/esp32spi_wsgiserver.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2019 Matt Costi for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
`esp32spi_wsgiserver`
7+
================================================================================
8+
9+
A simple WSGI (Web Server Gateway Interface) server that interfaces with the ESP32 over SPI.
10+
Opens a specified port on the ESP32 to listen for incoming HTTP Requests and
11+
Accepts an Application object that must be callable, which gets called
12+
whenever a new HTTP Request has been received.
13+
14+
The Application MUST accept 2 ordered parameters:
15+
1. environ object (incoming request data)
16+
2. start_response function. Must be called before the Application
17+
callable returns, in order to set the response status and headers.
18+
19+
The Application MUST return a single string in a list,
20+
which is the response data
21+
22+
Requires update_poll being called in the applications main event loop.
23+
24+
For more details about Python WSGI see:
25+
https://www.python.org/dev/peps/pep-0333/
26+
27+
* Author(s): Matt Costi
28+
"""
29+
# pylint: disable=no-name-in-module, protected-access
30+
31+
import io
32+
import gc
33+
import time
34+
35+
from micropython import const
36+
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
37+
38+
_the_interface = None # pylint: disable=invalid-name
39+
40+
41+
def set_interface(iface):
42+
"""Helper to set the global internet interface"""
43+
global _the_interface # pylint: disable=global-statement, invalid-name
44+
_the_interface = iface
45+
socket.set_interface(iface)
46+
47+
48+
NO_SOCK_AVAIL = const(255)
49+
50+
51+
def parse_headers(client):
52+
"""
53+
Parses the header portion of an HTTP request from the socket.
54+
Expects first line of HTTP request to have been read already.
55+
"""
56+
headers = {}
57+
while True:
58+
# line = str(client.readline(), "utf-8")
59+
line = str(socket_readline(client), "utf-8")
60+
if not line:
61+
break
62+
title, content = line.split(":", 1)
63+
headers[title.strip().lower()] = content.strip()
64+
return headers
65+
66+
67+
def socket_readline(_socket, eol=b"\r\n"):
68+
"""Attempt to return as many bytes as we can up to but not including
69+
end-of-line character (default is '\\r\\n')"""
70+
71+
# print("Socket readline")
72+
stamp = time.monotonic()
73+
while eol not in _socket._buffer:
74+
# there's no line already in there, read some more
75+
avail = _socket._available()
76+
if avail:
77+
_socket._buffer += _the_interface.socket_read(_socket._socknum, avail)
78+
elif _socket._timeout > 0 and time.monotonic() - stamp > _socket._timeout:
79+
_socket.close() # Make sure to close socket so that we don't exhaust sockets.
80+
raise OSError("Didn't receive full response, failing out")
81+
firstline, _socket._buffer = _socket._buffer.split(eol, 1)
82+
gc.collect()
83+
return firstline
84+
85+
86+
# pylint: disable=invalid-name
87+
class WSGIServer:
88+
"""
89+
A simple server that implements the WSGI interface
90+
"""
91+
92+
def __init__(self, port=80, debug=False, application=None):
93+
self.application = application
94+
self.port = port
95+
self._server_sock = socket.socket(socknum=NO_SOCK_AVAIL)
96+
self._client_sock = socket.socket(socknum=NO_SOCK_AVAIL)
97+
self._debug = debug
98+
99+
self._response_status = None
100+
self._response_headers = []
101+
102+
def start(self):
103+
"""
104+
starts the server and begins listening for incoming connections.
105+
Call update_poll in the main loop for the application callable to be
106+
invoked on receiving an incoming request.
107+
"""
108+
self._server_sock = socket.socket()
109+
_the_interface.start_server(self.port, self._server_sock._socknum)
110+
if self._debug:
111+
ip = _the_interface.pretty_ip(_the_interface.ip_address)
112+
print("Server available at {0}:{1}".format(ip, self.port))
113+
print(
114+
"Server status: ",
115+
_the_interface.server_state(self._server_sock._socknum),
116+
)
117+
118+
def update_poll(self):
119+
"""
120+
Call this method inside your main event loop to get the server
121+
check for new incoming client requests. When a request comes in,
122+
the application callable will be invoked.
123+
"""
124+
self.client_available()
125+
if self._client_sock and self._client_sock._available():
126+
environ = self._get_environ(self._client_sock)
127+
result = self.application(environ, self._start_response)
128+
self.finish_response(result)
129+
130+
def finish_response(self, result):
131+
"""
132+
Called after the application callbile returns result data to respond with.
133+
Creates the HTTP Response payload from the response_headers and results data,
134+
and sends it back to client.
135+
136+
:param string result: the data string to send back in the response to the client.
137+
"""
138+
try:
139+
response = "HTTP/1.1 {0}\r\n".format(self._response_status or "500 ISE")
140+
for header in self._response_headers:
141+
response += "{0}: {1}\r\n".format(*header)
142+
response += "\r\n"
143+
self._client_sock.send(response.encode("utf-8"))
144+
if isinstance(result, bytes): # send whole response if possible (see #174)
145+
self._client_sock.send(result)
146+
elif isinstance(result, str):
147+
self._client_sock.send(result.encode("utf-8"))
148+
else: # fall back to sending byte-by-byte
149+
for data in result:
150+
if isinstance(data, bytes):
151+
self._client_sock.send(data)
152+
else:
153+
self._client_sock.send(data.encode("utf-8"))
154+
gc.collect()
155+
finally:
156+
if self._debug > 2:
157+
print("closing")
158+
self._client_sock.close()
159+
160+
def client_available(self):
161+
"""
162+
returns a client socket connection if available.
163+
Otherwise, returns None
164+
:return: the client
165+
:rtype: Socket
166+
"""
167+
sock = None
168+
if self._server_sock._socknum != NO_SOCK_AVAIL:
169+
if self._client_sock._socknum != NO_SOCK_AVAIL:
170+
# check previous received client socket
171+
if self._debug > 2:
172+
print("checking if last client sock still valid")
173+
if self._client_sock._connected() and self._client_sock._available():
174+
sock = self._client_sock
175+
if not sock:
176+
# check for new client sock
177+
if self._debug > 2:
178+
print("checking for new client sock")
179+
client_sock_num = _the_interface.socket_available(
180+
self._server_sock._socknum
181+
)
182+
sock = socket.socket(socknum=client_sock_num)
183+
else:
184+
print("Server has not been started, cannot check for clients!")
185+
186+
if sock and sock._socknum != NO_SOCK_AVAIL:
187+
if self._debug > 2:
188+
print("client sock num is: ", sock._socknum)
189+
self._client_sock = sock
190+
return self._client_sock
191+
192+
return None
193+
194+
def _start_response(self, status, response_headers):
195+
"""
196+
The application callable will be given this method as the second param
197+
This is to be called before the application callable returns, to signify
198+
the response can be started with the given status and headers.
199+
200+
:param string status: a status string including the code and reason. ex: "200 OK"
201+
:param list response_headers: a list of tuples to represent the headers.
202+
ex ("header-name", "header value")
203+
"""
204+
self._response_status = status
205+
self._response_headers = [
206+
("Server", "esp32WSGIServer"),
207+
("Connection", "close"),
208+
] + response_headers
209+
210+
def _get_environ(self, client):
211+
"""
212+
The application callable will be given the resulting environ dictionary.
213+
It contains metadata about the incoming request and the request body ("wsgi.input")
214+
215+
:param Socket client: socket to read the request from
216+
"""
217+
env = {}
218+
# line = str(client.readline(), "utf-8")
219+
line = str(socket_readline(client), "utf-8")
220+
(method, path, ver) = line.rstrip("\r\n").split(None, 2)
221+
222+
env["wsgi.version"] = (1, 0)
223+
env["wsgi.url_scheme"] = "http"
224+
env["wsgi.multithread"] = False
225+
env["wsgi.multiprocess"] = False
226+
env["wsgi.run_once"] = False
227+
228+
env["REQUEST_METHOD"] = method
229+
env["SCRIPT_NAME"] = ""
230+
env["SERVER_NAME"] = _the_interface.pretty_ip(_the_interface.ip_address)
231+
env["SERVER_PROTOCOL"] = ver
232+
env["SERVER_PORT"] = self.port
233+
if path.find("?") >= 0:
234+
env["PATH_INFO"] = path.split("?")[0]
235+
env["QUERY_STRING"] = path.split("?")[1]
236+
else:
237+
env["PATH_INFO"] = path
238+
239+
headers = parse_headers(client)
240+
if "content-type" in headers:
241+
env["CONTENT_TYPE"] = headers.get("content-type")
242+
if "content-length" in headers:
243+
env["CONTENT_LENGTH"] = headers.get("content-length")
244+
body = client.recv(int(env["CONTENT_LENGTH"]))
245+
env["wsgi.input"] = io.StringIO(body)
246+
else:
247+
body = client.recv(0)
248+
env["wsgi.input"] = io.StringIO(body)
249+
for name, value in headers.items():
250+
key = "HTTP_" + name.replace("-", "_").upper()
251+
if key in env:
252+
value = "{0},{1}".format(env[key], value)
253+
env[key] = value
254+
255+
return env

examples/static/index.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries
3+
4+
SPDX-License-Identifier: MIT
5+
-->
6+
7+
<!DOCTYPE html>
8+
<html>
9+
<head>
10+
<script async src="led_color_picker_example.js"></script>
11+
</head>
12+
<body>
13+
<h1>LED color picker demo!</h1>
14+
<canvas id="colorPicker" height="300px" width="300px"></canvas>
15+
</body>
16+
</html>

0 commit comments

Comments
 (0)