Learning Portal

Our hands-on, comprehensive lesson plans span a range of levels. Browse our free STEM and coding learning resources.

Marty Image

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!")