| | 1 | = Tutorial 4: Add input frontend support = |
| | 2 | |
| | 3 | ''Files for this tutorial are available in [https://framagit.org/fma38/Py4bot/tree/master/py4bot/examples/tutorial_4 py4bot/examples/tutorial_4/].'' |
| | 4 | |
| | 5 | Here, we will see how to create a custom input frontend class for a new gamepad. This tutorial assumes you are running a linux OS. |
| | 6 | |
| | 7 | Again, we create a new empty dir in our home dir: |
| | 8 | |
| | 9 | {{{ |
| | 10 | $ mkdir tutorial_4 |
| | 11 | $ cd tutorial_4 |
| | 12 | }}} |
| | 13 | |
| | 14 | As example, we are going to add support for the gamepad of the Freebox Revolution v6 ADSL box, widely used in France: |
| | 15 | |
| | 16 | [[Image(gamepad_free.jpg, 300px)]] |
| | 17 | |
| | 18 | (Note that this model has been replaced by another one) |
| | 19 | |
| | 20 | First, we need to know how this gamepad is detected by the kernel. For that, we just launch, from a console, the command: |
| | 21 | |
| | 22 | {{{ |
| | 23 | $ sudo journalctl -f |
| | 24 | }}} |
| | 25 | |
| | 26 | and plug the gamepad. Here is what the kernel outputs: |
| | 27 | |
| | 28 | {{{ |
| | 29 | [3687970.593351] usb 2-1.7: new low-speed USB device number 15 using ehci-pci |
| | 30 | [3687970.706423] usb 2-1.7: New USB device found, idVendor=0079, idProduct=0006 |
| | 31 | [3687970.706426] usb 2-1.7: New USB device strings: Mfr=1, Product=2, SerialNumber=0 |
| | 32 | [3687970.706428] usb 2-1.7: Product: Generic USB Joystick |
| | 33 | [3687970.706430] usb 2-1.7: Manufacturer: DragonRise Inc. |
| | 34 | [3687974.416506] input: DragonRise Inc. Generic USB Joystick as /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.7/2-1.7:1.0/0003:0079:0006.0004/input/input15 |
| | 35 | [3687974.416759] dragonrise 0003:0079:0006.0004: input,hidraw3: USB HID v1.10 Joystick [DragonRise Inc. Generic USB Joystick ] on usb-0000:00:1d.0-1.7/input0 |
| | 36 | [3687974.416780] dragonrise 0003:0079:0006.0004: Force Feedback for DragonRise Inc. game controllers by Richard Walmsley <richwalm@gmail.com> |
| | 37 | }}} |
| | 38 | |
| | 39 | '''Py4bot''' comes with '''{{{py4bot-gui-evcal.py}}}''', a graphical tool to retreive an usb input device mapping. To use it, we need to give it the device path in '''{{{/dev}}}'''. Let's see what is our gamepad path: |
| | 40 | |
| | 41 | {{{ |
| | 42 | $ ls -1 /dev/input/by-id/ |
| | 43 | usb-1bcf_USB_Optical_Mouse-event-mouse |
| | 44 | usb-1bcf_USB_Optical_Mouse-mouse |
| | 45 | usb-DragonRise_Inc._Generic_USB_Joystick-event-joystick |
| | 46 | usb-DragonRise_Inc._Generic_USB_Joystick-joystick |
| | 47 | usb-Microsoft_Microsoft®_Digital_Media_Keyboard_3000-event-kbd |
| | 48 | usb-Microsoft_Microsoft®_Digital_Media_Keyboard_3000-if01-event-kbd |
| | 49 | }}} |
| | 50 | |
| | 51 | Here, the path to use is '''usb-DragonRise_Inc._Generic_USB_Joystick-event-joystick''': |
| | 52 | |
| | 53 | {{{ |
| | 54 | $ py4bot-gui-evcal.py /dev/input/by-id/usb-DragonRise_Inc._Generic_USB_Joystick-event-joystick |
| | 55 | |
| | 56 | {('EV_ABS', 3L): [(('ABS_X', 0L), |
| | 57 | AbsInfo(value=127, min=0, max=255, fuzz=0, flat=15, resolution=0)), |
| | 58 | (('ABS_Y', 1L), |
| | 59 | AbsInfo(value=127, min=0, max=255, fuzz=0, flat=15, resolution=0)), |
| | 60 | (('ABS_Z', 2L), |
| | 61 | AbsInfo(value=131, min=0, max=255, fuzz=0, flat=15, resolution=0)), |
| | 62 | (('ABS_RX', 3L), |
| | 63 | AbsInfo(value=127, min=0, max=255, fuzz=0, flat=15, resolution=0)), |
| | 64 | (('ABS_RZ', 5L), |
| | 65 | AbsInfo(value=127, min=0, max=255, fuzz=0, flat=15, resolution=0)), |
| | 66 | (('ABS_HAT0X', 16L), |
| | 67 | AbsInfo(value=0, min=-1, max=1, fuzz=0, flat=0, resolution=0)), |
| | 68 | (('ABS_HAT0Y', 17L), |
| | 69 | AbsInfo(value=0, min=-1, max=1, fuzz=0, flat=0, resolution=0))], |
| | 70 | ('EV_FF', 21L): [(['FF_EFFECT_MIN', 'FF_RUMBLE'], 80L), |
| | 71 | ('FF_PERIODIC', 81L), |
| | 72 | (['FF_SQUARE', 'FF_WAVEFORM_MIN'], 88L), |
| | 73 | ('FF_TRIANGLE', 89L), |
| | 74 | ('FF_SINE', 90L), |
| | 75 | (['FF_GAIN', 'FF_MAX_EFFECTS'], 96L)], |
| | 76 | ('EV_KEY', 1L): [(['BTN_JOYSTICK', 'BTN_TRIGGER'], 288L), |
| | 77 | ('BTN_THUMB', 289L), |
| | 78 | ('BTN_THUMB2', 290L), |
| | 79 | ('BTN_TOP', 291L), |
| | 80 | ('BTN_TOP2', 292L), |
| | 81 | ('BTN_PINKIE', 293L), |
| | 82 | ('BTN_BASE', 294L), |
| | 83 | ('BTN_BASE2', 295L), |
| | 84 | ('BTN_BASE3', 296L), |
| | 85 | ('BTN_BASE4', 297L), |
| | 86 | ('BTN_BASE5', 298L), |
| | 87 | ('BTN_BASE6', 299L)], |
| | 88 | ('EV_MSC', 4L): [('MSC_SCAN', 4L)], |
| | 89 | ('EV_SYN', 0L): [('SYN_REPORT', 0L), |
| | 90 | ('SYN_CONFIG', 1L), |
| | 91 | ('SYN_DROPPED', 3L), |
| | 92 | ('?', 4L), |
| | 93 | ('?', 21L)]} |
| | 94 | }}} |
| | 95 | |
| | 96 | The script automatically retreives the mapping, and creates a nice GUI: |
| | 97 | |
| | 98 | [[Image(py4bot-gui-evcal_1.png, 300px)]] |
| | 99 | |
| | 100 | Here, we can play with buttons. For analog axis to work, we need to press the ''Analog'' button; the red light should turn on. |
| | 101 | |
| | 102 | When playing with analog axis, we can see that a few of them are inverted. '''Py4bot''' expects than values increase when you push axis front/right. If it is not the case, we need to click the '''Invert''' checkbox. |
| | 103 | |
| | 104 | Note that on this specific gamepad, the '''{{{ABS_Z}}}''' axis is a combination of '''{{{ABS_X}}}''' and '''{{{ABS_RZ}}}'''. Don't know what is the purpose... I suggest we don't use it. |
| | 105 | |
| | 106 | Anyway, we should now have the following config: |
| | 107 | |
| | 108 | [[Image(py4bot-gui-evcal_2.png, 300px)]] |
| | 109 | |
| | 110 | Then, we just quit the script by closing the window. In the console, we see the final config: |
| | 111 | |
| | 112 | {{{ |
| | 113 | BUTTON_MAPPING = { |
| | 114 | 0x120: 0, # BTN_JOYSTICK |
| | 115 | 0x121: 1, # BTN_THUMB |
| | 116 | 0x122: 2, # BTN_THUMB2 |
| | 117 | 0x123: 3, # BTN_TOP |
| | 118 | 0x124: 4, # BTN_TOP2 |
| | 119 | 0x125: 5, # BTN_PINKIE |
| | 120 | 0x126: 6, # BTN_BASE |
| | 121 | 0x127: 7, # BTN_BASE2 |
| | 122 | 0x128: 8, # BTN_BASE3 |
| | 123 | 0x129: 9, # BTN_BASE4 |
| | 124 | 0x12a: 10, # BTN_BASE5 |
| | 125 | 0x12b: 11, # BTN_BASE6 |
| | 126 | } |
| | 127 | |
| | 128 | AXIS_MAPPING = { |
| | 129 | 0x00: 0, # ABS_X |
| | 130 | 0x01: 1, # ABS_Y |
| | 131 | 0x02: 2, # ABS_Z |
| | 132 | 0x03: 3, # ABS_RX |
| | 133 | 0x05: 4, # ABS_RZ |
| | 134 | 0x10: 5, # ABS_HAT0X |
| | 135 | 0x11: 6, # ABS_HAT0Y |
| | 136 | } |
| | 137 | |
| | 138 | DEFAULT_CALIBRATION = { |
| | 139 | 0: ( +0, +255), # ABS_X |
| | 140 | 1: (+255, +0), # ABS_Y |
| | 141 | 2: ( +0, +255), # ABS_Z |
| | 142 | 3: ( +0, +255), # ABS_RX |
| | 143 | 4: (+255, +0), # ABS_RZ |
| | 144 | 5: ( -1, +1), # ABS_HAT0X |
| | 145 | 6: ( +1, -1), # ABS_HAT0Y |
| | 146 | } |
| | 147 | }}} |
| | 148 | |
| | 149 | Let's now create a file named '''{{{dragonRise.py}}}'''. At the top of the file, we add a few imports: |
| | 150 | |
| | 151 | {{{ |
| | 152 | #!python |
| | 153 | |
| | 154 | from py4bot.services.logger import Logger |
| | 155 | from py4bot.common.intervalMapper import IntervalMapper |
| | 156 | from py4bot.inputs.frontends.frontendUsb import FrontendUsb |
| | 157 | }}} |
| | 158 | |
| | 159 | Then, we copy/paste the output of the script: |
| | 160 | |
| | 161 | {{{ |
| | 162 | #!python |
| | 163 | |
| | 164 | # Button mapping |
| | 165 | BUTTON_MAPPING = { |
| | 166 | 0x120: 0, # BTN_JOYSTICK |
| | 167 | 0x121: 1, # BTN_THUMB |
| | 168 | 0x122: 2, # BTN_THUMB2 |
| | 169 | 0x123: 3, # BTN_TOP |
| | 170 | 0x124: 4, # BTN_TOP2 |
| | 171 | 0x125: 5, # BTN_PINKIE |
| | 172 | 0x126: 6, # BTN_BASE |
| | 173 | 0x127: 7, # BTN_BASE2 |
| | 174 | 0x128: 8, # BTN_BASE3 |
| | 175 | 0x129: 9, # BTN_BASE4 |
| | 176 | 0x12a: 10, # BTN_BASE5 |
| | 177 | 0x12b: 11, # BTN_BASE6 |
| | 178 | } |
| | 179 | |
| | 180 | # Axis mapping |
| | 181 | AXIS_MAPPING = { |
| | 182 | 0x00: 0, # ABS_X |
| | 183 | 0x01: 1, # ABS_Y |
| | 184 | 0x02: 2, # ABS_Z |
| | 185 | 0x03: 3, # ABS_RX |
| | 186 | 0x05: 4, # ABS_RZ |
| | 187 | 0x10: 5, # ABS_HAT0X |
| | 188 | 0x11: 6, # ABS_HAT0Y |
| | 189 | } |
| | 190 | |
| | 191 | # Axis calibration |
| | 192 | DEFAULT_CALIBRATION = { |
| | 193 | 0: ( +0, +255), # ABS_X |
| | 194 | 1: (+255, +0), # ABS_Y |
| | 195 | 2: ( +0, +255), # ABS_Z |
| | 196 | 3: ( +0, +255), # ABS_RX |
| | 197 | 4: (+255, +0), # ABS_RZ |
| | 198 | 5: ( -1, +1), # ABS_HAT0X |
| | 199 | 6: ( +1, -1), # ABS_HAT0Y |
| | 200 | } |
| | 201 | |
| | 202 | }}} |
| | 203 | |
| | 204 | Finally, we create the class itself: |
| | 205 | |
| | 206 | {{{ |
| | 207 | #!python |
| | 208 | |
| | 209 | class DragonRise(FrontendUsb): |
| | 210 | """ Freebox Revolution v6 gamepad implementation (first model) |
| | 211 | """ |
| | 212 | def __init__(self, path, calibration=DEFAULT_AXIS_CALIBRATION): |
| | 213 | """ Init DragonRise object |
| | 214 | """ |
| | 215 | super(DragonRise, self).__init__("DragonRise", path) |
| | 216 | |
| | 217 | self._intervalMapper = {} |
| | 218 | for axis in calibration.keys(): |
| | 219 | self._intervalMapper[axis] = IntervalMapper(calibration[axis]) |
| | 220 | |
| | 221 | def _keyToButton(self, eventCode): |
| | 222 | code = BUTTON_MAPPING[eventCode] |
| | 223 | |
| | 224 | return code |
| | 225 | |
| | 226 | def _absToAnalog(self, eventCode, eventValue): |
| | 227 | axis = AXIS_MAPPING[eventCode] |
| | 228 | value = self._intervalMapper[axis](eventValue) |
| | 229 | |
| | 230 | return axis, value |
| | 231 | }}} |
| | 232 | |
| | 233 | That's it, our new gamepad support is added. We can then use it as fronted for our '''{{{Gamepad}}}''' class, instead of '''{{{Thrustmaster}}}''' (see [[Tutorial1FirstRobot|tutorial 1]]). |
| | 234 | |
| | 235 | Note that this class is already in '''Py4bot'''. |
| | 236 | |
| | 237 | In the [[Tutorial5AdvancedRemoteControlUsage|next tutorial]], we will go deeper in controllers usage. |