Control Your Marty With A Gamepad Using Python
Control your Marty with a GamePad using Python
What You'll Need
The Logitech F710 GamepadJust as a side-note, if you're running Marty with a Raspberry Pi and ROS installed, then there's a ROS-based Joystick controller for Marty here you can just download and use
We're using a Logitech F710 wireless Gamepad here, but most USB gamepads should work. Drivers and supported features might be different between different controllers, and some bluetooth controllers might work too. Something that looks like a 'normal' console controller with plenty of buttons, joysticks and triggers is ideal!
And you'll also need a Marty running with our current hardware (i.e. a Rick) - Though if you're running the way-old I2C Servo Board + Raspberry Pi combination, it is possible to adapt this example to work there too.
Lastly, you need to have a working Python install (preferably Python 3) on your Computer/Raspberry Pi. If you've never used Python before then we'd recommend first running through a basic Python tutorial to get you well acquainted with Python.
First Step - Set up a new Python Project
The first thing we'll need to do is create a project folder to work in, something like martypy-joystick
. Now we should install a few things in the folder; the commands below are for doing this in a Terminal (RPi/Linux/Mac) or Command Prompt/cmd (Windows).
We'll create an environment for our project to run in, called a virtual environment, or venv for short. This lets us choose exactly what version of Python and which libraries we want to use, separately from the ones that already come installed on your system.
Windows;
1 2 3 4
MKDIR martypy-joystick CHDIR martypy-joystick python -m venv .\VENV .\VENV\Scripts\activate.bat
Raspberry Pi, Mac and Linux;
1 2 3 4
mkdir martypy-joystick cd martypy-joystick python3 -m venv ./VENV source ./VENV/bin/activate
Next, we need to install some Libraries that contain some Python code we can use to help us work with the Game controller and Marty.
As with the MartyPy Introduction, we then need to install the latest version of martypy
, Robotical's Python library for Marty, and inputs
, an aptly named library for using Input Devices like Gamepads, Mice and Keyboards:
(VENV) $ pip install martypy
(VENV) $ pip install inputs
Note: If you use IDLE, Python's built-in IDE, you can start it in your venv like so, again from the command line:
(VENV) $ python -m idlelib.idle
Next Step - Getting the controller working
Our first task is going to be getting python to respond and do stuff when we press buttons and move controls on the Gamepad. We'll make a Python file, called joystick.py
in the Project folder we made earlier. Open it in a code editor like IDLE or Atom
Import the inputs
library we just installed, then, we'll ask it to tell us what Gamepads it can see plugged in to our computer:
1 2 3
import inputs print(inputs.devices.gamepads)
Run the Python file (press F5 in IDLE), which will print something that looks like this to the terminal:
(VENV) $ python ./joystick.py
[inputs.GamePad("/dev/input/by-id/usb-Logitech_Wireless_Gamepad_F710_4C089F2D-event-joystick")]
or
(VENV) >python .\joystick.py
[inputs.GamePad("/dev/input/by_id/usb-Microsoft_Corporation_Controller_0-event-joystick")]
If instead you just see []
, then check you've got the Gamepad plugged in and turned-on correctly.
We can also ask inputs
about all the USB devices currently plugged in with inputs.devices.all_devices
, but here we're only interested in Gamepads.
So, when we start our program it'll need to start using a Gamepad, so to check that there is one available, replace the print
line with this:
1 2 3 4
pads = inputs.devices.gamepads if len(pads) == 0: raise Exception("Couldn't find any Gamepads!")
This is just checking that there is at least one Gamepad available, and will exit the Python program if one can't be found. You can check that it works by unplugging the Gamepad from your computer, which will cause the Exception we just created. If the Gamepad is plugged in correctly, the program should run fine without error.
Getting events from the Gamepad
The inputs library can give us information about buttons being pressed and triggers and thumb-sticks being moved in things called events. To see what these look like, we can add a bit of code:
1 2 3 4
while True: events = inputs.get_gamepad() for event in events: print(event.ev_type, event.code, event.state)
The while True:
makes sure this code will run forever, or at least until you manually stop the program. Within the while
loop, we get the latest events from the Gamepad and print them to the terminal so we can see them.
If you then run the joystick.py
file again and press a couple of buttons on your controller, you should see a load of events stream in as you mess with the controller, looking something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Key BTN_SOUTH 1 Sync SYN_REPORT 0 Key BTN_SOUTH 0 Sync SYN_REPORT 0 Absolute ABS_RZ 8 Sync SYN_REPORT 0 Absolute ABS_RZ 26 Sync SYN_REPORT 0 Absolute ABS_RZ 93 Sync SYN_REPORT 0 Absolute ABS_RZ 12 Sync SYN_REPORT 0 Absolute ABS_RZ 0 Sync SYN_REPORT 0 Absolute ABS_HAT0Y -1 Sync SYN_REPORT 0 Absolute ABS_HAT0Y 0 Sync SYN_REPORT 0 ...
This might look a bit scary, but look at the print
statement from our code -- the first bit is event.ev_type
, which has come out as either Absolute
or Sync
for this Gamepad, and the second bit is event.code
which tells us which button or joystick was touched - ABS_HAT0Y
just means that the Hat #0 (i.e. the first, and only Hat on this pad) moved to -1
in the Y
direction and then back to 0
when the button was released. You can have a play with all the buttons at this stage to see what name they are given by inputs
and the range of values you can expect from them.
Make Marty do something!
We'll start with something simple, making Marty dance when we press a button. We'll use the same loop we had before, but now check for the button press we want - on this controller, the A button shows up as BTN_SOUTH
.
We'll need to establish a connection with Marty, so first we need to add some Python code to get that going:
1 2 3 4 5 6 7
# Add these lines at the top of the file: import martypy #... # Add this line just above the while loop: marty = martypy.Marty(url='socket://192.168.0.42') # <<--- REPLACE with the correct URL
You can find out what the IP address (the 192.168.0.42
bit) for your Marty is using a scanning tool, like our Simple Scanner or Angry IP
The above bit of code tries to connect to a Marty using a URL, like socket://192.168.0.42
to connect to Marty. If martypy
can't connect toy your Marty, it will raise an Exception.
Remember, with Python and most programming languages, it's good practice to always keep your imports at the top of your file, so add those there and then add all your code below.
Right, so now we can add the following in the the for
loop to make Marty celebrate when the A
button on the controller is hit!
1 2 3
if event.code == 'BTN_SOUTH' and event.state == 1: print('Celebrate!') marty.celebrate()
So, to recap, our whole program should now look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import inputs import martypy pads = inputs.devices.gamepads if len(pads) == 0: raise Exception("Couldn't find any Gamepads!") marty = martypy.Marty(url='socket://192.168.0.42') # <<--- REPLACE with the correct URL while True: events = inputs.get_gamepad() for event in events: print(event.ev_type, event.code, event.state) if event.code == 'BTN_SOUTH' and event.state == 1: print('Celebrate!') marty.celebrate()
From here, you can check for more Gamepad events and make Marty do practically whatever you want!
So, I'll leave you to it -- but if you want, here's a program we made earlier that has functions for most of the buttons and joysticks that you can download from a GitHub Gist. It also has a few added features, such as needing to give the URL to Marty as a command line argument.
You can run this version like so:
(VENV) $ python joystick.py socket://192.168.0.42
Let us know how you got on!
Post in the Forum or Send us a tweet at @RoboticalLtd!
Got stuck or need a hand? Start a chat with Support…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
import sys import time import inputs import martypy try: # Get pretty coloured text - $ pip install colored from colored import fg, bg, attr except ImportError: # Fall-back to no colours: print("Psst! Type 'pip install colored' to get coloured output text") def fg(*args, **kwargs): return '' def bg(*args, **kwargs): return '' def attr(*args, **kwargs): return '' # Create this here to prevent name errors try: marty = martypy.Marty(url=sys.argv[1]) marty.enable_safeties(False) marty.motor_protection(False) except IndexError as e: raise Exception("{}You need to give the url for Marty as an argument{}" "".format(fg('red'), attr('reset'))) from e def stretch(x, in_min, in_max, out_min, out_max): x, in_min, in_max, out_min, out_max = map(float, (x, in_min, in_max, out_min, out_max)) return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; def reset(state): if state == 0: print('{}Cleared & Zeroed{}'.format(fg('red'), attr('reset'))) marty.enable_motors() marty.enable_safeties() marty.stop('clear and zero') marty.enable_safeties(False) marty.motor_protection(False) print("{}{} Volts{}".format(fg('blue'), marty.get_battery_voltage(), attr('reset'))) def hello(state): if state == 0: print('{}Ready!{}'.format(fg('green'), attr('reset'))) marty.hello() def celebrate(state): if state == 1: print('{}Celebrate!{}'.format(fg('green'), attr('reset'))) marty.celebrate(2000) def circledance(state): if state == 1: print('{}Dance!{}'.format(fg('green'), attr('reset'))) marty.circle_dance('left', 600) marty.circle_dance('left', 600) arm_pos = { 'left' : -10, 'right' : -10, } arm_last_move = 0 def arm_left(state): global arm_pos global arm_last_move val = round(stretch(state, 0, 255, -10, -127)) if abs(val - arm_pos['left']) > 8 or time.time() - arm_last_move > 0.5: arm_pos['left'] = val arm_last_move = time.time() marty.arms(val, arm_pos['right'], 0) def arm_right(state): global arm_pos global arm_last_move val = round(stretch(state, 0, 255, -10, -127)) if abs(val - arm_pos['right']) > 8 or time.time() - arm_last_move > 0.5: arm_pos['right'] = val arm_last_move = time.time() marty.arms(arm_pos['left'], val, 0) def kick_left(state): if state == 1: print('{}Kick left{}'.format(fg('green'), attr('reset'))) marty.kick('left', move_time=1800) def kick_right(state): if state == 1: print('{}Kick right{}'.format(fg('green'), attr('reset'))) marty.kick('right', move_time=1800) def eyes(state): print('{}0_o{}'.format(fg('green'), attr('reset'))) val = round(stretch(state, -33000, 33000, -128, 127)) marty.eyes(val, 0) def sidestep(state): if state == 1: print('{}Slide to the Right{}'.format(fg('green'), attr('reset'))) marty.sidestep('right', 1, 75, 800) elif state == -1: print('{}Slide to the Left{}'.format(fg('green'), attr('reset'))) marty.sidestep('left', 1, 75, 800) oldlean = 0 def lean(state): global oldlean lean = round(stretch(state, -33000, 33000, 63, -64)) #marty.move_joint(0, lean, 0) #marty.move_joint(3, lean, 0) diff = abs(oldlean - lean) if diff > 5: print('{}Lean{}'.format(fg('green'), attr('reset'))) move_time = int(20 + diff * 2) marty.lean('forward', lean, move_time) oldlean = lean #time.sleep(move_time / 1000) lastwalk = 0 def walk(state, threshold): global lastwalk if abs(state) > threshold and time.time() - lastwalk >= 1: print('{}Walk{}'.format(fg('green'), attr('reset'))) lastwalk = time.time() marty.walk(num_steps=1, start_foot='auto', turn=0, step_length=50 if state < 0 else -45, move_time=1000) lastturn = 0 def turn(state): global lastturn if abs(state) > 23000 and time.time() - lastturn >= 1.2: if state < -1: print('{}Turning Left{}'.format(fg('green'), attr('reset'))) marty.walk(1, 'auto', 127, 0, 1200) if state > 1: print('{}Turning Right{}'.format(fg('green'), attr('reset'))) marty.walk(1, 'auto', -128, 0, 1200) lastwalk = time.time() lastwave = 0 def wave(_, pos): global lastwave, arm_pos if time.time() - lastwave >= 0.6: print('{}Wave!{}'.format(fg('green'), attr('reset'))) if pos == 'right': marty.eyes(-75, 100) marty.arms(127, arm_pos['right'], 150) marty.arms(-10, arm_pos['right'], 150) marty.arms(127, arm_pos['right'], 150) marty.arms(-10, arm_pos['right'], 150) marty.eyes(0, 200) else: marty.eyes(-75, 100) marty.arms(arm_pos['left'], 127, 150) marty.arms(arm_pos['left'], -10, 150) marty.arms(arm_pos['left'], 127, 150) marty.arms(arm_pos['left'], -10, 150) marty.eyes(0, 200) lastwave = time.time() ''' Set which actions happen when buttons and joysticks change: ''' event_lut = { 'BTN_MODE': reset, 'BTN_START' : hello, 'BTN_NORTH' : lambda z: wave(z, 'right'), 'BTN_SOUTH' : celebrate, 'BTN_EAST' : circledance, 'BTN_WEST' : lambda z: wave(z, 'left'), 'BTN_TR' : kick_right, 'BTN_TL' : kick_left, 'BTN_THUMBR' : kick_right, 'BTN_THUMBL' : kick_left, 'ABS_Z' : arm_left, 'ABS_RZ' : arm_right, 'ABS_X' : turn, 'ABS_Y' : lambda x: walk(x, 23000), 'ABS_RX' : eyes, 'ABS_RY' : None, #lean, 'ABS_HAT0X': sidestep, 'ABS_HAT0Y': lambda x: walk(x, 0.5), } def event_loop(events): ''' This function is called in a loop, and will get the events from the controller and send them to the functions we specify in the `event_lut` dictionary ''' for event in events: print('\t', event.ev_type, event.code, event.state) call = event_lut.get(event.code) if callable(call): call(event.state) if __name__ == '__main__': ''' This part of the probram is run when we are the main process ''' pads = inputs.devices.gamepads if len(pads) == 0: raise Exception("{}Couldn't find any Gamepads!{}".format(fg('red'), attr('reset'))) reset(0) try: while True: event_loop(inputs.get_gamepad()) except KeyboardInterrupt: print("Bye!")