Capture input from an Xbox Controller in Python on Mac OS X

As I’ve been hustling away at the blimp project, one of the most daunting challenges that I’m going to face is to deal with creating a user interface for the client application associated with the blimp. But then, out of nowhere, I had an epiphary. Using existing techmology, if I could offload the majority of a user interface to an easily accessible device like an XBox Controller, then the remaining tasks to place on the UI are minimal.  Moreover, building and testing the blimp would suddenly become cool if I could do a sufficient amount of testing using a game controller (flying quadrocopter in the office THIS WEEK!).

I subsequently starting googling pretty hard.  I found that there were off the shelf drivers for Windows, but I didn’t want to be caught on the streets writing a program for Windows.  Also I don’t have a Windows computer. Most of what I found pointed me toward PyGame for Mac OS X, but not only did it seem like a pain to install to begin with, but I was unable to install it after a decent amount of effort.

Then I found SDL, but it didn’t have any Python bindings. But only yesterday, I’d written about how to call C++ functions and methods from Python. After downloading SDL, I was up and running in a matter of minutes.

I’ve posted the sample code to a GitHub repository. Simply install SDL and Boost, adjust the Makefile to your particular environment, and then you can run the sample python program.

One of the slightly interesting aspects about this small project was to deal with a function that returned two different items.  In C++ you’d normally accomplish this by passing variables by reference, but you can’t count on that when you’re calling those functions from Python.  Instead, I wrote a small helper function that returns a Python tuple from C++.  See below:

boost::python::tuple items_to_tuple(int event_id, int value){
    boost::python::list python_list;
    python_list.append(event_id);
    python_list.append(value);
    return boost::python::tuple(python_list);
}

To walk through the rest of the code, the basic C++ functionality is like so:

boost::python::tuple XBoxControllerManager::get_next_event(){
    if(!this->controller_ready){
        return items_to_tuple(0, 0);
    }
    bool event_exists = SDL_PollEvent(&this->event);
    if (!event_exists){
        return items_to_tuple(0, 0);
    }
    switch(event.type){
        case SDL_JOYAXISMOTION:  /* Handle Joystick Motion */
            if ( ( event.jaxis.value < -3200 ) || (event.jaxis.value > 3200 ) ){
                int joystick_value = event.jaxis.value;
                if( event.jaxis.axis == 0) {
                    return items_to_tuple(LEFT_THUMBSTICK_HORIZONTAL, joystick_value);
                }
                else if( event.jaxis.axis == 1) {
                    return items_to_tuple(LEFT_THUMBSTICK_VERTICAL, joystick_value);
                }
                else if( event.jaxis.axis == 2){
                    return items_to_tuple(RIGHT_THUMBSTICK_HORIZONTAL, joystick_value);
                }
                else if( event.jaxis.axis == 3){
                    return items_to_tuple(RIGHT_THUMBSTICK_VERTICAL, joystick_value);
                }
            }
            break;
        case SDL_JOYBUTTONUP:
            return items_to_tuple(BUTTON_RELEASED, event.jbutton.button);
            break;
        case SDL_JOYBUTTONDOWN:  /* Handle Joystick Button Presses */
            return items_to_tuple(BUTTON_PRESSED, event.jbutton.button);
            break;
    }
    return items_to_tuple(0, 0);
}

Keep in mind there are also some functions to initialize SDL that I’m not posting here, but they are in the sample code on Github. The code above can then be ported over to Python in two steps that I like. 1.) Create a manager class that manages the objects that are specific to SDL so that we don’t have to create custom PyObjects for them and 2.) Create python bindings using boost. The manager class looks something like this:

class XBoxControllerManager{
    private:
        SDL_Joystick *xbox_controller;
        bool controller_ready;
        SDL_Event event;
    public:
        XBoxControllerManager();
        ~XBoxControllerManager();
        bool controller_is_ready(){ return controller_ready; }
        bool initialize();
        boost::python::tuple get_next_event();

};

Finally, we need to expose that class to Python using boost. After you’ve done this once, it’s extremely easy and straightforward:

using namespace boost::python;
BOOST_PYTHON_MODULE(boost_xbox_controller) // this parameter needs to match filename
{
    class_<XBoxControllerManager>("XBoxControllerManager")
        .def("initialize", &XBoxControllerManager::initialize)
        .def("controller_is_ready", &XBoxControllerManager::controller_is_ready)
        .def("get_next_event", &XBoxControllerManager::get_next_event);
}

Now, in python, the usage is incredibly simple:

from boost_xbox_controller import XBoxControllerManager

xbox_controller_manager = XBoxControllerManager()
success = xbox_controller_manager.initialize()

if success:
    while True:
        event_id, value = xbox_controller_manager.get_next_event()
        # event_id represents the type of action, i.e. left thumbstick
        # horizontal move

        # value represents the value associated with the action

That’s it!

  • Pingback: For moog | Audio Sketches()

  • BonzaiRob

    I’ve had some problems getting this to work – I’ll post my workings here, so anyone who wants to try it from scratch can follow along (as much as so I can get some help with a linking problem…)

    Other than the xbox controller driver – http://tattiebogle.net/index.php/ProjectRoot/Xbox360Controller/OsxDriver – I used homebrew to install all the parts:

    brew install python2.7
    brew install sdl
    brew install gcc (for the fortran libs – expect this one to take hours to compile 🙁 )
    brew install boost –with-python
    brew install boost-python –with-python

    As of writing the homebrew boost-python’s download link is broken (it complains about SHA mismatch and is about 4 bytes), so head over to sourceforge, grab the .tar.bz2 of the homebrew version, and replace the one in /Library/Caches/Homebrew.

    The Makefile then needed some adjustment for brew locations; I also had to remove the “-Wl,” flag as this was apparently trying to link to lib.dylib: http://pastebin.com/fVKrSS15

    Now, the .so file is generated, but in python, I get the following error:
    File “sample_python_program.py”, line 3, in
    xbox_controller_manager = XBoxControllerManager()
    TypeError: __init__() should return None, not ‘NoneType’

    From what I can tell this means boost hasn’t linked up to the right python version – it might be using system instead of homebrew. Having looked at the params for boost and boost-python on homebrew, it seems that –with-python isn’t actually an option. otool -L on the boost libs doesn’t mention python either. What can I do?