Minimal, fast, robust HTTP server library for Python/CircuitPython that uses non-blocking concurrent I/O even when asyncio isn't available!
MIT License
Biplane is an HTTP server library for Python/CircuitPython.
Compared to common alternatives such as Ampule, circuitpython-native-wsgiserver, and Adafruit_CircuitPython_HTTPServer, it has several unique features:
async
/await
/asyncio
isn't available!
asyncio
, we expose the entire server as a generator, where each step of the generator is O(1).max_request_line_size
and max_body_bytes
settings.request_timeout_seconds
setting.time
and errno
libraries, both of which are built into Python/CircuitPython (as well as wifi
, mdns
, and socketpool
if using the WiFi helpers).However, compared to those libraries, it intentionally doesn't include some features in order to keep the codebase small:
Install via Pip:
pip install biplane
To install Biplane using CircUp, ensure you have set it up according to the Adafruit CircUp guide. Then:
circup install biplane
For CircuitPython devices that don't support the CIRCUITPY drive used to upload code, you can instead manually upload biplane.py
from this folder to lib/biplane.py
on the board using one of the following methods:
python3 -c 'f=open("biplane.py");code=f.read();print(f"code={repr(code)};open(\"lib/biplane.py\",\"w\").write(code) if len(code)=={len(code)} else print(\"CODE CORRUPTED\")")'
in this folder, and copy the output of that command to the clipboard. This output is CircuitPython code that creates lib/biplane.py
with the correct contents inside.screen
or minicom
); to fix this, configure your terminal to wait 2ms-4ms after sending each character and try again (2ms is usually good enough). Also, make sure that you do this after freshly resetting the board.Lastly, Biplane is part of the CircuitPython Community Bundle, so if you have that installed, then you already have Biplane installed too.
Starts a WiFi network called "test" (password is "some_password") - when connected, you can see a Hello World page at http://app.local/
(tested on an ESP32C3):
import biplane
server = biplane.Server()
@server.route("/", "GET")
def main(query_parameters, headers, body):
return biplane.Response("<b>Hello, world!</b>", content_type="text/html")
for _ in server.circuitpython_start_wifi_ap("test", "some_password", "app"):
pass
Starts a server that displays a Hello World page at http://localhost:8000
, similar to the CircuitPython example above:
import socket
import biplane
server = biplane.Server()
@server.route("/", "POST")
def main(query_parameters, headers, body):
return biplane.Response("<b>Hello, world!</b>", content_type="text/html")
server_socket = socket.socket()
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # allow the server to reuse the address immediately after it's been closed
for _ in server.start(server_socket, listen_on=('127.0.0.1', 8000)):
pass
The usage is almost exactly the same, but we pass in a socket from the Python socket
library instead of from CircuitPython's socketpool
library.
Blinks an LED consistently at ~100Hz while serving HTTP requests, keeping a ~100Hz frequency regardless of how quickly HTTP requests are coming in:
import time
import board
import digitalio
import biplane
server = biplane.Server()
@server.route("/", "GET")
def main(query_parameters, headers, body):
return biplane.Response("<b>Hello, world!</b>", content_type="text/html")
def asyncio_sleep(seconds): # minimal implementation of asyncio.sleep() as a generator
start_time = time.monotonic()
while time.monotonic() - start_time < seconds:
yield
def blink_builtin_led():
with digitalio.DigitalInOut(pin) as led:
led.switch_to_output(value=False)
while True:
led.value = not led.value
yield from asyncio_sleep(0.01)
for _ in zip(blink_builtin_led(), server.circuitpython_start_wifi_ap("test", "some_password")): # run through both generators at the same time using zip()
pass
With other HTTP servers, blinking the LED while serving requests would either be impossible, or would become inconsistent when many HTTP requests are coming in.
Note that CircuitPython's GC pauses may cause occasional longer pauses - to mitigate this, run import gc; gc.collect()
at regular, predictable intervals, so that the GC never has to be invoked at unpredictable times.
Many CircuitPython implementations, especially those for boards with less RAM/flash, don't include the asyncio
library. However, if asyncio
is available, Biplane works well with it as well:
import time
import board
import digitalio
import biplane
server = biplane.Server()
@server.route("/", "GET")
def main(query_parameters, headers, body):
return biplane.Response("<b>Hello, world!</b>", content_type="text/html")
async def run_server():
for _ in server.circuitpython_start_wifi_ap("test", "some_password")
await asyncio.sleep(0) # let other tasks run
async def blink_builtin_led():
with digitalio.DigitalInOut(pin) as led:
led.switch_to_output(value=False)
while True:
led.value = not led.value
await asyncio.sleep(0.01)
asyncio.run(asyncio.gather(blink_builtin_led(), run_server())) # run both coroutines at the same time
Essentially, we just need to loop through the generator as usual while calling await asyncio.sleep(0)
each iteration to let other tasks run.
All of the application code lives in biplane.py
. Run tests using python3 tests/test_basic.py
.
Copyright 2023 Anthony Zhang (Uberi).
The source code is available online at GitHub.
This program is made available under the MIT license. See LICENSE.txt
in the project's root directory for more information.