diff --git a/README.rst b/README.rst index fe888f0..bb54843 100644 --- a/README.rst +++ b/README.rst @@ -21,12 +21,14 @@ Introduction :target: https://github.com/psf/black :alt: Code Style: Black -Simple HTTP Server for CircuitPython. +HTTP Server for CircuitPython. - Supports `socketpool` or `socket` as a source of sockets; can be used in CPython. - HTTP 1.1. - Serves files from a designated root. -- Simple routing available to serve computed results. +- Routing for serving computed responses from handler. +- Gives access to request headers, query parameters, body and client's address, the one from which the request came. +- Supports chunked transfer encoding. Dependencies diff --git a/adafruit_httpserver/headers.py b/adafruit_httpserver/headers.py new file mode 100644 index 0000000..cf9ea20 --- /dev/null +++ b/adafruit_httpserver/headers.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_httpserver.headers.HTTPHeaders` +==================================================== +* Author(s): MichaƂ Pokusa +""" + +try: + from typing import Dict, Tuple +except ImportError: + pass + + +class HTTPHeaders: + """ + A dict-like class for storing HTTP headers. + + Allows access to headers using **case insensitive** names. + + Does **not** implement all dict methods. + + Examples:: + + headers = HTTPHeaders({"Content-Type": "text/html", "Content-Length": "1024"}) + + len(headers) + # 2 + + headers.setdefault("Access-Control-Allow-Origin", "*") + headers["Access-Control-Allow-Origin"] + # '*' + + headers["Content-Length"] + # '1024' + + headers["content-type"] + # 'text/html' + + headers["User-Agent"] + # KeyError: User-Agent + + "CONTENT-TYPE" in headers + # True + """ + + _storage: Dict[str, Tuple[str, str]] + + def __init__(self, headers: Dict[str, str] = None) -> None: + + headers = headers or {} + + self._storage = {key.lower(): [key, value] for key, value in headers.items()} + + def get(self, name: str, default: str = None): + """Returns the value for the given header name, or default if not found.""" + return self._storage.get(name.lower(), [None, default])[1] + + def setdefault(self, name: str, default: str = None): + """Sets the value for the given header name if it does not exist.""" + return self._storage.setdefault(name.lower(), [name, default])[1] + + def items(self): + """Returns a list of (name, value) tuples.""" + return dict(self._storage.values()).items() + + def keys(self): + """Returns a list of header names.""" + return dict(self._storage.values()).keys() + + def values(self): + """Returns a list of header values.""" + return dict(self._storage.values()).values() + + def update(self, headers: Dict[str, str]): + """Updates the headers with the given dict.""" + return self._storage.update( + {key.lower(): [key, value] for key, value in headers.items()} + ) + + def copy(self): + """Returns a copy of the headers.""" + return HTTPHeaders(dict(self._storage.values())) + + def __getitem__(self, name: str): + return self._storage[name.lower()][1] + + def __setitem__(self, name: str, value: str): + self._storage[name.lower()] = [name, value] + + def __delitem__(self, name: str): + del self._storage[name.lower()] + + def __iter__(self): + return iter(dict(self._storage.values())) + + def __len__(self): + return len(self._storage) + + def __contains__(self, key: str): + return key.lower() in self._storage.keys() + + def __repr__(self): + return f"{self.__class__.__name__}({dict(self._storage.values())})" diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index 22758a8..9ee5fc0 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -8,10 +8,14 @@ """ try: - from typing import Dict, Tuple + from typing import Dict, Tuple, Union + from socket import socket + from socketpool import SocketPool except ImportError: pass +from .headers import HTTPHeaders + class HTTPRequest: """ @@ -19,6 +23,21 @@ class HTTPRequest: It is passed as first argument to route handlers. """ + connection: Union["SocketPool.Socket", "socket.socket"] + """ + Socket object usable to send and receive data on the connection. + """ + + client_address: Tuple[str, int] + """ + Address and port bound to the socket on the other end of the connection. + + Example:: + + request.client_address + # ('192.168.137.1', 40684) + """ + method: str """Request method e.g. "GET" or "POST".""" @@ -39,26 +58,26 @@ class HTTPRequest: http_version: str """HTTP version, e.g. "HTTP/1.1".""" - headers: Dict[str, str] + headers: HTTPHeaders """ - Headers from the request as `dict`. - - Values should be accessed using **lower case header names**. - - Example:: - - request.headers - # {'connection': 'keep-alive', 'content-length': '64' ...} - request.headers["content-length"] - # '64' - request.headers["Content-Length"] - # KeyError: 'Content-Length' + Headers from the request. """ raw_request: bytes - """Raw bytes passed to the constructor.""" + """ + Raw 'bytes' passed to the constructor and body 'bytes' received later. + + Should **not** be modified directly. + """ - def __init__(self, raw_request: bytes = None) -> None: + def __init__( + self, + connection: Union["SocketPool.Socket", "socket.socket"], + client_address: Tuple[str, int], + raw_request: bytes = None, + ) -> None: + self.connection = connection + self.client_address = client_address self.raw_request = raw_request if raw_request is None: @@ -120,12 +139,14 @@ def _parse_start_line(header_bytes: bytes) -> Tuple[str, str, Dict[str, str], st return method, path, query_params, http_version @staticmethod - def _parse_headers(header_bytes: bytes) -> Dict[str, str]: + def _parse_headers(header_bytes: bytes) -> HTTPHeaders: """Parse HTTP headers from raw request.""" header_lines = header_bytes.decode("utf8").splitlines()[1:] - return { - name.lower(): value - for header_line in header_lines - for name, value in [header_line.split(": ", 1)] - } + return HTTPHeaders( + { + name: value + for header_line in header_lines + for name, value in [header_line.split(": ", 1)] + } + ) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 5809bb8..dbd0a03 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -14,184 +14,216 @@ except ImportError: pass -from errno import EAGAIN, ECONNRESET import os - +from errno import EAGAIN, ECONNRESET from .mime_type import MIMEType +from .request import HTTPRequest from .status import HTTPStatus, CommonHTTPStatus +from .headers import HTTPHeaders class HTTPResponse: - """Details of an HTTP response. Use in `HTTPServer.route` decorator functions.""" + """ + Response to a given `HTTPRequest`. Use in `HTTPServer.route` decorator functions. + + Example:: + + # Response with 'Content-Length' header + @server.route(path, method) + def route_func(request): + + response = HTTPResponse(request) + response.send("Some content", content_type="text/plain") + + # or + + response = HTTPResponse(request) + with response: + response.send(body='Some content', content_type="text/plain") + + # or + + with HTTPResponse(request) as response: + response.send("Some content", content_type="text/plain") + + # Response with 'Transfer-Encoding: chunked' header + @server.route(path, method) + def route_func(request): + + response = HTTPResponse(request, content_type="text/plain", chunked=True) + with response: + response.send_chunk("Some content") + response.send_chunk("Some more content") + + # or + + with HTTPResponse(request, content_type="text/plain", chunked=True) as response: + response.send_chunk("Some content") + response.send_chunk("Some more content") + """ + + request: HTTPRequest + """The request that this is a response to.""" http_version: str status: HTTPStatus - headers: Dict[str, str] + headers: HTTPHeaders content_type: str - cache: Optional[int] - filename: Optional[str] - root_path: str + """ + Defaults to ``text/plain`` if not set. - body: str + Can be explicitly provided in the constructor, in `send()` or + implicitly determined from filename in `send_file()`. + + Common MIME types are defined in `adafruit_httpserver.mime_type.MIMEType`. + """ def __init__( # pylint: disable=too-many-arguments self, + request: HTTPRequest, status: Union[HTTPStatus, Tuple[int, str]] = CommonHTTPStatus.OK_200, - body: str = "", - headers: Dict[str, str] = None, - content_type: str = MIMEType.TYPE_TXT, - cache: Optional[int] = 0, - filename: Optional[str] = None, - root_path: str = "", + headers: Union[HTTPHeaders, Dict[str, str]] = None, + content_type: str = None, http_version: str = "HTTP/1.1", + chunked: bool = False, ) -> None: """ Creates an HTTP response. - Returns ``body`` if ``filename`` is ``None``, otherwise returns contents of ``filename``. + Sets `status`, ``headers`` and `http_version` + and optionally default ``content_type``. + + To send the response, call `send` or `send_file`. + For chunked response use + ``with HTTPRequest(request, content_type=..., chunked=True) as r:`` and `send_chunk`. """ + self.request = request self.status = status if isinstance(status, HTTPStatus) else HTTPStatus(*status) - self.body = body - self.headers = headers or {} + self.headers = ( + headers.copy() if isinstance(headers, HTTPHeaders) else HTTPHeaders(headers) + ) self.content_type = content_type - self.cache = cache - self.filename = filename - self.root_path = root_path self.http_version = http_version + self.chunked = chunked + self._response_already_sent = False - @staticmethod - def _construct_response_bytes( # pylint: disable=too-many-arguments - http_version: str = "HTTP/1.1", - status: HTTPStatus = CommonHTTPStatus.OK_200, - content_type: str = MIMEType.TYPE_TXT, - content_length: Union[int, None] = None, - cache: int = 0, - headers: Dict[str, str] = None, - body: str = "", - chunked: bool = False, - ) -> bytes: - """Constructs the response bytes from the given parameters.""" - - response = f"{http_version} {status.code} {status.text}\r\n" + def _send_headers( + self, + content_length: Optional[int] = None, + content_type: str = None, + ) -> None: + """ + Sends headers. + Implicitly called by `send` and `send_file` and in + ``with HTTPResponse(request, chunked=True) as response:`` context manager. + """ + headers = self.headers.copy() - # Make a copy of the headers so that we don't modify the incoming dict - response_headers = {} if headers is None else headers.copy() + response_message_header = ( + f"{self.http_version} {self.status.code} {self.status.text}\r\n" + ) - response_headers.setdefault("Content-Type", content_type) - response_headers.setdefault("Connection", "close") - if chunked: - response_headers.setdefault("Transfer-Encoding", "chunked") + headers.setdefault( + "Content-Type", content_type or self.content_type or MIMEType.TYPE_TXT + ) + headers.setdefault("Connection", "close") + if self.chunked: + headers.setdefault("Transfer-Encoding", "chunked") else: - response_headers.setdefault("Content-Length", content_length or len(body)) - - for header, value in response_headers.items(): - response += f"{header}: {value}\r\n" + headers.setdefault("Content-Length", content_length) - response += f"Cache-Control: max-age={cache}\r\n" + for header, value in headers.items(): + response_message_header += f"{header}: {value}\r\n" + response_message_header += "\r\n" - response += f"\r\n{body}" - - return response.encode("utf-8") + self._send_bytes( + self.request.connection, response_message_header.encode("utf-8") + ) - def send(self, conn: Union["SocketPool.Socket", "socket.socket"]) -> None: + def send( + self, + body: str = "", + content_type: str = None, + ) -> None: """ - Send the constructed response over the given socket. + Sends response with content built from ``body``. + Implicitly calls ``_send_headers`` before sending the body. + + Should be called **only once** per response. """ + if self._response_already_sent: + raise RuntimeError("Response was already sent") - if self.filename is not None: - try: - file_length = os.stat(self.root_path + self.filename)[6] - self._send_file_response( - conn, - filename=self.filename, - root_path=self.root_path, - file_length=file_length, - headers=self.headers, - ) - except OSError: - self._send_response( - conn, - status=CommonHTTPStatus.NOT_FOUND_404, - content_type=MIMEType.TYPE_TXT, - body=f"{CommonHTTPStatus.NOT_FOUND_404} {self.filename}", - ) - else: - self._send_response( - conn, - status=self.status, - content_type=self.content_type, - headers=self.headers, - body=self.body, - ) - - def send_chunk_headers( - self, conn: Union["SocketPool.Socket", "socket.socket"] - ) -> None: - """Send Headers for a chunked response over the given socket.""" - self._send_bytes( - conn, - self._construct_response_bytes( - status=self.status, - content_type=self.content_type, - chunked=True, - cache=self.cache, - body="", - ), + encoded_response_message_body = body.encode("utf-8") + + self._send_headers( + content_type=content_type or self.content_type, + content_length=len(encoded_response_message_body), ) + self._send_bytes(self.request.connection, encoded_response_message_body) + self._response_already_sent = True - def send_body_chunk( - self, conn: Union["SocketPool.Socket", "socket.socket"], chunk: str + def send_file( + self, + filename: str = "index.html", + root_path: str = "./", ) -> None: - """Send chunk of data to the given socket. Send an empty("") chunk to finish the session. + """ + Send response with content of ``filename`` located in ``root_path``. + Implicitly calls ``_send_headers`` before sending the file content. + + Should be called **only once** per response. + """ + if self._response_already_sent: + raise RuntimeError("Response was already sent") + + if not root_path.endswith("/"): + root_path += "/" + try: + file_length = os.stat(root_path + filename)[6] + except OSError: + # If the file doesn't exist, return 404. + HTTPResponse(self.request, status=CommonHTTPStatus.NOT_FOUND_404).send() + return + + self._send_headers( + content_type=MIMEType.from_file_name(filename), + content_length=file_length, + ) + + with open(root_path + filename, "rb") as file: + while bytes_read := file.read(2048): + self._send_bytes(self.request.connection, bytes_read) + self._response_already_sent = True + + def send_chunk(self, chunk: str = "") -> None: + """ + Sends chunk of response. + + Should be used **only** inside + ``with HTTPResponse(request, chunked=True) as response:`` context manager. - :param Union["SocketPool.Socket", "socket.socket"] conn: Current connection. :param str chunk: String data to be sent. """ - size = "%X\r\n".encode() % len(chunk) - self._send_bytes(conn, size) - self._send_bytes(conn, chunk.encode() + b"\r\n") + hex_length = hex(len(chunk))[2:] # removing 0x - def _send_response( # pylint: disable=too-many-arguments - self, - conn: Union["SocketPool.Socket", "socket.socket"], - status: HTTPStatus, - content_type: str, - body: str, - headers: Dict[str, str] = None, - ): self._send_bytes( - conn, - self._construct_response_bytes( - status=status, - content_type=content_type, - cache=self.cache, - headers=headers, - body=body, - ), + self.request.connection, f"{hex_length}\r\n{chunk}\r\n".encode("utf-8") ) - def _send_file_response( # pylint: disable=too-many-arguments - self, - conn: Union["SocketPool.Socket", "socket.socket"], - filename: str, - root_path: str, - file_length: int, - headers: Dict[str, str] = None, - ): - self._send_bytes( - conn, - self._construct_response_bytes( - status=self.status, - content_type=MIMEType.from_file_name(filename), - content_length=file_length, - cache=self.cache, - headers=headers, - ), - ) - with open(root_path + filename, "rb") as file: - while bytes_read := file.read(2048): - self._send_bytes(conn, bytes_read) + def __enter__(self): + if self.chunked: + self._send_headers() + return self + + def __exit__(self, exception_type, exception_value, exception_traceback): + if exception_type is not None: + return False + + if self.chunked: + self.send_chunk("") + return True @staticmethod def _send_bytes( diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index db953d6..0a64541 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -40,30 +40,17 @@ def __init__(self, socket_source: Protocol) -> None: self.root_path = "/" def route(self, path: str, method: HTTPMethod = HTTPMethod.GET): - """Decorator used to add a route. + """ + Decorator used to add a route. :param str path: filename path :param HTTPMethod method: HTTP method: HTTPMethod.GET, HTTPMethod.POST, etc. Example:: - @server.route(path, method) + @server.route("/example", HTTPMethod.GET) def route_func(request): - raw_text = request.raw_request.decode("utf8") - print("Received a request of length", len(raw_text), "bytes") - return HTTPResponse(body="hello world") - - - @server.route(path, method) - def route_func(request, conn): - raw_text = request.raw_request.decode("utf8") - print("Received a request of length", len(raw_text), "bytes") - res = HTTPResponse(content_type="text/html") - res.send_chunk_headers(conn) - res.send_body_chunk(conn, "Some content") - res.send_body_chunk(conn, "Some more content") - res.send_body_chunk(conn, "") # Send empty packet to finish chunked stream - return None # Return None, so server knows that nothing else needs to be sent. + ... """ def route_decorator(func: Callable) -> Callable: @@ -77,7 +64,7 @@ def serve_forever(self, host: str, port: int = 80, root_path: str = "") -> None: :param str host: host name or IP address :param int port: port - :param str root: root directory to serve files from + :param str root_path: root directory to serve files from """ self.start(host, port, root_path) @@ -94,7 +81,7 @@ def start(self, host: str, port: int = 80, root_path: str = "") -> None: :param str host: host name or IP address :param int port: port - :param str root: root directory to serve files from + :param str root_path: root directory to serve files from """ self.root_path = root_path @@ -146,7 +133,7 @@ def poll(self): the application callable will be invoked. """ try: - conn, _ = self._sock.accept() + conn, client_address = self._sock.accept() with conn: conn.settimeout(self._timeout) @@ -157,9 +144,9 @@ def poll(self): if not header_bytes: return - request = HTTPRequest(header_bytes) + request = HTTPRequest(conn, client_address, header_bytes) - content_length = int(request.headers.get("content-length", 0)) + content_length = int(request.headers.get("Content-Length", 0)) received_body_bytes = request.body # Receiving remaining body bytes @@ -173,25 +160,24 @@ def poll(self): # If a handler for route exists and is callable, call it. if handler is not None and callable(handler): - # Need to pass connection for chunked encoding to work. - try: - response = handler(request, conn) - except TypeError: - response = handler(request) - if response is None: - return + output = handler(request) + # TODO: Remove this deprecation error in future + if isinstance(output, HTTPResponse): + raise RuntimeError( + "Returning an HTTPResponse from a route handler is deprecated." + ) # If no handler exists and request method is GET, try to serve a file. - elif request.method == HTTPMethod.GET: - response = HTTPResponse( - filename=request.path, root_path=self.root_path, cache=604800 + elif handler is None and request.method == HTTPMethod.GET: + HTTPResponse(request).send_file( + filename=request.path, + root_path=self.root_path, ) - - # If no handler exists and request method is not GET, return 400 Bad Request. else: - response = HTTPResponse(status=CommonHTTPStatus.BAD_REQUEST_400) + HTTPResponse( + request, status=CommonHTTPStatus.BAD_REQUEST_400 + ).send() - response.send(conn) except OSError as ex: # handle EAGAIN and ECONNRESET if ex.errno == EAGAIN: @@ -228,9 +214,10 @@ def request_buffer_size(self, value: int) -> None: def socket_timeout(self) -> int: """ Timeout after which the socket will stop waiting for more incoming data. - When exceeded, raises `OSError` with `errno.ETIMEDOUT`. - Default timeout is 0, which means socket is in non-blocking mode. + Must be set to positive integer or float. Default is 1 second. + + When exceeded, raises `OSError` with `errno.ETIMEDOUT`. Example:: diff --git a/docs/api.rst b/docs/api.rst index cf4ba22..4615507 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,10 +7,7 @@ .. automodule:: adafruit_httpserver :members: -.. automodule:: adafruit_httpserver.methods - :members: - -.. automodule:: adafruit_httpserver.mime_type +.. automodule:: adafruit_httpserver.server :members: .. automodule:: adafruit_httpserver.request @@ -19,8 +16,14 @@ .. automodule:: adafruit_httpserver.response :members: -.. automodule:: adafruit_httpserver.server +.. automodule:: adafruit_httpserver.headers :members: .. automodule:: adafruit_httpserver.status :members: + +.. automodule:: adafruit_httpserver.methods + :members: + +.. automodule:: adafruit_httpserver.mime_type + :members: diff --git a/docs/examples.rst b/docs/examples.rst index 0100e37..6080b32 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -1,27 +1,64 @@ -Simple test ------------- +Simple file serving +------------------- -Ensure your device works with this simple test. +Serving the content of index.html from the filesystem. -.. literalinclude:: ../examples/httpserver_simpletest.py - :caption: examples/httpserver_simpletest.py +.. literalinclude:: ../examples/httpserver_simple_serve.py + :caption: examples/httpserver_simple_serve.py :linenos: -Temperature test --------------------- +If you want your code to do more than just serve web pages, +use the ``.start()``/``.poll()`` methods as shown in this example. -Send the microcontroller temperature back to the browser with this simple test. +Between calling ``.poll()`` you can do something useful, +for example read a sensor and capture an average or +a running total of the last 10 samples. -.. literalinclude:: ../examples/httpserver_temperature.py - :caption: examples/httpserver_temperature.py +.. literalinclude:: ../examples/httpserver_simple_poll.py + :caption: examples/httpserver_simple_poll.py :linenos: -Simple polling test -------------------- +Server with MDNS +---------------- + +It is possible to use the MDNS protocol to make the server +accessible via a hostname in addition to an IP address. + +In this example, the server is accessible via ``http://custom-mdns-hostname/`` and ``http://custom-mdns-hostname.local/``. + +.. literalinclude:: ../examples/httpserver_cpu_information.py + :caption: examples/httpserver_cpu_information.py + :linenos: + +Change NeoPixel color +--------------------- If you want your code to do more than just serve web pages, use the start/poll methods as shown in this example. -.. literalinclude:: ../examples/httpserver_simplepolling.py - :caption: examples/httpserver_simplepolling.py +For example by going to ``/change-neopixel-color?r=255&g=0&b=0`` you can change the color of the NeoPixel to red. +Tested on ESP32-S2 Feather. + +.. literalinclude:: ../examples/httpserver_neopixel.py + :caption: examples/httpserver_neopixel.py + :linenos: + +Get CPU information +--------------------- + +You can return data from sensors or any computed value as JSON. +That makes it easy to use the data in other applications. + +.. literalinclude:: ../examples/httpserver_cpu_information.py + :caption: examples/httpserver_cpu_information.py + :linenos: + +Chunked response +--------------------- + +Library supports chunked responses. This is useful for streaming data. +To use it, you need to set the ``chunked=True`` when creating a ``HTTPResponse`` object. + +.. literalinclude:: ../examples/httpserver_chunked.py + :caption: examples/httpserver_chunked.py :linenos: diff --git a/examples/httpserver_chunked.py b/examples/httpserver_chunked.py new file mode 100644 index 0000000..ae519ec --- /dev/null +++ b/examples/httpserver_chunked.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +import secrets # pylint: disable=no-name-in-module + +import socketpool +import wifi + +from adafruit_httpserver.request import HTTPRequest +from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member + +print("Connecting to", ssid) +wifi.radio.connect(ssid, password) +print("Connected to", ssid) + +pool = socketpool.SocketPool(wifi.radio) +server = HTTPServer(pool) + + +@server.route("/chunked") +def chunked(request: HTTPRequest): + """ + Return the response with ``Transfer-Encoding: chunked``. + """ + + with HTTPResponse(request, chunked=True) as response: + response.send_chunk("Adaf") + response.send_chunk("ruit") + response.send_chunk(" Indus") + response.send_chunk("tr") + response.send_chunk("ies") + + +print(f"Listening on http://{wifi.radio.ipv4_address}:80") +server.serve_forever(str(wifi.radio.ipv4_address)) diff --git a/examples/httpserver_cpu_information.py b/examples/httpserver_cpu_information.py new file mode 100644 index 0000000..cf3d13b --- /dev/null +++ b/examples/httpserver_cpu_information.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +import secrets # pylint: disable=no-name-in-module + +import json +import microcontroller +import socketpool +import wifi + +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.request import HTTPRequest +from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member + +print("Connecting to", ssid) +wifi.radio.connect(ssid, password) +print("Connected to", ssid) + +pool = socketpool.SocketPool(wifi.radio) +server = HTTPServer(pool) + + +@server.route("/cpu-information") +def cpu_information_handler(request: HTTPRequest): + """ + Return the current CPU temperature, frequency, and voltage as JSON. + """ + + data = { + "temperature": microcontroller.cpu.temperature, + "frequency": microcontroller.cpu.frequency, + "voltage": microcontroller.cpu.voltage, + } + + with HTTPResponse(request, content_type=MIMEType.TYPE_JSON) as response: + response.send(json.dumps(data)) + + +print(f"Listening on http://{wifi.radio.ipv4_address}:80") +server.serve_forever(str(wifi.radio.ipv4_address)) diff --git a/examples/httpserver_mdns.py b/examples/httpserver_mdns.py new file mode 100644 index 0000000..d2228c9 --- /dev/null +++ b/examples/httpserver_mdns.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +import secrets # pylint: disable=no-name-in-module + +import mdns +import socketpool +import wifi + +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.request import HTTPRequest +from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member + +print("Connecting to", ssid) +wifi.radio.connect(ssid, password) +print("Connected to", ssid) + +mdns_server = mdns.Server(wifi.radio) +mdns_server.hostname = "custom-mdns-hostname" +mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=80) + +pool = socketpool.SocketPool(wifi.radio) +server = HTTPServer(pool) + + +@server.route("/") +def base(request: HTTPRequest): + """ + Serve the default index.html file. + """ + with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response: + response.send_file("index.html") + + +print(f"Listening on http://{wifi.radio.ipv4_address}:80") +server.serve_forever(str(wifi.radio.ipv4_address)) diff --git a/examples/httpserver_neopixel.py b/examples/httpserver_neopixel.py new file mode 100644 index 0000000..ab7dabd --- /dev/null +++ b/examples/httpserver_neopixel.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +import secrets # pylint: disable=no-name-in-module + +import board +import neopixel +import socketpool +import wifi + +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.request import HTTPRequest +from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member + +print("Connecting to", ssid) +wifi.radio.connect(ssid, password) +print("Connected to", ssid) + +pool = socketpool.SocketPool(wifi.radio) +server = HTTPServer(pool) + +pixel = neopixel.NeoPixel(board.NEOPIXEL, 1) + + +@server.route("/change-neopixel-color") +def change_neopixel_color_handler(request: HTTPRequest): + """ + Changes the color of the built-in NeoPixel. + """ + r = request.query_params.get("r") + g = request.query_params.get("g") + b = request.query_params.get("b") + + pixel.fill((int(r or 0), int(g or 0), int(b or 0))) + + with HTTPResponse(request, content_type=MIMEType.TYPE_TXT) as response: + response.send(f"Changed NeoPixel to color ({r}, {g}, {b})") + + +print(f"Listening on http://{wifi.radio.ipv4_address}:80") +server.serve_forever(str(wifi.radio.ipv4_address)) diff --git a/examples/httpserver_simplepolling.py b/examples/httpserver_simple_poll.py similarity index 51% rename from examples/httpserver_simplepolling.py rename to examples/httpserver_simple_poll.py index d924a5c..db876c4 100644 --- a/examples/httpserver_simplepolling.py +++ b/examples/httpserver_simple_poll.py @@ -2,40 +2,49 @@ # # SPDX-License-Identifier: Unlicense -from secrets import secrets # pylint: disable=no-name-in-module +import secrets # pylint: disable=no-name-in-module import socketpool import wifi -from adafruit_httpserver.server import HTTPServer +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.request import HTTPRequest from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member -ssid = secrets["ssid"] print("Connecting to", ssid) -wifi.radio.connect(ssid, secrets["password"]) +wifi.radio.connect(ssid, password) print("Connected to", ssid) -print(f"Listening on http://{wifi.radio.ipv4_address}:80") pool = socketpool.SocketPool(wifi.radio) server = HTTPServer(pool) @server.route("/") -def base(request): # pylint: disable=unused-argument - """Default reponse is /index.html""" - return HTTPResponse(filename="/index.html") +def base(request: HTTPRequest): + """ + Serve the default index.html file. + """ + with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response: + response.send_file("index.html") + +print(f"Listening on http://{wifi.radio.ipv4_address}:80") -# startup the server +# Start the server. server.start(str(wifi.radio.ipv4_address)) while True: try: - # do something useful in this section, + # Do something useful in this section, # for example read a sensor and capture an average, # or a running total of the last 10 samples - # process any waiting requests + # Process any waiting requests server.poll() - except OSError: + except OSError as error: + print(error) continue diff --git a/examples/httpserver_simpletest.py b/examples/httpserver_simple_serve.py similarity index 51% rename from examples/httpserver_simpletest.py rename to examples/httpserver_simple_serve.py index e1be4b0..632c234 100644 --- a/examples/httpserver_simpletest.py +++ b/examples/httpserver_simple_serve.py @@ -2,29 +2,35 @@ # # SPDX-License-Identifier: Unlicense -from secrets import secrets # pylint: disable=no-name-in-module +import secrets # pylint: disable=no-name-in-module import socketpool import wifi -from adafruit_httpserver.server import HTTPServer +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.request import HTTPRequest from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member -ssid = secrets["ssid"] print("Connecting to", ssid) -wifi.radio.connect(ssid, secrets["password"]) +wifi.radio.connect(ssid, password) print("Connected to", ssid) -print(f"Listening on http://{wifi.radio.ipv4_address}:80") pool = socketpool.SocketPool(wifi.radio) server = HTTPServer(pool) @server.route("/") -def base(request): # pylint: disable=unused-argument - """Default reponse is /index.html""" - return HTTPResponse(filename="/index.html") +def base(request: HTTPRequest): + """ + Serve the default index.html file. + """ + with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response: + response.send_file("index.html") -# Never returns +print(f"Listening on http://{wifi.radio.ipv4_address}:80") server.serve_forever(str(wifi.radio.ipv4_address)) diff --git a/examples/httpserver_temperature.py b/examples/httpserver_temperature.py deleted file mode 100644 index 94aa541..0000000 --- a/examples/httpserver_temperature.py +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries -# -# SPDX-License-Identifier: Unlicense - -from secrets import secrets # pylint: disable=no-name-in-module - -import microcontroller -import socketpool -import wifi - -from adafruit_httpserver.server import HTTPServer -from adafruit_httpserver.response import HTTPResponse - -ssid = secrets["ssid"] -print("Connecting to", ssid) -wifi.radio.connect(ssid, secrets["password"]) -print("Connected to", ssid) -print(f"Listening on http://{wifi.radio.ipv4_address}:80") - -pool = socketpool.SocketPool(wifi.radio) -server = HTTPServer(pool) - - -@server.route("/temperature") -def base(request): # pylint: disable=unused-argument - """Return the current temperature""" - # pylint: disable=no-member - return HTTPResponse(body=f"{str(microcontroller.cpu.temperature)}") - - -# Never returns -server.serve_forever(str(wifi.radio.ipv4_address))