[ Intro ] Coucou, après avoir lu l'article de linux mag #113 sur la wiimote, j'ai eu envie de bidouiller avec ce truc marrant. Cet article en est donc grandement inspiré (voir aspiré pour la première partie). Si vous avez déjà lu l'article en question, cet article le complète. Eh wii, on va faire joujou avec la camera infrarouge de la wiimote ! De plus, on va utiliser une petite lib très simple (et classe) pour faire de la 3d, j'ai nommé vpython/visual. Si vous voulez une idée de ce qu'on va faire, allez faire un tour sur la page de Johny Chung Lee (http://www.cs.cmu.edu/~johnny/projects/wii/), section head tracking. Evidemment, on ne va pas faire les lunettes, mais le soft s'inspire grandement de ce qu'il a fait. Enfin, sachez que les descriptions techniques de la wii et de son protocole sont toutes tirées de WiiBrew (http://wiibrew.org/w/index.php?title=Wiimote), qui est très bien fait. Vous êtes toujours là ? [ Ingrédients ] Comme tout poulet basquaise, on va commencer par les ingrédients. En premier lieu, vérifiez que le bluetooth est bien géré par votre kernel. Si vous avez une distro avec un gros kernel, genre ubuntu, vous pouvez passer la partie suivante. [ Activation du bluetooth kernel ] # cd /usr/src/linux # make menuconfig activez le bluetooth : - Dans "Networking", sélectionnez "Bluetooth subsystem support" - Dans "Bluetooth subsystem support", faites votre sélection, mais n'oubliez pas l2cap. installez tout ça. [ Carte Bluetooth ] Si vous avez un portable, verifiez que vous avez bien une carte bluetooth (ça paraît idiot mais certains portables ont l'air d'en avoir et n'en ont pas) : $ lshw |grep bluetooth Sinon, sachez qu'un dongle bluetooth coûte dans les 10 euros à la fnac. [ Soft ] Maintenant, on va installer : sortez votre gestionnaire de paquets préféré, ici j'utiliserai celui de debian et ubuntu (apt-get). Installons bluez, qui implémente le protocole bluetooth et ces bindings python : $ sudo apt-get install bluez pybluez Maintenant, faisons nous plaisir avec visual python : $ sudo apt-get install python-visual [ Découverte de la wiimote ] La wiimote est un veritable bijou. Elle contient : - des boutons - un haut parleur - des LEDs - trois accélèromètres (pour chaque direction) - une camera avec un filtre infrarouge - de l'intelligence Tout cela, pour environ 40 euros. Les chercheurs en réalité virtuelle en raffollent. C'est pas le tout, mais je vous entend : "et si on codait ?" [ Détection de la wiimote ] Une wiimote est un peripherique bluetooth, et elle a une adresse pour l'identifier. On va faire un petit script pour extraire la votre. ________________________extractionAdresseWiimote_______________________________ 1: #!/usr/bin/python 2: import bluetooth 3: 4: peripheriques=bluetooth.discover_devices(lookup_names=True) 5: for appareil in peripheriques : 6: if (appareil[1] == "Nintendo RVL-CNT-01") : 7: fileHandle = open("wiimote.mac", "w") 8: fileHandle.write(appareil[0]) 9: fileHandle.close() 10: print("wiimote MAC address saved in wiimote.mac") _______________________________________________________________________________ Ce script utilise bluez pour chercher les périphériques bluetooth présents. Tout d'abord, il liste les devices bluetooth (ligne 4). Puis, si il tombe sur un device avec le bon nom (ligne 6), il enregistre son adresse dans le fichier wiimote.mac. Pour que le bluetooth détecte la wiimote, celle-ci doit se mettre en mode discover. Pour ceci, appuyez sur les boutons 1 et 2 en même temps. Cette manipulation sera à faire à chaque fois que vous lancerez un script wiimote, dorénavant. Lançons ce script (le fichier est executable (chmod +x)) : $ ./extractionAdresseWiimote wiimote MAC address saved in wiimote.mac Une petite lecture dans le fichier wiimote.mac nous apprend son adresse. $ cat wiimote.mac ; echo 00:21:BD:01:68:92 [ Dialogue de sourds ] Maintenant que l'on sait qui elle est, on va dialoguer avec notre wiimote. __________________________talkingToMyWiimote___________________________________ 1: #!/usr/bin/python 2: import bluetooth 3: 4: def getWiimoteAddress() : 5: fileHandle = open("wiimote.mac", 'r') 6: wiimoteAddress = fileHandle.read() 7: fileHandle.close() 8: return wiimoteAddress _______________________________________________________________________________ Cette première fonction récupère l'adresse dans wiimote.mac. __________________________talkingToMyWiimote___________________________________ 9: def main(input, output) : 10: output.send(chr(0x52) + chr(0x12) + chr(0x00) + chr(0x33)) 11: try : 12: while True : 13: charData = input.recv(23) 14: intData = [ ord(char) for char in charData ] 15: if (intData[3] & 0x08) == 8 : 16: return 17: except bluetooth.BluetoothError, inst : 18: print "bluetooth error : " + str(inst) 19: 20: def connect() : 21: wiimoteAddress = getWiimoteAddress() 22: input = bluetooth.BluetoothSocket(bluetooth.L2CAP) 23: output = bluetooth.BluetoothSocket(bluetooth.L2CAP) 24: input.connect((wiimoteAddress, 0x13)) 25: output.connect((wiimoteAddress, 0x11)) 26: if input and output : 27: print "connexion" 28: main(input, output) 29: else : 30: print "connexion rejetee" 31: return False 32: 33: connect() _______________________________________________________________________________ La fonction connect va permettre, comme son nom l'indique de se connecter à notre wiimote. On récupère son adresse (l.21), puis on ouvre deux socket : - input pour recevoir des données de la wiimote (port 0x13) - output pour lui envoyer des données (port 0x11) Si tout se passe bien, on lance main (l.9). [ Le langage de la wiimote ] On commence par envoyer une trame à la wiimote (l.10) : "0x52 0x12 0x00 0x33". - 0x52 est un identifiant pour cet interface (HID) - le deuxième octet est l'identifiant de canal, 0x12 est utilisé pour spécifier à la wiimote son mode de fonctionnement - Le 3ème octet permet de spécifier si on veut ue les données soit rapportées en continu. (2ème bit à 1, Ox04 si c'est le cas). - Enfin, le dernier octet désigne le mode. Il s'agit du mode "0x33: Core Buttons and Accelerometer with 12 IR bytes" Voici le format des rapports que celle-ci enverra : (a1) 33 BB BB AA AA AA II II II II II II II II II II II I BB BB : permet d'identifier les boutons. AA AA AA AA : information des accéléromètres les 12 octets II : les données camera infrarouge ! Dans main, après avoir sété le mode, on rentre dans une boucle d'interraction. On reçoit les informations de la wiimote (l.13). On regarde le 4ème octet de la trame. Si le 4ème bit est actvé, la touche A a été enfoncée. Dans ce cas, on quitte (return). Voilà, votre premier programme wiimote a été fait. Qui disait que c'était compliqué ? [ Infrarouge ] La camera infrarouge et sa gestion par la wiimote est très classe. Celle-ci peut extraire 4 points infrarouge, et donner leur taille. [ SensorBar ] Le SensorBar est la bonne blague de la wii. Ce qu'il faut se rappeler, c'est qu'à la sortie de la wii, tout le monde pensait que la SensorBar, comme son nom l'indiquait, était le capteur. Mais, vous le savez maintenant, le capteur est la camera dans la wiimote. Alors, la "SensorBar", c'est quoi ? Essentiellement des diodes infrarouge (blague, n'est-ce pas ?). Plusieurs diodes sont derrière des caches opaques à la lumière visible. Un cache diffuse la lumière, et permet ainsi de transformer plusieurs diodes en une source lumineuse pour la wiimote, plus grosse et plus puissante (cmb). [ Ecriture dans les registres ] Reprenons le code précédent : $ cp talkingToMyWiimote gettingMyWiimotePosition ajoutons l'import suivant, nous en aurons besoin : ________________________ gettingMyWiimotePosition _____________________________ 2: import time _______________________________________________________________________________ On va commencer par faire une fonction pour écrire dans un registre wiimote. Pour écrire dans un registre, on envoie une trame de cette forme (en hexa): (52) 16 MM FF FF FF SS DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD - 0x52 : le HID (cf-ci dessus) - 0x16 : le canal de commandes - MM : Selection de l'espace d'adressage. le bit 2 (0x04), s'il est à 1 permet d'accéder au registre, s'il est à 0, on accédera à la mémoire EPROM - FF FF FF : ces trois octets représentent l'adresse du registre - SS : la taille des données - DD (16 octets) : les données. Attention, même si on utilise pas des octets DD, on doit les envoyer tous. Notre fonction devrait être donc rapidement écrite. ________________________ gettingMyWiimotePosition _____________________________ 4: def writeRegister(output, addressBytes, dataBytes) : 5: message = [0x52] # HID 6: message.append(0x16) # chanel 7: message.append(0x04) # registers 8: message.extend(addressBytes) # array extension with address 9: message.append(len(dataBytes)) # data size 10: message.extend(dataBytes) # array extension with deta 11: message.extend([0 for i in range(16-len(dataBytes))]) # padding 12: output.send(intsToString(message)) _______________________________________________________________________________ Cette fonction prend en paramètre deux tableaux d'octets (entiers). Elle en forme un nouveau à partir de ceux-ci, représentant le message. Elle fait le remplissage d'octets DD (l.11) Enfin, elle le transforme en chaine et l'envoie (l.12) Evidemment, on définit intsToString, qui permet de convertir le tableau d'entier en la chaine à envoyer : ________________________ gettingMyWiimotePosition _____________________________ 4: def intsToString(ints) : 5: string = "" 6: for i in ints : 7: string += chr(i) 8: return string _______________________________________________________________________________ [ Initialisation de la camera ] L'initialisation de la camera, tout comme l'intialisation d'autres éléments, demande de suivre une série d'actions qui sont : - l'activation - le réglage de la sensibilité - le choix du mode de rapport Commençons par l'activation : ________________________ gettingMyWiimotePosition _____________________________ 20: def enableCamera(output) : 21: output.send(chr(0x52) + chr(0x13) + chr(0x04)) 22: output.send(chr(0x52) + chr(0x1a) + chr(0x04)) _______________________________________________________________________________ On envoie 0x04 au canal 13, et au canal 1a. On règle ensuite la sensibilité : ________________________ gettingMyWiimotePosition _____________________________ 24: def setSensitivity(output) : 25: writeRegister(output, [0xb0, 0x00, 0x00], 26: [0, 0, 0, 0, 0, 0, 0x90, 0, 0xc0]) 27: writeRegister(output, [0xb0, 0x00, 0x1a], [0x40, 0]) _______________________________________________________________________________ On envoie le premier bloc de sensibilité au ragistre 0xb00000, puis, on envoie le second bloc au registre 0xb0001a. Le dernier octet des deux blocs est la sensibilité : plus la valeur est grande plus la sensibilité est faible. Si le dernier octet du bloc 1 est 0x41, et le 2 0x00, la wiimote renverra des données pour les objets les plus sombres. Plus la sensibilité est haute, plus la résolution est bonne. Dans notre cas, on a pris des valeurs qui sont réputées marcher ^^. Enfin, on choisit le mode 3, qui est le mode étendu. Ce mode envoie chaque point par paquet de 3 octets. Notez que 3 * 4 = 12 (eh oui), ce qui est logique avec le mode d'envoi que nous avions choisis au début (sisi allez voir). On verra plus tard le format de ces données. Au début et à la fin de la configurationi sensibilité/mode, on envoie 8 au registre 0xb00030. On peut maintenant écrire la fonction initializeCamera : ________________________ gettingMyWiimotePosition _____________________________ 29: def initializeCamera(output) : 30: enableCamera(output) 31: time.sleep(0.05) 32: writeRegister(output, [0xb0, 0x00, 0x30], [8]) 33: time.sleep(0.05) 34: setSensitivity(output) 35: time.sleep(0.05) 36: writeRegister(output, [0xb0, 0x00, 0x33], [3]) 37: time.sleep(0.05) 38: writeRegister(output, [0xb0, 0x00, 0x30], [8]) _______________________________________________________________________________ Comme dirai Komugi, il est important de mettre des sleeps. Sans eux, l'initialisation ne marche pas. [ Récupération des coordonnées des points ] Maintenant, on a tout pour récupérer les infos de la camera. Reste à les comprendre. On a vu que dans le mode étendu, chaque point était codé sur 3 octets. Voici le format des trois octets envoyés. Octet 0 : Les 8 premiers bits de la coordonée X du point. Octet 1 : Les 8 premiers bits de la coordonée Y du point. Octet 2 : dans l'ordre (du poids fort au faible) - Les 2 bits hauts de la coordonée Y du point. - Les 2 bits hauts de la coordonée X du point - La taille du point sur les 4 bits bas Le tableau (tiré de wiibrew) suivant rendra les choses plus claires : Byte \ bit 7 6 5 4 3 2 1 0 0 X<7:0> 1 Y<7:0> 2 Y<9:8> X<9:8> S<3:0> Ecrivons la fonction qui permet de récuperer l'information d'un point : ________________________ gettingMyWiimotePosition _____________________________ 36: def getDot(data, offset) : 37: offset *= 3 38: offset += 3 39: xDown = data[4 + offset] 40: yDown = data[5 + offset] 41: yUp = (data[6 + offset] & (2**7 + 2**6)) >> 6 42: xUp = (data[6 + offset] & (2**5 + 2**4)) >> 4 43: size = (data[6 + offset] & (2**3 + 2**2 + 2 + 1)) 44: x = xDown + xUp * 2**8 45: y = yDown + yUp * 2**8 46: return ((x,y),size) _______________________________________________________________________________ Il s'agit principalement, de recomposer les données entières à partir des morceaux disponibles. On peut maintenant modifier notre main avec toutes ces fonctions ! [ Assemblage ] ________________________ gettingMyWiimotePosition _____________________________ 48: def main(input, output) : 49: output.send(chr(0x52) + chr(0x12) + chr(0x00) + chr(0x33)) 50: try : 51: initializeCamera(output) 52: while True : 53: charData = input.recv(23) 54: intData = [ ord(char) for char in charData ] 55: if (intData[3] & 0x08) == 8 : 56: return 57: if len(intData) > 9 : 58: dot1 = getDot(intData, 0) 59: dot2 = getDot(intData, 1) 60: print str(dot1) + " " + str(dot2) 61: except bluetooth.BluetoothError, inst : 62: print "bluetooth error : " + str(inst) _______________________________________________________________________________ [ Allumage des bougies ] Votre script est maintenant prêt. Allummez votre SensorBar. Si vous n'en avez pas, sachez que les bougies marchent très bien. Lancer le script et la wiimote, Et admirez... La camera infrarouge de votre wiimote est maintenant gérée! Avouez tout de même que cela manque d'interactvité. Chung lee n'aurait pas fait autant de bruit s'il avait fait mumuse avec son terminal. Et pourtant, vous allez voir que de l'un à l'autre il n'y a qu'un pas de hamster. [ Votre Scène 3d Visual ] Visual est une API vraiment sympa. Certes, elle est limitée, vous ne ferez pas de jeux vidéo avec du shadow mapping et autres shaders avec, mais ce qu'elle fait, elle le fait bien et simplement. C'est un bibliothèque appréciée par des chercheurs pour cela. On va s'en servir pour faire une scène qui vous rappellera la video de ce bon vieux johny. _________________________________target.py_____________________________________ 1: #!/usr/bin/python 2: from visual import * 3: import random 4: 5: def generateTarget(lineSize = 50.0, lineRadius = 0.2, 6: targetRadius = 1, targetDepth = 0.4) : 7: target = frame() 8: 9: halfLineSize = lineSize/2 10: cylinder(frame=target, pos=(0,0,-halfLineSize), axis=(0,0,lineSize), 11: radius=lineRadius, color=color.red) 12: cylinder(frame=target, pos=(0,0,halfLineSize), axis=(0,0,targetDepth), 13: radius=targetRadius, color=color.red) 14: cylinder(frame=target, pos=(0,0,halfLineSize), axis=(0,0,targetDepth), 15: radius=targetRadius, color=color.red) 16: cylinder(frame=target, pos=(0,0,halfLineSize), axis=(0,0,targetDepth), 17: radius=0.75 * targetRadius, color=color.white) 18: cylinder(frame=target, pos=(0,0,halfLineSize), axis=(0,0,targetDepth), 19: radius=0.50 * targetRadius, color=color.red) 20: cylinder(frame=target, pos=(0,0,halfLineSize), axis=(0,0,targetDepth), 21: radius=0.25 * targetRadius, color=color.white) 22: 23: return target _______________________________________________________________________________ à la ligne 2, on importe tout de visual. C'est bourrin, mais c'est ce qui est conseillé dans la documentation (http://vpython.org/contents/doc.html). On va écrire la fonction qui génère une cible. Pour cela, on va dessinner des cylindres. Ceux ci appartiendront tous à une frame (l.7), qui est un conteneur qui permet de réunir plusieurs objets. ainsi, on pourra faire tourner, translater cette ensemble de formes de manière transparante via la frame. En visual, comme dans d'autres API 3D, les axes sont définis dans le sens direct, avec Z vers l'observateur (vous). Les cylindre, ont une position, qui est la position d'une de leur base, un axe, un rayon, et une couleur. On peut acceder à la position des éléments via l'attribut pos. _________________________________target.py_____________________________________ 25: def generateTargetsFrame(targetsNumber) : 26: targets = frame() 27: targetsList = [] 28: for i in range(targetsNumber) : 29: targetsList.append((random.randint(-20, 20), 30: random.randint(-20, 20), 31: random.randint(-50,10))) 32: for coordinate in targetsList : 33: target = generateTarget() 34: target.pos = coordinate 35: target.frame = targets 36: 37: return targets _______________________________________________________________________________ La fonction generateTargetsFrame() va generer des cibles placées aléatoirement dans un champ prédéfini. Ainsi, on va avoir une frame constituuée de tout plein de cibles. Voilà, notre scène 3d est faite. Maintenant, passons aux choses sérieuses ! [ HeadTracking ou WiimoteTracking ? ] $ cp gettingMyWiimotePosition headTracking voici la liste des headers : _______________________________headtracking__________________________________ 1: #!/usr/bin/python 2: import bluetooth, time 3: from visual import * 4: from target import * _______________________________________________________________________________ On a récupéré des coordonées (x,y) pour chaque point. Vous me direz qu'il en manque une pour faire de la 3D. En effet, la wiimote renvoie les coordonées caméra des points, sur un écran de 1024*768 pixels. C'est à l'application d'en déduire les coordonnées spatiales de la source lumineuse par rapport à la wiimote. Je me contenterai de vous donner les formules que j'ai piqué à Johny. Je ne comprends pas tout mais ça marche, donc ça marche. Ainsi, nous pouvons écrire la fonction de projection inverse. _______________________________headtracking__________________________________ def get3dPositionFrom2dProjection(dot1, dot2) : x1, y1 = dot1[0][0], dot1[0][1] x2, y2 = dot2[0][0], dot2[0][1] if (x1 < 1023 and y1 < 767) and (x2 < 1023 and y2 < 767) : dx = (x1 - x2) * 1.0 / 1024 dy = (y1 - y2) * 1.0 / 768 fov = math.pi / 4 barWidth = 200.0 dist = sqrt(dx * dx + dy * dy) angl = fov * dist / 2.0 if angl == 0 : return headDist = (barWidth / 2.0) / tan(angl) mx = (x2 + x1) / 1024.0 / 2.0 my = (y2 + y1) / 768.0 / 2.0 return (-sin(fov * mx) * headDist * 0.5, -sin(fov * my) * headDist * 1.5, headDist) _______________________________________________________________________________ Cette fonction retourne None si elle n'a pas pu faire le calcul. [ Modification du main ] Reprenons notre main. _______________________________headtracking__________________________________ 52: def main(input, output) : 53: scene = display(title='headTracking',width=750, height=750, 54: background=(0,0,0)) 55: scene.autocenter = 0 56: scene.autoscale = 0 57: targets = generateTargetsFrame(50) 58: output.send(chr(0x52) + chr(0x12) + chr(0x00) + chr(0x33)) 59: try : 60: initializeCamera(output) 61: while True : 62: charData = input.recv(23) 63: intData = [ ord(char) for char in charData ] 64: if (intData[3] & 0x08) == 8 : 65: return 66: if len(intData) > 9 : 67: dot1 = getDot(intData, 0) 68: dot2 = getDot(intData, 1) 69: position = get3dPositionFrom2dProjection(dot1, 70: dot2) 71: if position != None : 72: targets.pos[0] = -10 - position[0] / 20 73: targets.pos[1] = 0 - position[1] / 200 74: targets.pos[2] = 60 - position[2] / 30 75: except bluetooth.BluetoothError, inst : 76: print "bluetooth error : " + str(inst) _______________________________________________________________________________ A la ligne 53, on crée la scene visual, en spécifiant la taille de la fenêtre et la couleur de fond. Par défaut, visual centre la vue et ajuste le zoom pour rendre l'ensemble des objets visibles. Ici, nous allons gérer le point de vue dans la scène, donc on désactive ces fonctionnalités (l.55, 56). On calcule la position 3d des points (l.69). Enfin, on déplace la scène en fonction de cette position (l.71-73). Les coordonnées de déplacement sont pondérées par des facteurs totalement experimentaux, c'est à vous de les régler si ils ne vous conviennent pas. Lancez le script, et faites vous plaisir. [ Conclusion ] La wiimote est incroyable. Mais ce qui l'est encore plus, c'est que nous avons fait ça en 116 lignes. A mon sens, il n'est pas forcément nécessaire d'utiliser des bibliothèques wiimote pour utiliser celle-ci dans des applications ou des jeux. Sur ma page personnelle, vous trouverez d'autres experimentation, avec les touches, l'accéléromètre, et même le speaker. Le code source de cet article y est disponible. Vous pouvez faire joujou avec votre wiimote maintenant ! [ Liens ] Johny Chung Lee : http://www.cs.cmu.edu/~johnny/projects/wii/ WiiBrew : http://wiibrew.org/w/index.php?title=Wiimote Vpython visual : http://vpython.org/contents/doc.html ma page : http://abdessel.iiens.net/wiimote