There are many solutions when it comes to remote controlling a Raspberry Pi robot. The approach which I have taken here is to use an Xbox-esque generic USB game controller, specifically the EasySMX ESM-9101. This controller does have to be put into its Android-compatible mode in order to work with the Pi, due to the default being a Windows-only, Xinput-based system with support for controller rumble.

EasySMX gamepad with USB receiver on the left

I will be using PiBorg’s fantastic gamepad library to interface with the controller. If you are planning on using a video game console controller I would recommend using the approxeng.input library instead, as those controllers are a little bit more fiddly due to their proprietary nature. approxeng.input provides a higher-level API which seems to abstract over some of those nuances. Note that, however, you can only use it with the list of supported controllers. Mine is not on that list.

Using a controller

So, let’s get started. The first thing we need to do is to download PiBorg’s gamepad library on our Raspberry Pi. Rather than having a typical pip-based install process, PiBorg’s library just requires you to git clone it.

cd ~
mkdir tools
cd tools
git clone https://github.com/piborg/Gamepad

Now let’s copy the required files into our project directory:

cd Gamepad
cp Controllers.py ~/path/to/project/directory
cp Gamepad.py ~/path/to/project/directory

Now we need to create a mapping between controller codes and buttons. Open up a new Python file called Controller.py. We will be putting our button and axis mappings in there. To discover the button and axis mappings, run python3 Gamepad.py and leave the device name blank. Try fiddling with your game controller. You should see the code for the button you are pressing or the axis you are changing.

My controller mappings will be for the aforementioned EasySMX wireless controller. You can base yours off this template.

# Controller.py

from Gamepad import Gamepad

class Controller(Gamepad):
    def __init__(self, joystickNumber = 0):
        Gamepad.__init__(self, joystickNumber)

        # Replace both the button and axis codes
        # with the ones for your controller. RT
        # and LT stand for the left and right
        # triggers, RS and LS stand for the
        # left and right analogue sticks and
        # LB and RB stand for the left and
        # right bumpers.

        self.buttonNames = {
            7 : "RT",
            5 : "RB",
            6 : "LT",
            4 : "LB",
            10 : "LS_BTN",
            11 : "RS_BTN",
            0 : "Y",
            1 : "B",
            3 : "X",
            2 : "A",
            8 : "BACK",
            9 : "START"
        }

        self.axisNames = {
            1 : "LS_Y",
            0 : "LS_X",
            3 : "RS_Y",
            2 : "RS_X"
        }

        self._setupReverseMaps()

Now we can create an example remote control program. It doesn’t actually do anything except log values to the console, but it serves as a good, simple example program:

# remote_control.py

from time import sleep

# This is the custom controller we created earlier
from Controller import Controller
# PiBorg's Gamepad library
import Gamepad

# Gamepad settings
gamepadType = Controller

exitControl = "BACK"

left_speed_control = "LS_Y"
right_speed_control = "RS_Y"


def main():
    left_speed: float = 0
    right_speed: float = 0

    # Wait for a connection
    if not Gamepad.available():
        print("=== Please connect your gamepad... ===")
        while not Gamepad.available():
            sleep(1.0)
        gamepad = gamepadType()
        print("=== Gamepad connected ===")

    # Handle joystick updates one at a time
    while gamepad.isConnected():
        # Wait for the next event
        eventType, control, value = gamepad.getNextEvent()

        # Print information about the event
        print(eventType)
        print(control)
        print(value)

        # Determine the event type
        if eventType == "BUTTON":
            # Button changed
            if control == exitControl:
                # Exit button (event on press)
                if value:
                    print("=== Exiting ===")
                    break
        elif eventType == "AXIS":
            # Joystick changed

            # On my particular controller I had
            # to inverse the axis values. This
            # may be different for yours.

            if control == left_speed_control:
                left_speed = value * -1
            elif control == right_speed_control:
                right_speed = value * -1

            print("Left speed: " + str(left_speed))
            print("Right speed: " + str(right_speed))

if __name__ == "__main__":
    main()

Try running it with python3 remote_control.py (or whatever you called your program). The speed readouts should increase as you manipulate the analogue controls. Note that this assumes a two-stick controller. If yours has one or no analogue controls then it will be far less ergonomic and easy to handle.

Conclusion

It is surprisingly easy to get a remote-controlled Raspberry Pi up and running these days. There are many high-level abstractions over raw filesystem APIs and other lower-level abstractions now that deal specifically with gamepads. Again, check out approxeng.input if you are lucky enough to have a supported controller.