Utiliser une manette sans fil de Xbox 360 avec Python 3
Installation du pilote
J’utilise une manette sans-fil de Xbox 360 prévue pour PC. Pour installer les pilotes :
sudo apt-get install xboxdrv
On vérifie que la manette est prête à être utilisée :
sudo xboxdrv
xboxdrv
a besoin des droits super utilisateur pour fonctionner
Comme indiqué, s’il y a une erreur Error couldn't claim the USB interface: LIBUSB_ERROR_BUSY
, il faut exécuter la commande sudo rmmod xpad
puis relancer xboxdrv, ou lancer xboxdrv avec l’option --detach-kernel-driver
.
On peut démarrer la manette si ce n’était pas déjà, et cette commande affiche les événements qu’elle reçoit. Si aucun événement ne s’affiche, il faut peut-être resynchroniser en appuyant 3 secondes sur le bouton du récepteur, puis sur celui de la manette.
Source :
http://mattdyson.org/blog/2013/01/using-an-xbox-360-wireless-controller-with-raspberry-pi/
Utilisation avec Python
Pour l’utiliser avec Python, j’utilise ce script écrit par Zephod : https://github.com/zephod/lego-pi/blob/master/lib/xbox_read.py qui va lire les évènemements renvoyés par xboxdrv
Pour l’utiliser en tant que librairie, on le met dans un dossier lib
avec un fichier __init__.py
qui peut être vide. Ce fichier sert à indiquer que le dossier est un package importable.
Le script fonctionne bien avec python, mais n’est pas compatible avec python 3. Pour une utilisation avec python 3, le remplacer par ce code :
from os import popen from sys import stdin import re s = re.compile('[ :]') class Event: def __init__(self,key,value,old_value): self.key = key self.value = value self.old_value = old_value def is_press(self): return self.value==1 and self.old_value==0 def __str__(self): return 'Event(%s,%d,%d)' % (self.key,self.value,self.old_value) def apply_deadzone(x, deadzone, scale): if x < 0: return (scale * min(0,x+deadzone)) / (32768-deadzone) return (scale * max(0,x-deadzone)) / (32768-deadzone) def event_stream(deadzone=0,scale=32768): _data = None subprocess = popen('nohup xboxdrv --detach-kernel-driver','r',65536) while (True): line = subprocess.readline() if 'Error' in line: raise ValueError(line) data = list(filter(bool,s.split(line[:-1]))) if len(data)==42: # Break input string into a data dict data = { data[x]:int(data[x+1]) for x in range(0,len(data),2) } if not _data: _data = data continue for key in data: if key=='X1' or key=='X2' or key=='Y1' or key=='Y2': data[key] = apply_deadzone(data[key],deadzone,scale) if data[key]==_data[key]: continue event = Event(key,data[key],_data[key]) yield event _data = data
L’arborescence des fichiers est :
. ├── lib │ ├── __init__.py Vide │ ├── xbox_read.py Script de Zephod pour traiter les évenements └── xbox.py Script principal
Le fichier test.py contient :
#!/usr/bin/python3 from lib import xbox_read for event in xbox_read.event_stream(deadzone=12000): print(event)
On lui donne les droits en exécution et on le lance, toujours avec sudo :
chmod +x test.py sudo ./test.py
Sources :
https://github.com/zephod/lego-pi
http://tomre.es/post/lego-xbox-raspberry-pi/
http://mattdyson.org/blog/2013/01/using-an-xbox-360-wireless-controller-with-raspberry-pi/
Contrôle du robot
On va commencer en faisant simple : la gâchette gauche fait avancer le côté gauche, la gâchette droite fait avancer le côté droit.
Je reprends le code écrit précédemment sur l’envoi de données au contrôleur T’REX, en envoyant une commande à chaque événement LT ou RT.
#!/usr/bin/python3 # -*- coding: utf-8 -*- from __future__ import division # Pour division float from pprint import pprint from lib import xbox_read import pigpio import time # Cablage TREX_ADDRESS = 0x07 SDA=2 # Pin no 3 SCL=3 # Pin no 5 # Connexion de pigpiod pigpio = pigpio.pi() def trex_command(): ''' Envoi une requete de commande au T'REX ''' # Verification de la connexion de pigpiod if not pigpio.connected: exit(0) # Preparation de la requete Bit Bang pigpio.bb_i2c_open(SDA, SCL, baud=100000) # Preparation des donnees right_motor_bytes = right_motor_speed.to_bytes(2, byteorder='big', signed=True) left_motor_bytes = left_motor_speed.to_bytes(2, byteorder='big', signed=True) servo_0_bytes = (0).to_bytes(2, byteorder='big'); servo_1_bytes = (0).to_bytes(2, byteorder='big'); servo_2_bytes = (0).to_bytes(2, byteorder='big'); servo_3_bytes = (0).to_bytes(2, byteorder='big'); servo_4_bytes = (0).to_bytes(2, byteorder='big'); servo_5_bytes = (0).to_bytes(2, byteorder='big'); impact_bytes = (50).to_bytes(2, byteorder='big'); battery_bytes = (550).to_bytes(2, byteorder='big'); # Envoi de la requete pigpio.bb_i2c_zip(SDA, [ 4, TREX_ADDRESS, # Set I2C adress to TREX_ADDRESS 2, # Start condition 7, 27, # Write 27 bytes of data 0x0F, # 1. Start byte 0x06, # 2. PWM frequency -> 122Hz left_motor_bytes[0], left_motor_bytes[1], # 3-4. Left motor speed 0x00, # 5 Left motor brake right_motor_bytes[0], right_motor_bytes[1], # 6-7. Right motor speed 0x00, # 8. Right motor brake servo_0_bytes[0], servo_0_bytes[1], # 9-10. Servo 0 position servo_1_bytes[0], servo_1_bytes[1], # 11-12. Servo 1 position servo_2_bytes[0], servo_2_bytes[1], # 13-14. Servo 2 position servo_3_bytes[0], servo_3_bytes[1], # 15-16. Servo 3 position servo_4_bytes[0], servo_4_bytes[1], # 17-18. Servo 4 position servo_5_bytes[0], servo_5_bytes[1], # 19-20. Servo 5 position 0x32, # 21. Accelerometer de-vibrate -> 50 impact_bytes[0], impact_bytes[1], # 22-23. Impact sensitivity battery_bytes[0], battery_bytes[1], # 24-25. Low battery TREX_ADDRESS, # 26. I2C address 0x00, # 27. Clock frequency 3, # Stop condition 0 # No more commands ]) # Fin de la requete Bit Bang pigpio.bb_i2c_close(SDA) if __name__ == '__main__': try: left_motor_speed = 0 right_motor_speed = 0 # En attente d'evenement de la manette for event in xbox_read.event_stream(deadzone=12000): # Roues droites if event.key == 'RT': right_motor_speed = event.value # De 0 a 255 trex_command() # Roues gauches if event.key == 'LT': left_motor_speed = event.value # De 0 a 255 trex_command() except KeyboardInterrupt: # Deconnexion de pigpiod pigpio.stop() print("\nAdieu monde cruel")
Contrôle un peu plus complexe
Pour l’instant ça fonctionne, mais ce n’est pas encore l’idéal pour contrôler aisément le robot. Je vais proposer un comportement un poil plus complexe :
- La gâchette de droite (RT) contrôle proportionnellement la marche avant.
- La gâchette de gauche (LT) contrôle proportionnellement la marche arrière.
- Si les deux gâchettes sont appuyées, c’est celle qui a la plus forte intensité qui devient prioritaire
- Le joystick gauche (X1) va gérer la direction en diminuant progressivement la vitesse d’un des 2 côtés :
- Le joystick au centre ne change rien à la vitesse des 2 côtés
- Le joystick à 50% à gauche va diminuer de 50% la vitesse des roues de gauche
- Le joystick à 100% à gauche va arrêter les roues de gauche
- Les touches directionnelles gauche (dl) et droite (dr) vont permettre de faire un demi tour en appliquant 100% à un côté et -100% de l’autre. Elles sont prioritaires.
Voyons ce que ça donne :
#!/usr/bin/python3 # -*- coding: utf-8 -*- from __future__ import division # Pour division float from pprint import pprint from lib import xbox_read import pigpio import time # Cablage TREX_ADDRESS = 0x07 SDA=2 # Pin no 3 SCL=3 # Pin no 5 # Connexion de pigpiod pigpio = pigpio.pi() def trex_command(): ''' Envoi une requete de commande au T'REX ''' # Verification de la connexion de pigpiod if not pigpio.connected: exit(0) # Preparation de la requete Bit Bang pigpio.bb_i2c_open(SDA, SCL, baud=100000) # Preparation des donnees right_motor_bytes = right_motor_speed.to_bytes(2, byteorder='big', signed=True) left_motor_bytes = left_motor_speed.to_bytes(2, byteorder='big', signed=True) servo_0_bytes = (0).to_bytes(2, byteorder='big'); servo_1_bytes = (0).to_bytes(2, byteorder='big'); servo_2_bytes = (0).to_bytes(2, byteorder='big'); servo_3_bytes = (0).to_bytes(2, byteorder='big'); servo_4_bytes = (0).to_bytes(2, byteorder='big'); servo_5_bytes = (0).to_bytes(2, byteorder='big'); impact_bytes = (50).to_bytes(2, byteorder='big'); battery_bytes = (550).to_bytes(2, byteorder='big'); # Envoi de la requete pigpio.bb_i2c_zip(SDA, [ 4, TREX_ADDRESS, # Set I2C adress to TREX_ADDRESS 2, # Start condition 7, 27, # Write 27 bytes of data 0x0F, # 1. Start byte 0x06, # 2. PWM frequency -> 122Hz left_motor_bytes[0], left_motor_bytes[1], # 3-4. Left motor speed 0x00, # 5 Left motor brake right_motor_bytes[0], right_motor_bytes[1], # 6-7. Right motor speed 0x00, # 8. Right motor brake servo_0_bytes[0], servo_0_bytes[1], # 9-10. Servo 0 position servo_1_bytes[0], servo_1_bytes[1], # 11-12. Servo 1 position servo_2_bytes[0], servo_2_bytes[1], # 13-14. Servo 2 position servo_3_bytes[0], servo_3_bytes[1], # 15-16. Servo 3 position servo_4_bytes[0], servo_4_bytes[1], # 17-18. Servo 4 position servo_5_bytes[0], servo_5_bytes[1], # 19-20. Servo 5 position 0x32, # 21. Accelerometer de-vibrate -> 50 impact_bytes[0], impact_bytes[1], # 22-23. Impact sensitivity battery_bytes[0], battery_bytes[1], # 24-25. Low battery TREX_ADDRESS, # 26. I2C address 0x00, # 27. Clock frequency 3, # Stop condition 0 # No more commands ]) # Fin de la requete Bit Bang pigpio.bb_i2c_close(SDA) if __name__ == '__main__': try: # Initialisation des variables left_motor_speed = 0 right_motor_speed = 0 rt_intensity = 0 lt_intensity = 0 x1_intensity = 0 move_buttons = ['RT','LT', 'X1', 'dl', 'dr'] # En attente d'evenement de la manette for event in xbox_read.event_stream(deadzone=12000): # On n'envoie une commande au TREX seulement si c'est un bouton qui concerne le deplacement if event.key in move_buttons: # Marche avant if event.key == 'RT': rt_intensity = event.value # De 0 a 255 # Marche arriere if event.key == 'LT': lt_intensity = event.value # De 0 a 255 # Direction if event.key == 'X1': x1_intensity = event.value # De -32768 a 32766 (ma manette a un probleme) # La vitesse des 2 cotes est proportionnel a la gachette left_motor_speed = right_motor_speed = (rt_intensity if rt_intensity >= lt_intensity else -lt_intensity) # En cas de direction, on descend proportionnellement la vitesse d'un cote if (x1_intensity >= 0): right_motor_speed -= int(x1_intensity * right_motor_speed / 32766) else: left_motor_speed -= int(-x1_intensity * left_motor_speed / 32768) # Demi tour gauche if event.key == 'dl': left_motor_speed = -255 * event.value right_motor_speed = 255 * event.value # Demi tour droite if event.key == 'dr': left_motor_speed = 255 * event.value right_motor_speed = -255 * event.value # Envoi de la commande trex_command() except KeyboardInterrupt: # Arret d'urgence left_motor_speed = 0 right_motor_speed = 0 trex_command() # Deconnexion de pigpiod pigpio.stop() print("\nAdieu monde cruel")