10. š§Ŗ Labs - Experimental APIs ā ļøļ
Note
The pi-top Python SDK Labs are a set of classes which are being provided as experiments in exciting new ways to interact with your device.
Warning
Everything in Labs is subject to change - so use at your own risk!
10.1. Webļ
This Web API has been created with the goal of giving users the ability to easily create a web application that runs directly on the pi-top that can easily offer a dynamic, interactive interface for controlling the pi-top.
The Web API provides a selection of web server interfaces, as well as a selection of prebuilt features known as Blueprints to be used with these servers.
For examples of how to use this, check out the labs examples directory on GitHub.
10.1.1. Serversļ
For simple static web apps or ground-up customisation, use WebServer.
If you would like a ābatteries includedā WebServer that makes it easy to interact with your pi-top, use WebController.
For a quick way to control your pi-top [4] Robotics Kit, use RoverWebController, which offers a preconfigured but customisable WebController for rover-style robots.
10.1.1.1. WebServerļ
The WebServer class is used to create a zero-config server that can:
serve static files and templates
handle requests
handle WebSocket connections
WebServer is a preconfigured gevent WSGIServer, due to this it can be started and stopped just like a gevent BaseServer:
from pitop.labs import WebServer
server = WebServer()
# start server in the background
server.start()
# stop server that has been started in the background
server.stop()
# start server and wait until interrupted
server.serve_forever()
WebServer serves static files and templates found in the working directory
automatically. The entrypoint file is always index.html
. All html files
found are considered to be Jinja templates, this means that if you have a
file layout.html
in the same directory as your WebServer:
<html>
<head>
<title>My Web App</title>
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
It is possible to use it as template for other html files. For example
index.html
can extend layout.html
:
{% extends 'layout.html' %}
{% block body %}
<h1>My Custom Body</h1>
{% endblock %}
To add routes you can use the underlying Flask appās route decorator:
from pitop.labs import WebServer
server = WebServer()
@server.app.route('/ping')
def ping():
return 'pong'
server.serve_forever()
WebSocket routes can be added by using the route decorator provided by Flask Sockets:
from pitop.labs import WebServer
server = WebServer()
@server.sockets.route('/ws')
def ws(socket):
while not socket.closed:
message = socket.receive()
socket.send(message)
server.serve_forever()
The server port
defaults to 8070 but can be customised:
from pitop.labs import WebServer
server = WebServer(port=8071)
It is also possible to customise the Flask app by passing your own into the
app
keyword argument:
from pitop.labs import WebServer
from flask import Flask
server = WebServer(app=Flask(__name__))
WebServer is fully compatible with Flask blueprints, which can be passed to
the blueprints
keyword argument:
from pitop.labs import WebServer
from flask import Blueprint
WebServer(blueprints=[
Blueprint('custom', __name__)
])
We provide a number of premade blueprints:
By default WebServer uses the BaseBlueprint
Warning
When using WebServer in a multithreaded project you must use gevent threading. This is because using Python standard library threading while using a gevent server can result in unexpected behaviour, or may not work at all. See the dashboard example for a basic idea of how gevent threading can be used.
10.1.1.2. WebControllerļ
The WebController class is subclass of WebServer that uses the ControllerBlueprint. It exists as a convenience class so that blueprints are not required to be able to build simple web controllers.
from pitop import Camera
from pitop.labs import WebController
camera = Camera()
def on_dinner_change(data):
print(f'dinner is now {data}')
server = WebController(
get_frame=camera.get_frame,
message_handlers={'dinner_changed': on_dinner_change}
)
server.serve_forever()
See the ControllerBlueprint reference for more detail.
10.1.1.3. RoverWebControllerļ
The RoverWebController class is subclass of WebServer that uses the RoverControllerBlueprint. It exists as a convenience class so that blueprints are not required to build simple rover web controllers.
from pitop import Pitop, Camera, DriveController, PanTiltController
from pitop.labs import RoverWebController
rover = Pitop()
rover.add_component(Camera())
rover.add_component(DriveController())
rover.add_component(PanTiltController())
server = RoverWebController(
get_frame=rover.camera.get_frame,
drive=rover.drive,
pan_tilt=rover.pan_tilt
)
server.serve_forever()
See the RoverControllerBlueprint reference for more detail.
10.1.2. Blueprintsļ
10.1.2.1. BaseBlueprintļ
BaseBlueprint provides a layout and styles that are the base of the
templates found in other blueprints. It adds a base.html
template which
has the following structure:
<html>
<head>
<title>{% block title %}{% endblock %}</title>
{% block head %}
<link rel="stylesheet" href="/base/index.css"></link>
{% endblock %}
</head>
<body>
{% block body %}
<header> {% block header %}{% endblock %} </header>
<main> {% block main %}{% endblock %} </main>
<footer> {% block footer %}{% endblock %} </footer>
{% endblock %}
</body>
</html>
The base.html
adds some basic styles and variables to the page by
linking the index.css
static file.
:root {
--background-color: #00B2A2
}
body {
background-color: var(--background-color);
margin: 0;
padding: 0;
}
Adding the BaseBlueprint to a WebServer is done as follows:
from pitop.labs import WebServer, BaseBlueprint
server = WebServer(blueprints=[
BaseBlueprint()
])
server.serve_forever()
Note: WebServer uses BaseBlueprint by default, so the above is only necessary if you are using BaseBlueprint with other blueprints.
Then you are able to extend the base.html
in your other html files:
{% extends 'base.html' %}
{% block title %}Custom Page{% endblock %}
{% block head %}
<!-- call super() to add index.css -->
{{ super() }}
<link rel="styles" href="custom-styles.css"></link>
{% endblock %}
{% block header %}
<img src="logo.png"></img>
{% endblock %}
{% block main %}
<section>Section One</section>
<section>Section Two</section>
{% endblock %}
{% block footer %}
Contact Info: 123456789
{% endblock %}
If you want to use the static files provided without extending the
base.html
template you can do so by adding them to the page yourself:
<html>
<head>
<link rel="stylesheet" href="/base/index.css"></link>
</head>
<body>
</body>
</html>
10.1.2.2. WebComponentsBlueprintļ
WebComponentsBlueprint provides a set of Web Components for adding complex elements to the page.
Adding the WebComponentsBlueprint to a WebServer is done as follows:
from pitop.labs import WebServer, WebComponentsBlueprint
server = WebServer(blueprints=[
WebComponentsBlueprint()
])
server.serve_forever()
To add the components to the page WebComponentsBlueprint provides a setup template
setup-components.html
that can be included in the head
of your
page
<head>
{% include "setup-webcomponents.html" %}
</head>
Currently the only component included is the joystick-component
, which
acts a wrapper around nippleJS.
<joystick-component
mode="static"
size="200"
position="relative"
positionTop="100"
positionLeft="100"
positionRight=""
positionBottom=""
onmove="console.log(data)"
onend="console.log(data)"
></joystick-component>
To add the joystick-component to the page without using templates you can add it
to the page by adding the nipplejs.min.js
and
joystick-component.js
scripts to the head
of your page:
<head>
<script type="text/javascript" src="/webcomponents/vendor/nipplejs.min.js"></script>
<script type="text/javascript" src="/webcomponents/joystick-component.js"></script>
</head>
10.1.2.3. MessagingBlueprintļ
MessagingBlueprint is used to communicate between your python code and the page.
Adding the MessagingBlueprint to a WebServer is done as follows:
from pitop.labs import WebServer, MessagingBlueprint
server = WebServer(blueprints=[
MessagingBlueprint()
])
server.serve_forever()
To add messaging to the page MessagingBlueprint provides a setup template
setup-messaging.html
that can be included in the head
of your
page:
<head>
{% include "setup-messaging.html" %}
</head>
This adds a JavaScript function publish
to the page, which you can use
to send JavaScript Objects to your WebServer. The messages must have a type
,
and can optionally have some data
.
<select
id="dinner-select"
onchange="publish({ type: 'dinner_changed', data: this.value })"
>
<option value="tacos">Tacos</option>
<option value="spaghetti">Spaghetti</option>
</select>
To receive the messages sent by publish
you can pass a
message_handlers
dictionary to MessagingBlueprint. The keys of
message_handlers
correspond to the type
of the message and the
value must be a function that handles the message, a āmessage handlerā. The
message handler is passed the messageās data
value as itās first
argument.
from pitop.labs import WebServer, MessagingBlueprint
def on_dinner_change(data):
print(f'dinner is now {data}')
messaging = MessagingBlueprint(message_handlers={
'dinner_changed': on_dinner_change
})
server = WebServer(blueprints=[messaging])
server.serve_forever()
The second argument of a message handler is a send
function which
can send a message back to the page:
def on_dinner_change(data, send):
print(f'dinner is now {data}')
send({ 'type': 'dinner_received' })
To receive messages sent from a message handler the MessagingBlueprint also adds
a JavaScript function subscribe
to the page:
<script>
subscribe((message) => {
if (message.type === 'dinner_received') {
console.log('Dinner Received!')
}
})
</script>
Another way of sending messages to the page is to use the MessagingBlueprintās
broadcast
method:
from pitop import Button
from pitop.labs import WebServer, MessagingBlueprint
button = Button('D1')
def on_dinner_change(data):
print(f'dinner is now {data}')
messaging = MessagingBlueprint(message_handlers={
'dinner_changed': on_dinner_change
})
def reset():
messaging.broadcast({ 'type': 'reset' })
button.on_press = reset
server = WebServer(blueprints=[messaging])
server.serve_forever()
This is received by the same subscribe function as before:
<script>
subscribe((message) => {
if (message.type === 'reset') {
console.log('Reset')
}
})
</script>
There is one difference between broadcast
and send
:
broadcast
sends the message to every client whereas send
only
responds to the client that sent the message being handled.
10.1.2.4. VideoBlueprintļ
VideoBlueprint adds the ability to add a video feed from your python code to the page.
Adding the VideoBlueprint to a WebServer is done as follows:
from pitop import Camera
from pitop.labs import WebServer, VideoBlueprint
camera = Camera()
server = WebServer(blueprints=[
VideoBlueprint(get_frame=camera.get_frame)
])
server.serve_forever()
To add video styles to the page VideoBlueprint provides a setup template
setup-video.html
that can be included in the head
of your
page:
<head>
{% include "setup-video.html" %}
</head>
This adds a set of classes that can be used to style your video:
.background-video {
height: 100vh;
position: fixed;
top: 0;
left: 50%;
transform: translateX(-50%);
z-index: -1;
}
In order to render the video on the page you must use an img
tag with
the src
attribute of video.mjpg
:
<body>
<img src="video.mjpg" class="background-video"></img>
</body>
It is also possible to add multiple VideoBlueprints to a WebServer:
from pitop import Camera
from pitop.labs import WebServer, VideoBlueprint
camera_one = Camera(index=0)
camera_two = Camera(index=1)
server = WebServer(blueprints=[
VideoBlueprint(name="video-one", get_frame=camera_one.get_frame),
VideoBlueprint(name="video-two", get_frame=camera_two.get_frame)
])
server.serve_forever()
This makes it possible to to add multiple video feeds to the page, where the
src
attribute uses the name of the VideoBlueprint with a .mjpg
extension:
<body>
<img src="video-one.mjpg"></img>
<img src="video-two.mjpg"></img>
</body>
If you want to use the static files on your page without using templates you can do so by adding them to the page yourself:
<head>
<link rel="stylesheet" href="/video/styles.css"></link>
</head>
10.1.2.5. ControllerBlueprintļ
ControllerBlueprint combines blueprints that are useful in creating web apps that interact with your pi-top. The blueprints it combines are the BaseBlueprint, WebComponentsBlueprint, MessagingBlueprint and VideoBlueprint.
from pitop import Camera
from pitop.labs import WebServer, ControllerBlueprint
camera = Camera()
def on_dinner_change(data):
print(f'dinner is now {data}')
server = WebServer(blueprints=[
ControllerBlueprint(
get_frame=camera.get_frame,
message_handlers={'dinner_changed': on_dinner_change}
)
])
server.serve_forever()
To simplify setup ControllerBlueprint provides a base-controller.html
template which includes all the setup snippets for itās children blueprints:
{% extends "base.html" %}
{% block title %}
Web Controller
{% endblock %}
{% block head %}
{{ super() }}
{% include "setup-video.html" %}
{% include "setup-messaging.html" %}
{% include "setup-webcomponents.html" %}
{% endblock %}
base-controller.html
extends base.html
, this means you can use
blocks defined in base.html
when extending base-controller.html
:
{% extends "base-controller.html" %}
{% block title %}My WebController{% endblock %}
{% block head %}
<!-- call super() to setup blueprints -->
{{ super() }}
<link rel="stylesheet" href="custom-styles.css"></link>
{% endblock %}
{% block main %}
<h1>Video</h1>
<img src="video.mjpg"></img>
{% endblock %}
10.1.2.6. RoverControllerBlueprintļ
RoverControllerBlueprint uses the ControllerBlueprint to create a premade web controller specifically built for rover projects.
from pitop import Pitop, Camera
from pitop.labs import WebServer, RoverControllerBlueprint
rover = Pitop()
rover.add_component(Camera())
rover.add_component(DriveController())
rover.add_component(PanTiltController())
server = WebServer(blueprints=[
RoverControllerBlueprint(
get_frame=rover.camera.get_frame,
drive=rover.drive,
pan_tilt=rover.pan_tilt
)
])
server.serve_forever()
RoverControllerBlueprint provides a page template base-rover.html
which
has a background video and two joysticks:
By default the right joystick is used to drive the rover around and the left
joystick controls the pan tilt mechanism. The drive
keyword argument is
required, but the pan_tilt
keyword argument is optional; if it is not
passed the left joystick is not rendered.
It is possible to customise the page by extending the base-rover.html
template:
{% extends "base-rover.html" %}
{% block title %}My Rover Controller{% endblock %}
{% block main %}
<!-- call super() to keep video and joysticks -->
{{ super() }}
<button onclick="publish({ type: 'clicked' })"></button>
{% endblock %}
It is also possible to customise the message handlers used by the RoverControllerBlueprint, for example to swap the joysticks so the left drives the rover and the right controls pan tilt:
from pitop import Camera, DriveController, PanTiltController, Pitop
from pitop.labs import RoverWebController
from pitop.labs.web.blueprints.rover import drive_handler, pan_tilt_handler
rover = Pitop()
rover.add_component(DriveController())
rover.add_component(PanTiltController())
rover.add_component(Camera())
rover_controller = RoverWebController(
get_frame=rover.camera.get_frame,
message_handlers={
"left_joystick": lambda data: drive_handler(rover.drive, data),
"right_joystick": lambda data: pan_tilt_handler(rover.pan_tilt, data),
},
)
rover_controller.serve_forever()
Note that when left_joystick
or right_joystick
are in
message_handlers
the pan_tilt
and drive
arguments do not
need to be passed respectively.