My Account

Wish List (0)


PiBorg is still open and we are still shipping orders.
We expect orders may be delayed by a day or two due to COVID-19.

Gamepad library

Written by on .

As you may know our robots and motor control boards come with examples of how they can be controlled using a game controller connected to the Raspberry Pi. This is a great method for when you want to drive robots in remote control mode :)

This has been great for using, but a pain to write the code for. The trouble comes from a few recurring issues:

  • We have to use hard to remember numbers to identify the buttons and joysticks
  • The numbers need to be changed if we change controllers
  • It is hard for others to change the scripts for their own controllers
  • We were using Pygame, which does not always play well with SSH connections

To solve these problems we set out to create our own simple controller library. The result was our new Gamepad library :)

Main features

In order to fulfill our needs we came up with a list of requirements which match what we use on our robots now, and cover future additions such as new controllers.

This is the list we agreed on:

  • Work with any controller in the /dev/input/js? list
  • Work with readable names for the controls
  • Keep the same names for the same buttons on different controllers
  • Easy to understand and use
  • Flexible in how it can be used
  • Easy to extend to new controllers
  • Works on both Python 2 and Python 3
  • Supports multiple controllers at once
  • Only depends on the basic Python libraries
  • Lightweight on CPU and RAM

What we created is a library of two fairly simple Python files which work with all of the controllers we have that can be connected to a Raspberry Pi :)

The library is so much easier to use than our old method, which relied on Pygame, that we wanted to freely share it with everybody who would like to try it.

It has been designed and tested with Raspberry Pis running Raspbian, but it should work with any Linux install that uses the standard /dev/input/js? device layout and has a copy of Python installed.

Installing Gamepad

The Gamepad library only needs to be downloaded from GitHub to be used:

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

Once downloaded any project you like can use the library as long as it can find the Gamepad.py and Controllers.py files. This can be done by copying the files into your project or updating the Python path so that it knows where to find them. You can also write your own scripts in the same directory if you want :)

Some of the examples (e.g. rockyJoy.py) make use of other libraries by inserting their location into the Python path within the script itself. This technique also works with the Gamepad library.

How to use the library

The Gamepad library provides three basic ways of reading controller inputs:

  1. Polling - where the script reads changes one at a time
  2. Asynchronous - where the library reads changes in the background
  3. Events - where the library makes callbacks to the script when changes happen

Having the three different modes allows you to use the library in the way which is easiest to understand or best suited for what your script is doing. We have included examples of all three methods.

In all three modes you create an instance of the Gamepad class to connect to the controller:

import Gamepad
gamepad = Gamepad.Gamepad()

You can check if the controller is available first using the available() function. If Controllers.py contains a device with named controls you can use it instead to use those names as well:

import Gamepad
if Gamepad.available():
    gamepad = Gamepad.PS4()
else:
    print('Controller not connected :(')

If you want to use multiple controllers, or just a specific controller, you can specify the one to use by its ID number. Different controller types also work at the same time.

import Gamepad
if Gamepad.available(0):
    first_gamepad = Gamepad.PS4(0)
else:
    print('First controller not connected :(')
if Gamepad.available(1):
    second_gamepad = Gamepad.PS3(1)
else:
    print('Second controller not connected :(')

Polling cannot be mixed with the other two modes, but asynchronous and event modes can be used together. The three methods are described in full below.

Method 1 - Polling

In polling mode the script asks the Gamepad library for each event one at a time. This means you handle each update in order without any background threads involved.

Each time you make the getNextEvent() call the code will return the next recorded event, or wait for one if there are no more ready. Each call returns three values:

eventType, control, value = gamepad.getNextEvent()

The eventType value will either be 'BUTTON' or 'AXIS' to let you know the type of control that changed.

The control value will either be the string name for the control if available, or a number if there is no name.

The value depends on the control type. For buttons the value will be True for pressed and False otherwise. For joystick axis the value will be a float between -1.0 and +1.0.

See PollingExample.py for an example of use.

Method 2 - Asynchronous

In asynchronous mode the Gamepad instance updates itself using a background thread. The state of the controller can then be checked whenever the script likes. The thread is entirely managed by the library.

In order to use this mode the background thread must be started when the instance has been created, and stopped before the script ends:

import Gamepad
gamepad = Gamepad.PS4()

# Start the background updating
gamepad.startBackgroundUpdates()

# Your code here :)

# Stop the background thread
gamepad.disconnect()

Like the polling method above, you can check if the controller is connected first. If you are using multiple controllers they need to be started and stopped separately.

Several functions are provided to check the controller state and handle button changes:

  1. isConnected() - check if the controller is still connected.
  2. isPressed(X) - check if a button is currently pressed.
  3. beenPressed(X) - see if a button was pressed since the last check. Only returns True once per press.
  4. beenReleased(X) - see if a button was released since the last check. Only returns True once per release.
  5. axis(X) - read the latest joystick position. This is a float number between -1.0 and +1.0.

For all these functions X can either be the string name or the raw number (e.g. 'CIRCLE' or 1).

As the updates happen in the background, the script can call these functions as often or as little as you like :)

See AsyncExample.py for an example of use.

Method 3 - Events

In event mode the Gamepad instance updates itself using a background thread. When a change occurs any functions registered for the event get called with the updated state. The thread is entirely managed by the library.

Note that as the diagram above shows the event callbacks are made in the background thread. This means they can run at the same time as your other tasks in the script.

Like the asynchronous mode the background thread must be started when the instance has been created, and stopped before the script ends. Once started event handlers can be registered.

import Gamepad
gamepad = Gamepad.PS4()

# Start the background updating
gamepad.startBackgroundUpdates()

# Your code here :)
# This includes callback registration

# Stop the background thread
gamepad.disconnect()

Like the polling method above, you can check if the controller is connected first. If you are using multiple controllers they need to be started and stopped separately.

There are a few types of event which can be registered for:

  • addButtonPressedHandler(X, F) - called when a button is pressed, no values passed.
  • addButtonReleasedHandler(X, F) - called when a button is released, no values passed.
  • addButtonChangedHandler(X, F) - called when a button changed state, boolean passed with True for pressed or False for released.
  • addAxisMovedHandler(X, F) - called when a joystick is moved, a float between -1.0 and +1.0 is passed.

For all of these functions X can either be the string name or the raw number (e.g. 'CIRCLE' or 1). F is the function which gets called when the event occurs.

For example you might register a function like this:

def printHappy():
   print(':)')

gamepad.addButtonPressedHandler('TRIANGLE', printHappy)

Note that each event can have multiple registered functions, and a function can be registered with multiple events if you want :)

Functions can also be unregistered by swapping add for remove in the calls above. removeAllEventHandlers() will wipe the entire callback list for a controller.

See EventExample.py for an example of use.

Mixing asynchronous and event modes

The final option is mixing the asynchronous and event modes. This works exactly like the event method, but you check the state of the Gamepad instance as well.

This does not work with the polling method as both the script and the library will be trying to grab the next controller event, causing the polling loop to miss events :(

Mixing these two modes can be useful as some things suit the event method better, such as button presses, while other actions like joystick movements make more sense to update at a regular rate.

This is my preferred choice as I find it the easiest to read and understand when writing my own scripts.

See AsyncAndEventExample.py for an example of use.

Real world examples

To give a better idea of how the Gamepad library works in practice we have a couple of examples controlling real robots :)

rockyJoy.py controls a RockyBorg.

monsterJoy.py controls a MonsterBorg.

Setting up your own controllers

So you have decided to give the Gamepad library a try and it does not have your controller, how do you add it?

The process is actually quite simple and can also be used to correct the mapping if it is not right for your system. You can add as many controller types as you like :)

Start by running ./Gamepad.py and press ENTER without typing any name when asked what your device is. Upon moving a joystick or pressing a button you should see events shown with index numbers:

For each control make a note of its index number (2nd column in green), keeping the button and axis numbers separate. Note that for some controls (e.g. L2 on a PS4 controller) you may get both an axis and a button number for the same control, if so make a note of both.

Once you have made a note of all the button and axis numbers for your controller you are ready to add it to the Controllers.py file.

Start by copying the example class:

class example(Gamepad):
    # This class must have self.axisNames with a map
    # of numbers to capitalised strings. Follow the
    # conventions the other classes use for generic
    # axes, make up your own names for axes unique
    # to your device.
    # self.buttonNames needs the same treatment.
    # Use python Gamepad.py to get the event mappings.
    fullName = 'Enter the human readable name of the device here'

    def __init__(self, joystickNumber = 0):
        Gamepad.__init__(self, joystickNumber)
        self.axisNames = {
            0: 'AXIS0',
            1: 'AXIS1',
            2: 'AXIS2'
        }
        self.buttonNames = {
            0: 'BUTTON0',
            1: 'BUTTON1',
            2: 'BUTTON2'
        }
        self._setupReverseMaps()

The first thing to do is change the class name and the description in fullName to match the controller. I will also remove the help comment and the example entries:

class ClearWired(Gamepad):
    fullName = 'Unbranded PC USB controller'

    def __init__(self, joystickNumber = 0):
        Gamepad.__init__(self, joystickNumber)
        self.axisNames = {
        }
        self.buttonNames = {
        }
        self._setupReverseMaps()

Add the axis numbers into the axisNames dictionary with names in upper-case:

class ClearWired(Gamepad):
    fullName = 'Unbranded PC USB controller'

    def __init__(self, joystickNumber = 0):
        Gamepad.__init__(self, joystickNumber)
        self.axisNames = {
            0: 'LEFT-X',
            1: 'LEFT-Y',
            2: 'RIGHT-Y',
            3: 'RIGHT-X',
            4: 'DPAD-X',
            5: 'DPAD-Y'
        }
        self.buttonNames = {
        }
        self._setupReverseMaps()

Now do the same with the button numbers and the buttonNames dictionary:

class ClearWired(Gamepad):
    fullName = 'Unbranded PC USB controller'

    def __init__(self, joystickNumber = 0):
        Gamepad.__init__(self, joystickNumber)
        self.axisNames = {
            0: 'LEFT-X',
            1: 'LEFT-Y',
            2: 'RIGHT-Y',
            3: 'RIGHT-X',
            4: 'DPAD-X',
            5: 'DPAD-Y'
        }
        self.buttonNames = {
            0:  '1',
            1:  '2',
            2:  '3',
            3:  '4',
            4:  'L1',
            5:  'L2',
            6:  'R1',
            7:  'R2',
            8:  'SELECT',
            9:  'START',
            10: 'L3',
            11: 'R3'
        }
        self._setupReverseMaps()

Ideally the names should match the names on other controllers where they make sense. For this controller the four buttons on the right side are labelled 1, 2, 3, 4. I could instead make them match the PS3 / PS4 controllers based on their position:

class ClearWired(Gamepad):
    fullName = 'Unbranded PC USB controller'

    def __init__(self, joystickNumber = 0):
        Gamepad.__init__(self, joystickNumber)
        self.axisNames = {
            0: 'LEFT-X',
            1: 'LEFT-Y',
            2: 'RIGHT-Y',
            3: 'RIGHT-X',
            4: 'DPAD-X',
            5: 'DPAD-Y'
        }
        self.buttonNames = {
            0:  'SQUARE',
            1:  'TRIANGLE',
            2:  'CROSS',
            3:  'CIRCLE',
            4:  'L1',
            5:  'L2',
            6:  'R1',
            7:  'R2',
            8:  'SELECT',
            9:  'START',
            10: 'L3',
            11: 'R3'
        }
        self._setupReverseMaps()

This last version makes it easier to swap scripts between Gamepad.PS4 and the new Gamepad.ClearWired without having to change any button or axis names.

Once done you can test the new controller by running ./Gamepad.py again and checking the names match the controls as intended.

Now you are ready to use your controller with Gamepad.

And finally

If you have any questions, improvements, or suggestions about Gamepad stop by our forum and let us know. You can also file bug reports directly on GitHub if you have found a problem.

We would also love it if anyone wants to help out with improving Gamepad. The entire library has been released with the open-source MIT license and is entirely written in Python. In particular it would be great if you have a controller type we do not have and can share the mapping for it :)

Last update: Oct 15, 2019

Related Article

Related Products

Comments

Leave a Comment

Leave a Reply

The product is currently Out-of-Stock. Enter your email address below and we will notify you as soon as the product is available.