-
Notifications
You must be signed in to change notification settings - Fork 74
Add Server creation and management support #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
5b28118
5e09b7b
faf031a
8c4035d
b8fd02e
5bd6c86
83d1525
f6d9415
d469ab9
94fce44
c429455
92d0c66
6af7675
d1fe777
842ac46
c2b7cb0
4f3d07f
fc97dac
41eab1c
e6bfd36
473f850
a93c099
fd4e191
6951f8c
a5d59f8
8dbeef0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
# The MIT License (MIT) | ||
# | ||
# Copyright (c) 2019 ladyada for Adafruit Industries | ||
mscosti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# | ||
# 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 | ||
mscosti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
* 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) | ||
|
||
sommersoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
NO_SOCK_AVAIL = const(255) | ||
INDEX_HTML = "/index.html" | ||
|
||
|
||
# pylint: disable=unused-argument, redefined-builtin, invalid-name | ||
class server: | ||
mscosti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" 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): | ||
mscosti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
allows for setting a directory of static files that will be auto-served | ||
when that file is GET requested at'/<fileName.extension>' | ||
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" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,7 @@ def set_interface(iface): | |
|
||
SOCK_STREAM = const(1) | ||
AF_INET = const(2) | ||
NO_SOCKET_AVAIL = const(255) | ||
|
||
MAX_PACKET = const(4000) | ||
|
||
|
@@ -59,14 +60,16 @@ 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): | ||
# 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") | ||
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) | ||
# pylint: enable=too-many-arguments | ||
|
||
def connect(self, address, conntype=None): | ||
"""Connect the socket to the 'address' (which can be 32bit packed IP or | ||
|
@@ -90,7 +93,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: | ||
|
@@ -106,7 +109,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: | ||
|
@@ -122,7 +125,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)) | ||
|
@@ -148,6 +151,39 @@ def settimeout(self, value): | |
"""Set the read timeout for sockets, if value is 0 it will block""" | ||
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(): | ||
return True | ||
else: | ||
status = _the_interface.socket_status(self.socknum) | ||
# TODO: why is esp.<ConstantName> not defined? using magic numbers in mean time | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to use the same constants as defined in Any ideas? We obviously don't want the magic numbers here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which line are you referring to here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. all of the numbers in All of those are constants corresponding to ones defined in esp32spi.py https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/blob/master/adafruit_esp32spi/adafruit_esp32spi.py#L101-L111 My initial attempt to use them wasn't working, I'm sure there is just something silly i'm missing with being able to re-use those constants here. |
||
result = status not in (1, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for context, stole this logic from the arduino |
||
0, | ||
5, | ||
6, | ||
10, | ||
2, | ||
3, | ||
7) | ||
if not result: | ||
self.close() | ||
self._socknum = NO_SOCKET_AVAIL | ||
return result | ||
|
||
@property | ||
def socknum(self): | ||
"""The socket number""" | ||
return self._socknum | ||
|
||
def close(self): | ||
"""Close the socket, after reading whatever remains""" | ||
_the_interface.socket_close(self._socknum) | ||
|
Uh oh!
There was an error while loading. Please reload this page.