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:

_images/rover.jpg

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.