├── .gitignore ├── LICENSE ├── README.md └── xinput.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2009 Jason R. Coombs 4 | Copyright (c) 2014 r4dian 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Xbox-Controller-for-Python 2 | ============================== 3 | 4 | Use an Xbox 360 /XBone Controller with Python on Windows 5 | 6 | A module for getting input from Microsoft XBox controllers via the XInput library on Windows. 7 | 8 | Adapted from Jason R. Coombs' code here: 9 | http://pydoc.net/Python/jaraco.input/1.0.1/jaraco.input.win32.xinput/ 10 | under the MIT licence terms 11 | 12 | * Upgraded to Python 3 13 | * Modified to add deadzones, reduce noise, and support vibration 14 | * Only req is Pyglet ~1.2alpha1 or higher~ confirmed to work with 1.2.4 (latest): ```pip install pyglet``` 15 | -------------------------------------------------------------------------------- /xinput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | A module for getting input from Microsoft XBox 360 controllers via the XInput library on Windows. 5 | 6 | Adapted from Jason R. Coombs' code here: 7 | http://pydoc.net/Python/jaraco.input/1.0.1/jaraco.input.win32.xinput/ 8 | under the MIT licence terms 9 | 10 | Upgraded to Python 3 11 | Modified to add deadzones, reduce noise, and support vibration 12 | Only req is Pyglet 1.2alpha1 or higher: 13 | pip install --upgrade http://pyglet.googlecode.com/archive/tip.zip 14 | """ 15 | 16 | import ctypes 17 | import sys 18 | import time 19 | from operator import itemgetter, attrgetter 20 | from itertools import count, starmap 21 | from pyglet import event 22 | 23 | # structs according to 24 | # http://msdn.microsoft.com/en-gb/library/windows/desktop/ee417001%28v=vs.85%29.aspx 25 | 26 | 27 | class XINPUT_GAMEPAD(ctypes.Structure): 28 | _fields_ = [ 29 | ('buttons', ctypes.c_ushort), # wButtons 30 | ('left_trigger', ctypes.c_ubyte), # bLeftTrigger 31 | ('right_trigger', ctypes.c_ubyte), # bLeftTrigger 32 | ('l_thumb_x', ctypes.c_short), # sThumbLX 33 | ('l_thumb_y', ctypes.c_short), # sThumbLY 34 | ('r_thumb_x', ctypes.c_short), # sThumbRx 35 | ('r_thumb_y', ctypes.c_short), # sThumbRy 36 | ] 37 | 38 | 39 | class XINPUT_STATE(ctypes.Structure): 40 | _fields_ = [ 41 | ('packet_number', ctypes.c_ulong), # dwPacketNumber 42 | ('gamepad', XINPUT_GAMEPAD), # Gamepad 43 | ] 44 | 45 | 46 | class XINPUT_VIBRATION(ctypes.Structure): 47 | _fields_ = [("wLeftMotorSpeed", ctypes.c_ushort), 48 | ("wRightMotorSpeed", ctypes.c_ushort)] 49 | 50 | class XINPUT_BATTERY_INFORMATION(ctypes.Structure): 51 | _fields_ = [("BatteryType", ctypes.c_ubyte), 52 | ("BatteryLevel", ctypes.c_ubyte)] 53 | 54 | xinput = ctypes.windll.xinput1_4 55 | #xinput = ctypes.windll.xinput9_1_0 # this is the Win 8 version ? 56 | # xinput1_2, xinput1_1 (32-bit Vista SP1) 57 | # xinput1_3 (64-bit Vista SP1) 58 | 59 | 60 | def struct_dict(struct): 61 | """ 62 | take a ctypes.Structure and return its field/value pairs 63 | as a dict. 64 | 65 | >>> 'buttons' in struct_dict(XINPUT_GAMEPAD) 66 | True 67 | >>> struct_dict(XINPUT_GAMEPAD)['buttons'].__class__.__name__ 68 | 'CField' 69 | """ 70 | get_pair = lambda field_type: ( 71 | field_type[0], getattr(struct, field_type[0])) 72 | return dict(list(map(get_pair, struct._fields_))) 73 | 74 | 75 | def get_bit_values(number, size=32): 76 | """ 77 | Get bit values as a list for a given number 78 | 79 | >>> get_bit_values(1) == [0]*31 + [1] 80 | True 81 | 82 | >>> get_bit_values(0xDEADBEEF) 83 | [1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 0L, 1L, 0L, 1L, 1L, 0L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L] 84 | 85 | You may override the default word size of 32-bits to match your actual 86 | application. 87 | >>> get_bit_values(0x3, 2) 88 | [1L, 1L] 89 | 90 | >>> get_bit_values(0x3, 4) 91 | [0L, 0L, 1L, 1L] 92 | """ 93 | res = list(gen_bit_values(number)) 94 | res.reverse() 95 | # 0-pad the most significant bit 96 | res = [0] * (size - len(res)) + res 97 | return res 98 | 99 | 100 | def gen_bit_values(number): 101 | """ 102 | Return a zero or one for each bit of a numeric value up to the most 103 | significant 1 bit, beginning with the least significant bit. 104 | """ 105 | number = int(number) 106 | while number: 107 | yield number & 0x1 108 | number >>= 1 109 | 110 | ERROR_DEVICE_NOT_CONNECTED = 1167 111 | ERROR_SUCCESS = 0 112 | 113 | 114 | class XInputJoystick(event.EventDispatcher): 115 | 116 | """ 117 | XInputJoystick 118 | 119 | A stateful wrapper, using pyglet event model, that binds to one 120 | XInput device and dispatches events when states change. 121 | 122 | Example: 123 | controller_one = XInputJoystick(0) 124 | """ 125 | max_devices = 4 126 | 127 | def __init__(self, device_number, normalize_axes=True): 128 | values = vars() 129 | del values['self'] 130 | self.__dict__.update(values) 131 | 132 | super(XInputJoystick, self).__init__() 133 | 134 | self._last_state = self.get_state() 135 | self.received_packets = 0 136 | self.missed_packets = 0 137 | 138 | # Set the method that will be called to normalize 139 | # the values for analog axis. 140 | choices = [self.translate_identity, self.translate_using_data_size] 141 | self.translate = choices[normalize_axes] 142 | 143 | def translate_using_data_size(self, value, data_size): 144 | # normalizes analog data to [0,1] for unsigned data 145 | # and [-0.5,0.5] for signed data 146 | data_bits = 8 * data_size 147 | return float(value) / (2 ** data_bits - 1) 148 | 149 | def translate_identity(self, value, data_size=None): 150 | return value 151 | 152 | def get_state(self): 153 | "Get the state of the controller represented by this object" 154 | state = XINPUT_STATE() 155 | res = xinput.XInputGetState(self.device_number, ctypes.byref(state)) 156 | if res == ERROR_SUCCESS: 157 | return state 158 | if res != ERROR_DEVICE_NOT_CONNECTED: 159 | raise RuntimeError( 160 | "Unknown error %d attempting to get state of device %d" % (res, self.device_number)) 161 | # else return None (device is not connected) 162 | 163 | def is_connected(self): 164 | return self._last_state is not None 165 | 166 | @staticmethod 167 | def enumerate_devices(): 168 | "Returns the devices that are connected" 169 | devices = list( 170 | map(XInputJoystick, list(range(XInputJoystick.max_devices)))) 171 | return [d for d in devices if d.is_connected()] 172 | 173 | def set_vibration(self, left_motor, right_motor): 174 | "Control the speed of both motors seperately" 175 | # Set up function argument types and return type 176 | XInputSetState = xinput.XInputSetState 177 | XInputSetState.argtypes = [ctypes.c_uint, ctypes.POINTER(XINPUT_VIBRATION)] 178 | XInputSetState.restype = ctypes.c_uint 179 | 180 | vibration = XINPUT_VIBRATION( 181 | int(left_motor * 65535), int(right_motor * 65535)) 182 | XInputSetState(self.device_number, ctypes.byref(vibration)) 183 | 184 | def get_battery_information(self): 185 | "Get battery type & charge level" 186 | BATTERY_DEVTYPE_GAMEPAD = 0x00 187 | BATTERY_DEVTYPE_HEADSET = 0x01 188 | # Set up function argument types and return type 189 | XInputGetBatteryInformation = xinput.XInputGetBatteryInformation 190 | XInputGetBatteryInformation.argtypes = [ctypes.c_uint, ctypes.c_ubyte, ctypes.POINTER(XINPUT_BATTERY_INFORMATION)] 191 | XInputGetBatteryInformation.restype = ctypes.c_uint 192 | 193 | battery = XINPUT_BATTERY_INFORMATION(0,0) 194 | XInputGetBatteryInformation(self.device_number, BATTERY_DEVTYPE_GAMEPAD, ctypes.byref(battery)) 195 | 196 | #define BATTERY_TYPE_DISCONNECTED 0x00 197 | #define BATTERY_TYPE_WIRED 0x01 198 | #define BATTERY_TYPE_ALKALINE 0x02 199 | #define BATTERY_TYPE_NIMH 0x03 200 | #define BATTERY_TYPE_UNKNOWN 0xFF 201 | #define BATTERY_LEVEL_EMPTY 0x00 202 | #define BATTERY_LEVEL_LOW 0x01 203 | #define BATTERY_LEVEL_MEDIUM 0x02 204 | #define BATTERY_LEVEL_FULL 0x03 205 | batt_type = "Unknown" if battery.BatteryType == 0xFF else ["Disconnected", "Wired", "Alkaline","Nimh"][battery.BatteryType] 206 | level = ["Empty", "Low", "Medium", "Full"][battery.BatteryLevel] 207 | return batt_type, level 208 | 209 | def dispatch_events(self): 210 | "The main event loop for a joystick" 211 | state = self.get_state() 212 | if not state: 213 | raise RuntimeError( 214 | "Joystick %d is not connected" % self.device_number) 215 | if state.packet_number != self._last_state.packet_number: 216 | # state has changed, handle the change 217 | self.update_packet_count(state) 218 | self.handle_changed_state(state) 219 | self._last_state = state 220 | 221 | def update_packet_count(self, state): 222 | "Keep track of received and missed packets for performance tuning" 223 | self.received_packets += 1 224 | missed_packets = state.packet_number - \ 225 | self._last_state.packet_number - 1 226 | if missed_packets: 227 | self.dispatch_event('on_missed_packet', missed_packets) 228 | self.missed_packets += missed_packets 229 | 230 | def handle_changed_state(self, state): 231 | "Dispatch various events as a result of the state changing" 232 | self.dispatch_event('on_state_changed', state) 233 | self.dispatch_axis_events(state) 234 | self.dispatch_button_events(state) 235 | 236 | def dispatch_axis_events(self, state): 237 | # axis fields are everything but the buttons 238 | axis_fields = dict(XINPUT_GAMEPAD._fields_) 239 | axis_fields.pop('buttons') 240 | for axis, type in list(axis_fields.items()): 241 | old_val = getattr(self._last_state.gamepad, axis) 242 | new_val = getattr(state.gamepad, axis) 243 | data_size = ctypes.sizeof(type) 244 | old_val = self.translate(old_val, data_size) 245 | new_val = self.translate(new_val, data_size) 246 | 247 | # an attempt to add deadzones and dampen noise 248 | # done by feel rather than following http://msdn.microsoft.com/en-gb/library/windows/desktop/ee417001%28v=vs.85%29.aspx#dead_zone 249 | # ags, 2014-07-01 250 | if ((old_val != new_val and (new_val > 0.08000000000000000 or new_val < -0.08000000000000000) and abs(old_val - new_val) > 0.00000000500000000) or 251 | (axis == 'right_trigger' or axis == 'left_trigger') and new_val == 0 and abs(old_val - new_val) > 0.00000000500000000): 252 | self.dispatch_event('on_axis', axis, new_val) 253 | 254 | def dispatch_button_events(self, state): 255 | changed = state.gamepad.buttons ^ self._last_state.gamepad.buttons 256 | changed = get_bit_values(changed, 16) 257 | buttons_state = get_bit_values(state.gamepad.buttons, 16) 258 | changed.reverse() 259 | buttons_state.reverse() 260 | button_numbers = count(1) 261 | changed_buttons = list( 262 | filter(itemgetter(0), list(zip(changed, button_numbers, buttons_state)))) 263 | tuple(starmap(self.dispatch_button_event, changed_buttons)) 264 | 265 | def dispatch_button_event(self, changed, number, pressed): 266 | self.dispatch_event('on_button', number, pressed) 267 | 268 | # stub methods for event handlers 269 | def on_state_changed(self, state): 270 | pass 271 | 272 | def on_axis(self, axis, value): 273 | pass 274 | 275 | def on_button(self, button, pressed): 276 | pass 277 | 278 | def on_missed_packet(self, number): 279 | pass 280 | 281 | list(map(XInputJoystick.register_event_type, [ 282 | 'on_state_changed', 283 | 'on_axis', 284 | 'on_button', 285 | 'on_missed_packet', 286 | ])) 287 | 288 | 289 | def determine_optimal_sample_rate(joystick=None): 290 | """ 291 | Poll the joystick slowly (beginning at 1 sample per second) 292 | and monitor the packet stream for missed packets, indicating 293 | that the sample rate is too slow to avoid missing packets. 294 | Missed packets will translate to a lost information about the 295 | joystick state. 296 | As missed packets are registered, increase the sample rate until 297 | the target reliability is reached. 298 | """ 299 | # in my experience, you want to probe at 200-2000Hz for optimal 300 | # performance 301 | if joystick is None: 302 | joystick = XInputJoystick.enumerate_devices()[0] 303 | 304 | j = joystick 305 | 306 | print("Move the joystick or generate button events characteristic of your app") 307 | print("Hit Ctrl-C or press button 6 (<, Back) to quit.") 308 | 309 | # here I use the joystick object to store some state data that 310 | # would otherwise not be in scope in the event handlers 311 | 312 | # begin at 1Hz and work up until missed messages are eliminated 313 | j.probe_frequency = 1 # Hz 314 | j.quit = False 315 | j.target_reliability = .99 # okay to lose 1 in 100 messages 316 | 317 | @j.event 318 | def on_button(button, pressed): 319 | # flag the process to quit if the < button ('back') is pressed. 320 | j.quit = (button == 6 and pressed) 321 | 322 | @j.event 323 | def on_missed_packet(number): 324 | print('missed %(number)d packets' % vars()) 325 | total = j.received_packets + j.missed_packets 326 | reliability = j.received_packets / float(total) 327 | if reliability < j.target_reliability: 328 | j.missed_packets = j.received_packets = 0 329 | j.probe_frequency *= 1.5 330 | 331 | while not j.quit: 332 | j.dispatch_events() 333 | time.sleep(1.0 / j.probe_frequency) 334 | print("final probe frequency was %s Hz" % j.probe_frequency) 335 | 336 | 337 | def sample_first_joystick(): 338 | """ 339 | Grab 1st available gamepad, logging changes to the screen. 340 | L & R analogue triggers set the vibration motor speed. 341 | """ 342 | joysticks = XInputJoystick.enumerate_devices() 343 | device_numbers = list(map(attrgetter('device_number'), joysticks)) 344 | 345 | print('found %d devices: %s' % (len(joysticks), device_numbers)) 346 | 347 | if not joysticks: 348 | sys.exit(0) 349 | 350 | j = joysticks[0] 351 | print('using %d' % j.device_number) 352 | 353 | battery = j.get_battery_information() 354 | print(battery) 355 | 356 | @j.event 357 | def on_button(button, pressed): 358 | print('button', button, pressed) 359 | 360 | left_speed = 0 361 | right_speed = 0 362 | 363 | @j.event 364 | def on_axis(axis, value): 365 | left_speed = 0 366 | right_speed = 0 367 | 368 | print('axis', axis, value) 369 | if axis == "left_trigger": 370 | left_speed = value 371 | elif axis == "right_trigger": 372 | right_speed = value 373 | j.set_vibration(left_speed, right_speed) 374 | 375 | while True: 376 | j.dispatch_events() 377 | time.sleep(.01) 378 | 379 | if __name__ == "__main__": 380 | sample_first_joystick() 381 | # determine_optimal_sample_rate() 382 | --------------------------------------------------------------------------------