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