Equip Trilobot with a camera and remote control

I still had an unused Raspberry Pi 4 lying around and used it to build Trilobot, a mid-level robot learning platform from Pimoroni. The bot is easy to assemble and guarantees hours of fun tweaking its code to implement different functionalities.

So far I’ve only managed to assign a few functions to the helpful buttons that are integrated on the bot. The code is mostly based on the extensive trilobot-python library that Pimoroni kindly provides to get you started. If you’re setting up Trilobot for the first time, please follow their step-by-step tutorial before you read on here.

With a few tweaks to the original examples, Trilobot can be remote-controlled and activate its camera all at the same time. In my case it also automatically runs the script at startup. In the weeks to come I’m planning on extending it further with sensors and face recognition, but the list of options is open-ended.

Find the full script below and on my github page. Further instructions can be found in this blog post. I will also add to them as I extend my bots functionality. Please keep in mind that this is only a very simple script to get Trilobot up and running. No fancy programming involved!

Getting Trilobot up and running

The script below adds three functions to the Trilobot buttons: remote control, camera and an off-switch.

#!/usr/bin/env python3

import time
import os
import math
import threading
from trilobot import Trilobot, BUTTON_A, BUTTON_B, BUTTON_X
from trilobot.simple_controller import SimpleController

"""
This script assigns functions such as remote and camera control to the Trilobot 
buttons and is automatically started at boot.
"""

RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

tbot = Trilobot()


def camera_active():
    """Activate the camera using libcamera. Connect to localhost with a client.
    """
    while True:
        try:
            print("Camera up and running. Connect client to stream.")
            os.system("libcamera-vid -t 0 --width 960 --height 635 -q 100 -n --inline --listen -o tcp://0.0.0.0:8888")
            time.sleep(5)
        except KeyboardInterrupt:
            print("Camera disabled.")
            os._exit(0)


def create_8bitdo_lite_controller():
    """ Create a controller class for the 8BitDo Lite controller.
    """
    controller = SimpleController("8BitDo Lite gamepad")

    # Button and axis registrations for 8BitDo Lite
    controller.register_button("A", 305)
    controller.register_button("B", 304)
    controller.register_button("X", 307)
    controller.register_button("Y", 306)
    controller.register_button("Plus", 311, alt_name="Start")
    controller.register_button("Minus", 310, alt_name="Select")
    controller.register_button("L1", 308, alt_name="LB")
    controller.register_axis_as_button("L2", 2, 0, 1023, alt_name="LT")
    controller.register_button("R1", 309, alt_name="RB")
    controller.register_axis_as_button("R2", 5, 0, 1023, alt_name="RT")
    controller.register_button("Home", 139)
    controller.register_axis_as_button("L_Left", 0, 0, 32768)
    controller.register_axis_as_button("L_Right", 0, 65535, 32768)
    controller.register_axis_as_button("L_Up", 1, 0, 32768)
    controller.register_axis_as_button("L_Down", 1, 65535, 32768)
    controller.register_axis_as_button("R_Left", 3, 0, 32768)
    controller.register_axis_as_button("R_Right", 3, 65535, 32768)
    controller.register_axis_as_button("R_Up", 4, 0, 32768)
    controller.register_axis_as_button("R_Down", 4, 65535, 32768)
    controller.register_axis_as_button("Left", 16, -1, 0)
    controller.register_axis_as_button("Right", 16, 1, 0)
    controller.register_axis_as_button("Up", 17, -1, 0)
    controller.register_axis_as_button("Down", 17, 1, 0)

    controller.register_axis("LX", 0, 0, 65536)
    controller.register_axis("LY", 1, 0, 65536)
    controller.register_axis("RX", 3, 0, 65536)
    controller.register_axis("RY", 4, 0, 65536)

    return controller


def remote_active():
    """Connect the 8BitDo gamepad to Trilobot. Remote needs a paired bluetooth connection.
    """
    print("Remote up and running.")

    # Connect to 8BitDo Lite gamepad
    print("Connecting to 8BitDo Lite gamepad...")
    controller = create_8bitdo_lite_controller()

    # Attempt to connect to the created controller
    controller.connect()

    # Run an animation on the underlights to show a controller has been selected
    for led in range(NUM_UNDERLIGHTS):
        tbot.clear_underlighting(show=False)
        tbot.set_underlight(led, RED)
        time.sleep(0.1)
        tbot.clear_underlighting(show=False)
        tbot.set_underlight(led, GREEN)
        time.sleep(0.1)
        tbot.clear_underlighting(show=False)
        tbot.set_underlight(led, BLUE)
        time.sleep(0.1)

    tbot.clear_underlighting()

    h = 0
    v = 0
    spacing = 1.0 / NUM_UNDERLIGHTS

    tank_steer = False
    while True:

        if not controller.is_connected():
            # Attempt to reconnect to the controller if 10 seconds have passed since the last attempt
            controller.reconnect(10, True)

        try:
            # Get the latest information from the controller. This will throw a RuntimeError if the
            # controller connection is lost
            controller.update()
        except RuntimeError:
            # Lost contact with the controller, so disable the motors to stop Trilobot if it was moving
            tbot.disable_motors()

        if controller.is_connected():

            # Read the controller bumpers to see if the tank steer mode has been enabled or disabled
            try:
                if controller.read_button("L1") and tank_steer:
                    tank_steer = False
                    print("Tank Steering Disabled")
                if controller.read_button("R1") and not tank_steer:
                    tank_steer = True
                    print("Tank Steering Enabled")
            except ValueError:
                # Cannot find 'L1' or 'R1' on this controller
                print("Tank Steering Not Available")

            try:
                if tank_steer:
                    # Have the left stick's Y axis control the left motor, and the right stick's Y axis
                    # control the right motor
                    ly = controller.read_axis("LY")
                    ry = controller.read_axis("RY")
                    tbot.set_left_speed(-ly)
                    tbot.set_right_speed(-ry)
                else:
                    # Have the left stick control both motors
                    lx = controller.read_axis("LX")
                    ly = 0 - controller.read_axis("LY")
                    tbot.set_left_speed(ly + lx)
                    tbot.set_right_speed(ly - lx)
            except ValueError:
                # Cannot find 'LX', 'LY', or 'RY' on this controller
                print("Motors disabled")
                tbot.disable_motors()

            # Run a rotating rainbow effect on the RGB underlights
            for led in range(NUM_UNDERLIGHTS):
                led_h = h + (led * spacing)
                if led_h >= 1.0:
                    led_h -= 1.0

                try:
                    if controller.read_button("A"):
                        tbot.set_underlight_hsv(led, 0.0, 0.0, 0.7, show=False)
                    else:
                        tbot.set_underlight_hsv(led, led_h, show=False)
                except ValueError:
                    # Cannot find 'A' on this controller
                    tbot.set_underlight_hsv(led, led_h, show=False)

            tbot.show_underlighting()

            # Advance the rotating rainbow effect
            h += 0.5 / 360
            if h >= 1.0:
                h -= 1.0

        else:
            # Run a slow red pulsing animation to show there is no controller connected
            val = (math.sin(v) / 2.0) + 0.5
            tbot.fill_underlighting(val * 127, 0, 0)
            v += math.pi / 200

        time.sleep(0.01)


def deactivate_programme():
    """Switch off the lights, button LEDs and exit the programme.
    """
    tbot.clear_underlighting()
    tbot.set_button_led(BUTTON_A, 0)
    tbot.set_button_led(BUTTON_B, 0)
    tbot.set_button_led(BUTTON_X, 0)
    time.sleep(2)
    os._exit(0)


def activate_button():
    """Buttons are hardwired to three specific functions: camera (A), remote (B) and off-switch (X).
    """
    last_state_a = False
    last_state_b = False
    last_state_x = False

    while True:
        # Read the button
        button_state_a = tbot.read_button(BUTTON_A)
        button_state_b = tbot.read_button(BUTTON_B)
        button_state_x = tbot.read_button(BUTTON_X)

        if button_state_a != last_state_a:

            # Button A was pressed
            if button_state_a:
                print("Camera is being activated.")

                # Turn the button's neighboring LED on or off
                tbot.set_button_led(BUTTON_A, 0.1)

                # Turn on the camera
                threading.Thread(target=camera_active).start()

            # Update our record of the button state
            last_state_a = button_state_a

        elif button_state_b != last_state_b:

            # Button B was pressed
            if button_state_b:
                print("Remote control is being activated.")

                # Turn the button's neighboring LED on or off
                tbot.set_button_led(BUTTON_B, 0.1)

                # Turn on the remote control
                threading.Thread(target=remote_active).start()

            # Update our record of the button state
            last_state_b = button_state_b

        elif button_state_x != last_state_x:

            # Button X was pressed
            if button_state_x:
                print("Program exit, quitting threads.")

                # Turn the button's neighboring LED on or off
                tbot.set_button_led(BUTTON_X, 0.5)

                # Deactivate programme
                deactivate_programme()

            # Update our record of the button state
            last_state_x = button_state_x


if __name__ == "__main__":

    try:

        # creating first thread
        t1 = threading.Thread(target=activate_button)

        # starting first thread to recognise when a button is pressed
        t1.start()
        t1.join()

        while True:
            time.sleep(100)

    # exiting all threads and turning off underlighting and leds
    except (KeyboardInterrupt, SystemExit):
        print("\nReceived keyboard interrupt, quitting threads.\n")
        tbot.clear_underlighting()
        tbot.set_button_led(BUTTON_A, 0)
        tbot.set_button_led(BUTTON_B, 0)
        tbot.set_button_led(BUTTON_X, 0)
        os._exit(0)

Getting the camera to work

For the 64-bit Raspberry Pi OS, which I am currently using, libcamera is the only working option for now. Unfortunately, it doesn’t have any Python bindings but it works well via the command line. If you’re still on the 32-bit OS, you can also use picamera to stream video. It should come with your OS by default.

To get the camera working, Trilobot has to open a TCP/IP stream on the network. The port is defined via the listen option in the code above. On the other end, the client (in my case a second Linux computer) has to open the tcp connection using an appropriate video player for network streaming.

I’m using the ffplay video player on Linux for this. Typing the following command should open the video stream for you.

ffplay tcp://<IP of Trilobot>:8888 -vf "setpts=N/30" -fflags nobuffer -flags low_delay -framedrop

A word on remotes

For this example I’m using my 8BitDo Lite gamepad, which is supported by Trilobot.

The mappings for this and several other controllers are included in the Pimoroni library. Just copy them into your code to enable remote-control for Trilobot.

Running the script at startup

Getting code to run automatically at startup is sometimes a bit tricky on the Raspberry Pi. In this case the rc.local method works nicely. Just edit the rc.local config file with an editor of your choice.

sudo nano /etc/rc.local

Add the command to execute the python script and don’t forget to use the absolute path to the file. As the program will run in a loop and is not likely to exit, also add the & at the end.

Leave the exit 0 at the end and then save the file. In nano, type Ctrl-x and Y to save and exit.

python3 /home/pi/sample.py &

Now reboot the Pi.

sudo reboot

And that’s it. Connect Trilobot to its power source and you should be good to go!

2 thoughts on “Equip Trilobot with a camera and remote control”

  1. I also have a trilobot and assembled it with no problems. then i looked at remote controlling and looked to use a wiimote as that is what i have. Someone, unfortunately i cannot remember who sent me a controller mapping py program to use. Initially this would not work and had several snytax errors which i seemed to correct so the program would run under geany with no issues. I then tried to use porimoni program remote_control.py which highlighted an error in the mapping. It said that something was not defined. Being very ignorant on programming i do not know how to solve this issue. Can you help?

    Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: