Friday, 17 October 2014

Raspberry Pi, Xbox 360 Controller, Python

I was looking for a way of controlling my initio robot by remote and having purchased 3 xbox 360’s in the past (red ring of death :( replacements) I have got a few wireless controllers knocking about!  I decided re-using one of these was the way forward.

I grabbed a £5 xbox USB wireless receiver (you can get them on Amazon UK US) and robbed the code from zephods lego pi car project (http://blog.zephod.com/post/37120089376/lego-xbox-raspberry-pi) but I found it to be really unstable, often resulting in a ‘segmentation’ fault within a few minutes – so I decided to write my own.

Zephod’s code used the screen output of xboxdrv (a linux xbox controller driver) to create events which could be interpreted from python.  I decided on a different route and (after being shown the way by Dave Honess at Pycon) used pygame to interface with xboxdrv and the controller directly.

I originally just hacked together some code to make my solution work but after asking twitter whether anyone else would find it useful I created a generic python module to allow anyone to incorporate an xbox controller into their projects.


The module works by interpreting the pygame events which xboxdrv creates when the xbox controller is used (button pressed, button released, analogue stick moved, trigger pressed, etc) and keeps track of the values of all the buttons, sticks and triggers on the controller.

These values can be read directly from the properties on the class (e.g  xboxController.RTRIGGER) or the values can be passed to your program through the use of call backs i.e. when a button is pressed or a stick moved a function in your program is called and the details about what was changed and what the new value is are passed to it.

Installing and testing the module

If there is demand in the future I will wrap the module into a proper python package, but for the time being its just a separate python file (XboxController.py) which you can add to your python project.

Install xboxdrv
sudo apt-get install xboxdrv
Grab the code from GitHub (github.com/martinohanlon/XboxController) and copy the XboxController.py file to your project:
git clone https://github.com/martinohanlon/XboxController
cp XboxController/XboxContoller.py pathToYourProject
You need to run xboxdrv before you can use the module, run
sudo xboxdrv --silent &
You may get an error asking you to run xboxdrv with the option --detach-kernel-driver, if so run:
sudo xboxdrv --silent --detach-kernel-driver &
You can test the module by running the XboxController.py file
python XboxController.py
When you see the message on the screen saying the controller is running, press a button on your xbox controller.


Using the module

The module is pretty easy to use, but there are a few complex concepts to get your head around such as call backs and threading - first you need to import it into your code:
import XboxController
Then you can create an instance to the XboxController:
xboxCont = XboxController.XboxController(
    controllerCallBack = None,
    joystickNo = 0,
    deadzone = 0.1,
    scale = 1,
    invertYAxis = False)
You can adjust the behaviour of the module by passing different parameters:
  • joystickNo : specify the number of the pygame joystick you want to use, usually 0
  • deadzone : the minimum value which is reported from the analogue sticks, a deadzone is good to reduce sensitivity
  • scale : the scale the analogue sticks will report to, so 1 will mean values are returned between -1 and 1, 0 is always the middle
  • invertYAxis : the Y axis is reported as -1 being up and 1 being down, which is just weird, so this will invert it
  • controllerCallBack : pass the name of a function and the xbox controller will call this function each time the controller changes (i.e. a button is pressed) returning the id of the control (what button, stick or trigger) and the current value
To add a controller call back you would use:
def myCallBack(controlId, value):
    print "Control id = {}, Value = {}".format(controlId, value)

xboxCont = XboxController.XboxController(
    controllerCallBack = myCallBack)
You can also add other call back functions so that when specific buttons, sticks or triggers are pressed or moved it will call a specific function, e.g. to add a function which is called when the start button is pressed / released you would used the code:
def startButtonCallBack(value):
    print "Start button pressed / released"

xboxCont.setupControlCallback(
    xboxCont.XboxControls.START,
    startButtonCallBack)
The XboxController runs in its own thread, so you need to tell the controller to start using
xboxCont.start()
Control values can be read directly from the XboxController while it is running, by using the properties of the class e.g. to read the current value of the right trigger you would use:
print xboxCont.RTRIGGER
The XboxController also needs to be stopped at the end of your program using
xboxCont.stop()
For more information about the module, see the code in the the XboxController.py file.

49 comments:

  1. Hello Martin,

    just wanted to let you know that your module works like a charm and helped me undestand python in general and Callback functions in particular a great deal better. At the moment your module controls a GoPiGo robot but will soon also control a foam rocket launcher on top of the GoPiGo ;)

    Thanks for the great help you are providing with your code!

    ReplyDelete
    Replies
    1. Excellent news, please let me know how you get on.

      Delete
  2. Hi Martin,

    Do you know if this works with an Xbox one controller?

    Thanks,

    Steve

    ReplyDelete
    Replies
    1. I dont believe there is a linux driver for the xbox one controller yet so unfortunately not. I suspect its only a matter of time tho.

      Delete
  3. Hi Martin,

    thanks for the code, it works great! I am in the process of building a cnc mill and the joysticks control what I have so far (X stage and Z axis). Very satisfying to control something in real life with the xbox controller.

    Just one addition to your code - I had to add time.sleep(0.001) to your main loop. This way my main thread gets some processing time and prevents xboxCont from hogging it all.

    Thanks again,
    Will

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Sorry for the previous comment. I ended up just doing a fresh install of Rasbian using noobs, copied your stuff over, and got the script to work. When I ended up coding stuff on my own, CTRL + C wasn't killing things the way it should have - so I stole the code at the end of XboxController.py [with the try/catch stuff] and then everything was working flawlessly. Being able to programatically take input from the controller is flat out amazing, thank you for making it even easier. With this + relays + a vehichle mounted DTV dish, I'm going to probably get a bunch of fun looks when I'm controlling my dish with a wireless 360 controller

    ReplyDelete
    Replies
    1. Hey Douglas, I'm in the process of trying to write a software equivalent of CTRL + C right now. What exact commands or lines did you take from the XboxController.py script?

      Delete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Martin - will xboxcontroller.py work with ubuntu-xboxdrv on a pc-based Linux box? Thanks.

    ReplyDelete
    Replies
    1. I see no reason why not. Its only requirement is the pygame library. Providing that is installed, yes it should work.

      Delete
    2. Thank you Martin. I'll give it a try and let you know.

      Delete
  8. when i try the callback function
    "def startButtonCallBack(value):
    print "Start button pressed / released"

    xboxCont.setupControlCallback(
    self.xboxCont.XboxControls.START,
    startButtonCallBack)"
    then i get the error "NameError: name 'self' is not defined"

    ReplyDelete
    Replies
    1. You dont need the "self." - my bad.

      Post updated, thanks for letting me know.

      Delete
  9. How would you use these events to trigger a GPIO on the pi? I only want the pin to be HIGH while the joystick is in a position or a button is pressed.

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. Can someone post a If else example (trying to figure out how to use this am fairly new at this )

    ReplyDelete
    Replies
    1. if this == True:
      print("True")
      else:
      print("False")

      Delete
  12. ok I got the cod to work... Its awesome this is what i have:

    from rpi_serial import *
    import XboxController
    xboxCont = XboxController.XboxController(
    controllerCallBack = None,
    joystickNo = 0,
    deadzone = 0.1,
    scale = 1,
    invertYAxis = False)

    def LeftButtonCallBack(value):
    doMotorRun("M1",-120)
    doMotorRun("M2",120)

    xboxCont.setupControlCallback(
    xboxCont.XboxControls.LTRIGGER,
    LeftButtonCallBack)



    def RightButtonCallBack(value):
    doMotorRun("M1",120)
    doMotorRun("M2",-120)

    xboxCont.setupControlCallback(
    xboxCont.XboxControls.RTRIGGER,
    RightButtonCallBack)



    def UpButtonCallBack(value):
    doMotorRun("M1",120)
    doMotorRun("M2",120)

    xboxCont.setupControlCallback(
    xboxCont.XboxControls.Y,
    UpButtonCallBack)



    def DownButtonCallBack(value):
    doMotorRun("M1",-120)
    doMotorRun("M2",-120)

    xboxCont.setupControlCallback(
    xboxCont.XboxControls.X,
    DownButtonCallBack)


    def StopButtonCallBack(value):
    doMotorRun("M1",0)
    doMotorRun("M2",0)

    xboxCont.setupControlCallback(
    xboxCont.XboxControls.B,
    StopButtonCallBack)


    def AutoButtonCallBack(value):
    doMotorRun("M1",120)
    doMotorRun("M2",120)

    xboxCont.setupControlCallback(
    xboxCont.XboxControls.A,
    AutoButtonCallBack)


    xboxCont.start()

    print xboxCont.controlId

    xboxCont.stop()



    ====================================

    But how do I use the Joystick ? can i get a example for that Please it would be greatly appreciated...

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. The same way you have for the other controls but you will have to use

      xboxCont.XboxControl.LTHUMBX
      xboxCont.XboxControl.LTHUMBY
      xboxCont.XboxControl.RTHUMBX
      xboxCont.XboxControl.RTHUMBY

      Delete
    3. This is great ! just what I'm looking for.

      Delete
  13. Thank you, so Much...
    But how do you get the LTHUMBX Value into a variable that can be used to increases the motor speed ?

    like the 1 through 100 and the -1 through -100 in my code above M1 is for motor one and M2 for motor 2 but the 120 is hard coded speed what am trying to do is take the X and Y value turn it into a variable so that when you increase the joystick into a forward motion it will speed up the robot.

    ReplyDelete
  14. I got it to work by doing the following:


    def LeftButtonCallBack(value):
    speed = float("{0:.2f}".format(value))
    doMotorRun("M1",speed*200)
    doMotorRun("M2",speed*200)
    print "{}".format(speed*200)

    xboxCont.setupControlCallback(
    xboxCont.XboxControls.LTHUMBX,
    LeftButtonCallBack)

    ReplyDelete
  15. lol don't use the above code - I fried my board...

    Still a work in progress but this code will mess up your stuff..

    def LeftButtonCallBack(value):
    speed = float("{0:.2f}".format(value))
    doMotorRun("M1",speed*200)
    doMotorRun("M2",speed*200)
    print "{}".format(speed*200)

    xboxCont.setupControlCallback(
    xboxCont.XboxControls.LTHUMBX,
    LeftButtonCallBack)

    ReplyDelete
  16. there was smoke and fire... it was glorious lol, in the process of buying another board there 25 dollars each.. will not have one for about 3 weeks, will update on findings as soon as i can...

    ReplyDelete
    Replies
    1. O no...

      This might help, its the code i created to move my robot using the xbox controller (as seen in the video) https://github.com/martinohanlon/initio/blob/master/initioXboxControl.py

      Delete
  17. I got my new boards lol this is the error am getting when using the code you provided Thank you for your help by the way..


    started
    Unexpected error:

    stop
    Traceback (most recent call last):
    File "joystick.py", line 150, in
    if initioCont.running == True: initioCont.stop()
    NameError: name 'initioCont' is not defined

    ReplyDelete
    Replies
    1. It should have been defined before, i.e.

      initioCont = InitioXboxControl()

      https://github.com/martinohanlon/initio/blob/master/initioXboxControl.py#L140

      Delete
  18. Martin, I think I am confused as to how this needs to be setup. Is this a necessary initialization/does it format all events to act in such a way from now on? How does it affect anything if your callback is empty? I thought it was just a general 'this is how you would set up one, just make it more specific to the button/trigger and what you want it to do' type deal but someone posted their code with this at the beggining and you didn't mention anything about that being unnecessary.
    xboxCont = XboxController.XboxController(
    controllerCallBack = None,
    joystickNo = 0,
    deadzone = 0.1,
    scale = 1,
    invertYAxis = False)

    ReplyDelete
  19. Yes, it is neccessary to create the XboxController class i.e.:

    xboxcontroller = XboxController.XboxController()

    You then have to start it running:

    xboxcontroller.start()

    You then have a choice how you use it, either setting up callbacks for each button, trigger, stick or by reading the properties directly.

    Its also a good idea to stop it at the end of your program:

    xboxcontroller.stop()

    ReplyDelete
  20. This comment has been removed by the author.

    ReplyDelete
  21. Hey Martin thanks for all this code. I'm having one issue though, the code doesn't keep outputting values from the analog sticks once they stop moving, is there a way to fix this? I would like to find a way to keep polling the analog sticks and the triggers for their values.

    ReplyDelete
  22. Wait scratch that seems like that error is controller specific. Now the error I'm getting is with xboxCont.start (), it says there is a runtime error:threads can only be started once

    ReplyDelete
    Replies
    1. Based on the code you put in your comment:

      while True:
      xboxCont.start()
      xboxCont.stop()

      You are stopping and starting the controller continuously as you go round the loop.

      You need to start the controller once and stop it once.

      The invalid joystick id number could be because you are using id 1, normally the first joystick has the id 0.

      Delete
    2. Martin you are awesome. I wish I updated my question earlier. I tried id 0 and it didnt work initially so I switched it to 1 to check and forgot to change it back. I have an older controller that seems to have no issue. Last question though,
      (and I tried to look at your code before hand), it seems how to handle variable information coming in isnt as simple as confining it and outputting it within those parameters (I.E if..pwm=...led.start(pwm)).. because now I can get the controller to blink the led but not change brightness based on input.

      Delete
  23. I still haven't understand how can I set up conditions with this code. Where do I have to write "if" and "else" codes? and what do I have to write after "if" to verify whether a button is pressed or not?

    ReplyDelete
    Replies
    1. Sorry I dont really understand your question? Perhaps you need to go back to basics of python programming?

      Delete
  24. My friend I need to send the control to arduino but I can't to do it, I can't send de "value" from the xbox controller botons

    ReplyDelete
    Replies
    1. How are you sending the data to the arduino? Serial? The values from the buttons are just numbers, you shouldnt have too much trouble turning the values into strings to send.

      Delete
    2. yes, I did it, It was easy to do it, but Now I can't to do that Arduino read always correct values for the buttons,

      Delete
  25. New to this and this is exactly what i want to do, control my bot with my xbox controller. i followed the page, when i run the XboxContoller.py i get the following error

    Traceback (most recent call last):
    File "XboxController.py", line 360, in
    xboxCont = XboxController(controlCallBack, deadzone = 30, scale = 100, invertYAxis = True)
    File "XboxController.py", line 163, in __init__
    self._setupPygame(joystickNo)
    File "XboxController.py", line 251, in _setupPygame
    joy = pygame.joystick.Joystick(joystickNo)
    pygame.error: Invalid joystick device number

    when i run the driver i see the controller

    Controller: Microsoft Xbox 360 Wireless Controller (PC)
    Vendor/Product: 045e:0719
    USB Path: 001:004
    Wireless Port: 0
    Controller Type: Xbox360 (wireless)

    Your Xbox/Xbox360 controller should now be available as:
    /dev/input/js0
    /dev/input/event2


    Thanks

    ReplyDelete
    Replies
    1. Perhaps try a different joystick no. pygame might be picking it up as a different id.

      i.e.

      XboxController(joystickNo = 1)

      Delete
    2. Im using an Adafruit MotorHat, any suggestions how to use it? I wanted to avoid using a breadboard

      Delete
  26. Update to python-pygame worked for me.
    THanks

    ReplyDelete
  27. I beat my head against the wall on this for a couple of hours only to realize that I needed to type sudo python XboxController.py... may want to change the how to above :)
    AC

    ReplyDelete
    Replies
    1. Ok. You shouldnt need to though. I wonder what is different about your install?!

      Delete
  28. Thank you for writing this code, it has helped me a lot with my remote control car project!

    I wanted to let you know that I ran into some hiccups with the program. Some buttons were not reacting correctly. For instance, when I activated the right thumb x axis on my xbox controller, the program thought I had activated the y axis. After some troubleshooting, I concluded my controller has different Control IDs than the program expected. My Control IDs are as follows:

    Left Trigger = 2 (program expected 5)
    Right Trigger = 5 (program expected 4)
    Right Thumb Y = 4 (program expected 3)
    Right Thumb X = 3 (program expected 2)

    When I went into the XboxController.py file and changed the Control ID numbers in the XboxControls class, I didn’t see a change. But when I changed them in the PyGameAxis class, the problem was resolved.

    I’m using a Raspberry Pi 3 running Raspbian GNU/Linux 8 (jessie), an Xbox 360 wireless controller, and a PC Wireless Gaming Receiver.

    I’m not a very experienced programmer. It’s possible I’ve botched something during this process. If you have time, I would be grateful if you could take a look at this and let me know if you see the same results.

    ReplyDelete
    Replies
    1. It looks like pygame was detecting the xbox 360 controller as a different joystick to when I created the class.

      Its entirely possible, drivers change, different software stacks, updates, etc.

      You changed the right piece of code, the PygameAxis is the mapping between the id's pygame returns and a internal description the code uses.

      Delete