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

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *