Adventures of FartBot

Posted on Sun 14 August 2022 in electronics

Overview

This is Trilobot kit powered by a Raspberry Pi 4 and USB battery pack. There's a cheap speaker on the back to play the sounds. The controls are through a Python script.

I control the robot with a keyboard over SSH. It streams its camera over the LAN so that I can see what it's doing. In the video below I see what the robot sees, in the lower-right corner of the screen.

Video

:pouting_cat: :poop: :pouting_cat:

Hardware

Here's what I needed to make this, with some comments. :white_check_mark:

  • Raspberry Pi 4 8GB
    • The CPU and memory usage is very low, so a less powerful Raspberry Pi would be more efficient.
  • Trilobot
    • Official setup guide
    • The kit provides the chassis, wheels, and distance sensors (which I don't use for now)
    • The Raspberry Pi is sandwiched between the chassis boards
    • The camera is sandwiched in plates at the front of the body
  • A cheap non-official Raspberry Pi camera
    • I got mine cheap on Amazon. If it's non-standard, the camera might have components on the PCB that make it hard to mount properly.
  • A 3A 5V battery pack
    • In my opinion, the 3A is necessary. The Pi would sometimes reboot with less powerful batteries.
    • The official Trilobot battery is 3A.
  • A speaker with its own battery
    • Mine is older and was probably cheaper than this. Not sure where we got it, but it was probably from a discount store.
    • This kind of speaker is charged by USB and connects by a standard audio jack, which the Raspberry Pi has.
    • They are pretty loud!

Optional stuff :question:

  • Argon Fan HAT
    • The Raspberry Pi 4 gets hot, so having a fan might help.
  • Headers
    • If you're going to add HATs to the Pi, you'll need to play with different header sizes to make things connect properly.
  • Spacers
    • Again, if you're adding stuff, you need a variety of sizes to make fit properly. A bit of trial and error.

Software

Streaming

Controlling a robot like this is difficult with lag. It was a challenge to find a something that worked.

Luckily the libcamera documentation has a lot of examples on how to do low-latency streaming. For the least latency, use the ffplay commands not the cvlc commands.

In my case, I set the camera framerate to 12. I find that this seems to reduce lag further. I'm guessing this is because the client "falls behind" at higher framerates.

My command for the server/robot:

libcamera-vid -t 0 -n --width 800 --height 600 --codec h264 --profile baseline --framerate 12 --inline --listen -o tcp://0.0.0.0:8888

My command for the client/controller:

ffplay tcp://192.168.0.123:8888 -fflags nobuffer -flags low_delay -framedrop

Playing sound

I got free sound files from Wikimedia Commons. On Linux you can play .wav with aplay. For .ogg files you need to install vorbis-tools and then play them with the ogg123 command. Both of these commands can be used non-blocking in Python with subprocess.Popen().

Long-range VPN access

I use wireguard in a rpi <-> server <-> remote-controller setup. The "server" peer has a static IP address, so as long as the robot uses the PersistentKeepalive = 25 configuration, it can be connected to from anywhere. I've controlled the robot from far away with a laptop and a cellphone hotspot, with very little lag.

Some guides if you're interested in wireguard.

  • https://www.digitalocean.com/community/tutorials/how-to-set-up-wireguard-on-ubuntu-22-04
  • https://wiki.archlinux.org/title/WireGuard

Remote control Python script

I'm still working on this code. I feel like there must be a better way.

When you connect to the Pi through SSH, you only have a terminal to send commands over. What this code does is listen for key presses and run code conditionally. If a key is held down, keeps performing an action. If a key stops being pressed, the code returns to rest after a short delay.

So if you hold down w the robot will keep moving forward. Since you're holding down w, you're actually sending repetitive wwwwwwwwwwww every 100ms or so through the console. When you stop pressing w, the robot will keep moving forward for 500ms and then stop.

If you tap a the robot will rotate to the left for 500ms, then stop.

This makes it like playing a first-person video game on your keyboard. But it's a lot less responsive and you can't press key combinations.

import sys
import select
import tty
import termios
from random import sample
from subprocess import Popen
from time import sleep
from trilobot import Trilobot

tbot = Trilobot()

def isData():
    return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], [])

old_settings = termios.tcgetattr(sys.stdin)

def tbot_dispatch(i, speed):
    if i in ["w", "a", "s", "d", "q", "e"]:
        if i == "w":
            tbot.forward(speed)
        elif i == "a":
            tbot.turn_left(0.8*speed)
        elif i == "s":
            tbot.backward(speed)
        elif i == "d":
            tbot.turn_right(0.8*speed)
        elif i == "q":
            tbot.set_motor_speeds(0.8*speed, speed)
        elif i == "e":
            tbot.set_motor_speeds(speed, 0.8*speed)
    elif i == "m":
        to_play = sample([
        ["ogg123", "meow1.ogg"],
        ["ogg123", "meow2.ogg"],
        ["aplay", "meow3.wav"],
        ["ogg123", "meow4.ogg"]
        ], 1)
        Popen(to_play[0])
    elif i == "f":
        to_play = sample([
        ["ogg123", "fart1.ogg"],
        ["aplay", "fart2.wav"],
        ["ogg123", "fart3.ogg"]
        ], 1)
        Popen(to_play[0])
    elif i == "l":
        tbot.fill_underlighting(255, 255, 255)
    elif i == "k":
        tbot.fill_underlighting(0, 0, 0)
    elif i in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]:
        i = float(i)
        if i == 0.0:
            speed = 1.0
        else:
            speed = i/10.0
    else:
        tbot.coast()
    # Return the speed value
    return speed

speed = 0.5
downtime = 0
increment = 0.05
threshold = 10 * 0.05
at_rest = False
new, old = "", ""
try:
    tty.setcbreak(sys.stdin.fileno())

    while True:
        if isData():
            new = sys.stdin.read(1)
            at_rest = False
            downtime = 0
            if new == '\x1b':         # x1b is ESC
                break
            elif new != old:
                speed = tbot_dispatch(new, speed)
            old = new
        else:
            if at_rest:
                pass
            else:
                if downtime > threshold:
                    at_rest = True
                    new, old = "", ""
                    tbot.coast()
                    print("AT REST")
                else:
                    downtime += increment
                    sleep(increment)

finally:
    termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)

Next steps

  • Better lights to see in the dark :flashlight:
  • A microphone to stream audio :microphone:
  • Some kind of treat dispenser to make my cats a new friend :smiley_cat: