4. Recipes

In addition to the examples provided for each component/device in the API reference section of this documentation, the following recipes demonstrate some of the more advanced capabilities of the pi-top Python SDK. In particular, these recipes focus on practical use-cases that make use of multiple components/devices within the pi-top Python SDK.

Be sure to check out each component/device separately for simple examples of how to use them.

4.1. PMA: Using a Button to Control an LED

from time import sleep

from pitop import LED, Button

button = Button("D1")
led = LED("D2")

# Connect button to LED
button.when_pressed = led.on
button.when_released = led.off

# Wait for Ctrl+C to exit
try:
    while True:
        sleep(1)
except KeyboardInterrupt:
    pass

4.2. Robotics Kit: DIY Rover

from threading import Thread
from time import sleep

from pitop import BrakingType, EncoderMotor, ForwardDirection

# Setup the motors for the rover configuration

motor_left = EncoderMotor("M3", ForwardDirection.CLOCKWISE)
motor_right = EncoderMotor("M0", ForwardDirection.COUNTER_CLOCKWISE)

motor_left.braking_type = BrakingType.COAST
motor_right.braking_type = BrakingType.COAST


# Define some functions for easily controlling the rover


def drive(target_rpm: float):
    print("Start driving at target", target_rpm, "rpm...")
    motor_left.set_target_rpm(target_rpm)
    motor_right.set_target_rpm(target_rpm)


def stop_rover():
    print("Stopping rover...")
    motor_left.stop()
    motor_right.stop()


def turn_left(rotation_speed: float):
    print("Turning left...")
    motor_left.stop()
    motor_right.set_target_rpm(rotation_speed)


def turn_right(rotation_speed: float):
    print("Turning right...")
    motor_right.stop()
    motor_left.set_target_rpm(rotation_speed)


# Start a thread to monitor the rover


def monitor_rover():
    while True:
        print(
            "> Rover motor RPM's (L,R):",
            round(motor_left.current_rpm, 2),
            round(motor_right.current_rpm, 2),
        )
        sleep(1)


monitor_thread = Thread(target=monitor_rover, daemon=True)
monitor_thread.start()

# Go!

rpm_speed = 100
for _ in range(4):
    drive(rpm_speed)
    sleep(5)

    turn_left(rpm_speed)
    sleep(5)

stop_rover()

4.3. Robotics Kit: Robot - Moving Randomly

from random import randint
from time import sleep

from pitop import Pitop
from pitop.robotics.drive_controller import DriveController

# Create a basic robot
robot = Pitop()
drive = DriveController(left_motor_port="M3", right_motor_port="M0")
robot.add_component(drive)


# Use miniscreen display
robot.miniscreen.display_multiline_text("hey there!")


def random_speed_factor():
    # 0.01 - 1, 0.01 resolution
    return randint(1, 100) / 100


def random_sleep():
    # 0.5 - 2, 0.5 resolution
    return randint(1, 4) / 2


# Move around randomly
robot.drive.forward(speed_factor=random_speed_factor())
sleep(random_sleep())

robot.drive.left(speed_factor=random_speed_factor())
sleep(random_sleep())

robot.drive.backward(speed_factor=random_speed_factor())
sleep(random_sleep())

robot.drive.right(speed_factor=random_speed_factor())
sleep(random_sleep())

4.4. Robotics Kit: Robot - Line Detection

from signal import pause

from pitop import Camera, DriveController, Pitop
from pitop.processing.algorithms.line_detect import process_frame_for_line

# Assemble a robot
robot = Pitop()
robot.add_component(DriveController(left_motor_port="M3", right_motor_port="M0"))
robot.add_component(Camera())


# Set up logic based on line detection
def drive_based_on_frame(frame):
    processed_frame = process_frame_for_line(frame)

    if processed_frame.line_center is None:
        print("Line is lost!", end="\r")
        robot.drive.stop()
    else:
        print(f"Target angle: {processed_frame.angle:.2f} deg ", end="\r")
        robot.drive.forward(0.25, hold=True)
        robot.drive.target_lock_drive_angle(processed_frame.angle)
        robot.miniscreen.display_image(processed_frame.robot_view)


# On each camera frame, detect a line
robot.camera.on_frame = drive_based_on_frame


pause()

4.5. Displaying camera stream in pi-top [4]’s miniscreen

from pitop import Camera, Pitop

camera = Camera()
pitop = Pitop()
camera.on_frame = pitop.miniscreen.display_image

4.6. Robotics Kit: Robot - Control using Bluedot

Note

BlueDot is a Python library that allows you to control Raspberry Pi projects remotely. This example demonstrates a way to control a robot with a virtual joystick.

from signal import pause
from threading import Lock

from bluedot import BlueDot

from pitop import DriveController

bd = BlueDot()
bd.color = "#00B2A2"
lock = Lock()

drive = DriveController(left_motor_port="M3", right_motor_port="M0")


def move(pos):
    if lock.locked():
        return

    if any(
        [
            pos.angle > 0 and pos.angle < 20,
            pos.angle < 0 and pos.angle > -20,
        ]
    ):
        drive.forward(pos.distance, hold=True)
    elif pos.angle > 0 and 20 <= pos.angle <= 160:
        turn_radius = 0 if 70 < pos.angle < 110 else pos.distance
        speed_factor = -pos.distance if pos.angle > 110 else pos.distance
        drive.right(speed_factor, turn_radius)
    elif pos.angle < 0 and -160 <= pos.angle <= -20:
        turn_radius = 0 if -110 < pos.angle < -70 else pos.distance
        speed_factor = -pos.distance if pos.angle < -110 else pos.distance
        drive.left(speed_factor, turn_radius)
    elif any(
        [
            pos.angle > 0 and pos.angle > 160,
            pos.angle < 0 and pos.angle < -160,
        ]
    ):
        drive.backward(pos.distance, hold=True)


def stop(pos):
    lock.acquire()
    drive.stop()


def start(pos):
    if lock.locked():
        lock.release()
    move(pos)


bd.when_pressed = start
bd.when_moved = move
bd.when_released = stop

pause()

4.7. Using the pi-topPULSE’s LED matrix to show the battery level

from time import sleep

from pitop import Pitop
from pitop.pulse import ledmatrix


def draw_battery_outline():  # Draw the naked battery
    for y in range(0, 6):
        ledmatrix.set_pixel(1, y, 64, 64, 255)
        ledmatrix.set_pixel(5, y, 64, 64, 255)
    for x in range(2, 5):
        ledmatrix.set_pixel(x, 0, 64, 64, 255)
        ledmatrix.set_pixel(x, 6, 192, 192, 192)
    ledmatrix.show()


def update_battery_state(charging_state, capacity):
    r = 0
    g = 0
    b = 0
    if charging_state == 0:
        if capacity < 11:
            r = 255
        else:
            g = 255
    elif charging_state == 1:
        r = 255
        g = 225

    cap = int(capacity / 20) + 1
    if cap < 0:
        cap = 0
    if cap > 5:
        cap = 5

    if cap > 0:
        for y in range(1, cap + 1):
            ledmatrix.set_pixel(2, y, r, g, b)
            ledmatrix.set_pixel(3, y, r, g, b)
            ledmatrix.set_pixel(4, y, r, g, b)
    if cap == 0:
        cap = 1
    if cap < 6:
        if (capacity < 50) and (charging_state == 0):
            # blinking warning
            for i in range(1, 3):
                for y in range(cap + 1, 6):
                    ledmatrix.set_pixel(2, y, 0, 0, 0)
                    ledmatrix.set_pixel(3, y, 0, 0, 0)
                    ledmatrix.set_pixel(4, y, 0, 0, 0)
                ledmatrix.show()
                sleep(0.4)
                for y in range(cap + 1, 6):
                    ledmatrix.set_pixel(2, y, 255, 0, 0)
                    ledmatrix.set_pixel(3, y, 255, 0, 0)
                    ledmatrix.set_pixel(4, y, 255, 0, 0)
                ledmatrix.show()
                sleep(0.4)

        else:
            for y in range(cap + 1, 6):
                ledmatrix.set_pixel(2, y, 0, 0, 0)
                ledmatrix.set_pixel(3, y, 0, 0, 0)
                ledmatrix.set_pixel(4, y, 0, 0, 0)
            ledmatrix.show()
            sleep(5)
    return 0


def main():
    ledmatrix.rotation(0)
    ledmatrix.clear()  # Clear the display
    draw_battery_outline()  # Draw the battery outline

    battery = Pitop().battery

    while True:
        try:
            charging_state, capacity, _, _ = battery.get_full_state()
            update_battery_state(charging_state, capacity)  # Fill battery with capacity

        except Exception as e:
            print("Error getting battery info: " + str(e))


if __name__ == "__main__":
    main()

4.8. Choose a pi-top [4] miniscreen startup animation

Note

This code makes use of the GIPHY SDK. Follow the instructions here to find out how to apply for an API Key to use with this project.

Replace <MY GIPHY KEY> with the key provided (keep the quotes).

You can change the type of images that you get by changing SEARCH_TERM = “Monochrome” to whatever you want.

import json
from configparser import ConfigParser
from os import geteuid
from random import randint
from signal import pause
from sys import exit
from time import sleep
from urllib.parse import urlencode
from urllib.request import urlopen

from PIL import Image
from requests.models import PreparedRequest

from pitop.miniscreen import Miniscreen


def is_root():
    return geteuid() == 0


if not is_root():
    print("Admin access required - please run this script with 'sudo'.")
    exit()

# Define Giphy parameters
SEARCH_LIMIT = 10
SEARCH_TERM = "Monochrome"

CONFIG_FILE_PATH = "/etc/pt-miniscreen/settings.ini"
STARTUP_GIF_PATH = "/home/pi/miniscreen-startup.gif"


API_KEY = "<MY GIPHY KEY>"

# Define global variables
gif = None
miniscreen = Miniscreen()
req = PreparedRequest()
req.prepare_url(
    "http://api.giphy.com/v1/gifs/search",
    urlencode({"q": SEARCH_TERM, "api_key": API_KEY, "limit": f"{SEARCH_LIMIT}"}),
)


def display_instructions_dialog():
    miniscreen.select_button.when_pressed = play_random_gif
    miniscreen.cancel_button.when_pressed = None
    miniscreen.display_multiline_text(
        "Press SELECT to load a random GIF!", font_size=18
    )


def display_user_action_select_dialog():
    miniscreen.select_button.when_pressed = save_gif_as_startup
    miniscreen.cancel_button.when_pressed = play_random_gif
    miniscreen.display_multiline_text(
        "SELECT: save GIF as default startup animation. CANCEL: load new GIF",
        font_size=12,
    )


def display_loading_dialog():
    miniscreen.select_button.when_pressed = None
    miniscreen.cancel_button.when_pressed = display_instructions_dialog
    miniscreen.display_multiline_text("Loading random GIF...", font_size=18)


def display_saving_dialog():
    miniscreen.select_button.when_pressed = None
    miniscreen.cancel_button.when_pressed = None
    miniscreen.display_multiline_text(
        "GIF saved as default startup animation!", font_size=18
    )
    # Saving is fast, so we need to wait a short while for the message to be seen on the display
    sleep(1)


def play_random_gif():
    global gif

    # Show "Loading..." while processing for a GIF
    display_loading_dialog()

    # Get GIF data from Giphy
    with urlopen(req.url) as response:
        data = json.loads(response.read())

    # Extract random GIF URL from JSON response
    gif_url = data["data"][randint(0, SEARCH_LIMIT - 1)]["images"]["fixed_height"][
        "url"
    ]

    # Load GIF from URL
    gif = Image.open(urlopen(gif_url))

    # Play one loop of GIF animation
    miniscreen.play_animated_image(gif)

    # Ask user if they want to save it
    display_user_action_select_dialog()


def save_gif_as_startup():
    # Display "saving" dialog
    display_saving_dialog()

    # Save file to home directory
    gif.save(STARTUP_GIF_PATH, save_all=True)

    config = ConfigParser()
    cfg_section = "Bootsplash"

    if not config.has_section(cfg_section):
        config.add_section(cfg_section)

    config.set(cfg_section, "Path", STARTUP_GIF_PATH)

    with open(CONFIG_FILE_PATH, "w") as f:
        config.write(f)

    # Go back to the start
    display_instructions_dialog()


# Display initial dialog
display_instructions_dialog()

# Wait indefinitely for user input
pause()