Parler de développement bluetooth est un peu comme parler de développement réseau. Et c'est encore plus vrai sous linux ou on développe "en bluetooth" la plupart du temps en utilisant des sockets tout ce qu'il y a de plus simples.
Je ne parlerai pas dans cet article de progammation kernel pour aider le projet BlueZ mais uniquement de développement d'applications user-space utilisant les bluez-libs.
Je vais commencer par me contredire. En fait on peut développer à plusieurs niveaux de la pile protocolaire et seules des applications de niveau L2CAP ou RFCOMM utiliseront des sockets à coup sûr. Pour la couche HCI, on a 2 choix, les bluez-libs offrent de nombreuses fonctions utilitaires qui permettent de mener à bien des tâches courantes (faire une recherche de périphériques ou envoyer une commande spécifique par exemple) mais on peut aussi envoyer des trames bluetooth brutes et recevoir des évènements conformes au protocole H4 avec des sockets HCI "en mode raw". Pour la programmation SDP, on utilise uniquement des fonctions utilitaires et non des sockets (sauf si on veut développer une implémentation personnalisée du protocole).
Cette précision faite, intéressons nous à ce qui nous est nécessaire pour développer ... Il nous faut bien sûr une chaîne de compilation classique (gcc, ld, libc-dev, ...) mais aussi les entêtes correspondant aux librairies bluetooth. Si vous avez installé le support bluetooth à partir des sources de bluez, vous avez déjà tout, sinon, il vous faudra vous mettre en quête du paquet correspondant. Il s'agit simplement de libbluetooth1-dev sous debian. Il existait auparavant une libsdp séparée, mais les packageurs nous ont simplifié la tâche et nous livrent le tout dans le même paquet.
On va réécrire la recherche de périphériques (inquiry dans la spécification) à l'aide de l'interface qui nous permet d'interagir avec la HCI (hci_lib).
#include <netdb.h> #include <bluetooth/bluetooth.h> #include <bluetooth/hci.h> #include <bluetooth/hci_lib.h> int main (int argc, char **argv) { int dev_id, num_rsp, length, flags; inquiry_info *info = NULL; bdaddr_t bdaddr; int i; dev_id = 0; /* device hci 0 */ length = 8; /* 10.24 seconds */ num_rsp = 10; flags = 0 ; num_rsp = hci_inquiry (dev_id, length, num_rsp, NULL, &info, flags); for (i=0 ; i<num_rsp ; i++ ) { baswap (&bdaddr, &(info+i)->bdaddr); printf ("\t%s\n", batostr(&bdaddr)) ; } free (info); return 0; }
On commence par inclure quelques fichiers d'entêtes qui vont nous être nécessaires. Vient ensuite la déclaration des variables, dev_id
qui permet de dire à quel périphérique bluetooth de la machine on s'adresse (dev_id = 0
si on veut utiliser le périphérique hci0), num_rsp
qui nous sert à dire après combien de périphériques trouvés la recherche doit être arrêtée (ici, 10), length
qui fixe, en tranches de 1,28 secondes, le temps que la recherche durera au maximum. La recherche s'arrête quand la première condition est remplie, nombre de périphériques ayant répondu ou temps de recherche écoulé.
Il nous suffit ensuite d'utiliser la fonction hci_inquiry
pour récupérer dans le tableau info
, notre résultat de recherche et dans num_rsp
le nombre de périphériques trouvés.
On utilise ensuite 2 fonctions utilitaires. baswap
pour retourner les adresses des périphériques (rappelez vous qu'elles nous parviennent à l'envers dans les trames) et ba2str
qui, comme son nom l'indique, nous fournit une chaîne de caractères représentant l'adresse bluetooth, sur 6 octets, passée en paramètre.
Je vous laisse découvrir par vous même les autres informations contenues dans la structure inquiry_info
en fouillant dans le fichier d'entête adéquat: bluetooth/hci.h.
$ gcc -o inquiry inquiry.c -lbluetooth $ ./inquiry 00:02:78:39:CE:C8 00:0E:6D:4E:52:EF $
La compilation d'une application bluetooth est aussi simple que la compilation d'une application utilisant une librairie "normale", il suffit d'indiquer au linker que l'on veut lier le programme à la bonne bibliothèque, grâce au flag -lbluetooth
.
Nous allons maintenant refaire la même chose mais en "discutant" directement avec la hci ... Ce n'est guère plus compliqué, comme on va le voir tout de suite.
#include <netdb.h> #include <bluetooth/bluetooth.h> #include <bluetooth/hci.h> #include <bluetooth/hci_lib.h> #include <sys/types.h> #include <sys/socket.h> int main (int argc, char **argv) { int sock, retval; int i; unsigned char buf[HCI_MAX_FRAME_SIZE]; struct sockaddr_hci addr; struct hci_filter filter; unsigned char cmd[] = {0x01, 0x01, 0x04, 0x05, 0x33, 0x8B, 0x9E, 0x08, 0x0A}; int cmd_len = sizeof(cmd); int encore = 1; sock = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI); if (-1 == sock) exit(1); hci_filter_clear(&filter); hci_filter_all_ptypes(&filter); hci_filter_all_events(&filter); retval = setsockopt(sock, SOL_HCI, HCI_FILTER, &filter, sizeof(filter)); if (-1 == retval) exit(1); addr.hci_family = AF_BLUETOOTH; addr.hci_dev = 0; retval = bind(sock, (struct sockaddr *)&addr, sizeof(addr)); if (-1 == retval) exit(1); retval = send (sock, cmd, cmd_len, 0); if (-1 == retval) exit(1); do { memset (buf, 0, sizeof(buf)); retval = recv (sock, buf, sizeof(buf), 0); if (-1 == retval) exit(1); switch (buf[1]) { case EVT_CMD_STATUS: if (buf[3]) { printf ("Erreur !\n"); encore = 0; } else { printf ("Commande en cours\n"); } break; case EVT_INQUIRY_RESULT: printf ("Périphérique trouvé:\n"); printf (" * Adresse : %02x:%02x:%02x:%02x:%02x:%02x\n", buf[9], buf[8], buf[7], buf[6], buf[5], buf[4]); printf (" * Classe : 0x%02x%02x%02x\n", buf[15], buf[14], buf[13]); break; case EVT_INQUIRY_COMPLETE: encore = 0; break; default: break; } } while (encore); close (sock); return 0; }
On inclut les fichiers nécessaires à une communication "brute" avec la hci bluetooth. On stocke notre commande de découverte, au format H4 pour utilisation ultérieure ainsi qu'un buffer statique suffisamment grand pour contenir les trames qu'on recevra en retour.
On initialise une socket, la famille d'adresse est AF_BLUETOOTH
pour bluetooth. Comme déjà dit, on utilise une socket de type SOCK_RAW
, et on choisit le protocole hci, BTPROTO_HCI
.
On peut choisir de ne recevoir que certains évènements en provenance de la hci et on utilise à cette fin des filtres de type struct hci_filter
. Nous voulons ici tout entendre, on commence donc par utiliser hci_filter_clear
pour partir d'un filtre sain et les 2 fonctions qui suivent disent que tous les évènements reçus doivent remonter à notre programme. Si on avait voulu ne recevoir que les évènements de type nouveau périphérique détécté et fin de recherche, on aurait écrit :
hci_filter_clear (&filter); hci_filter_set_ptype (HCI_EVENT_PKT, &filter); hci_filter_set_event (EVT_INQUIRY_RESULT, &filter); hci_filter_set_event (EVT_INQUIRY_COMPLETE, &filter);
On peut ainsi ajouter les évènements auxquels on veut pouvoir réagir. Il faudrait également ajouter EVT_CMD_STATUS
au filtre, sinon, en cas de problème, on resterait bloqué puisque l'évènement EVT_INQUIRY_COMPLETE
ne survient pas en cas de problème. Pour savoir si un problème a empêché l'envoi de la commande, il faut vérifier le statut renvoyé via l'évènement EVT_CMD_STATUS
. Pour atribuer ce filtre à la socket et l'activer, on utilise la fonction setsockopt
.
Il nous faut ensuite informer le système, lui dire avec lequel des périphériques bluetooth de la machine on va communiquer. On utilise à cette fin la fonction bind
, à laquelle on passe une structure sockaddr_hci
. On répète AF_BLUETOOTH
dans hci_family
et on passe 0
dans hci_dev
pour utiliser le premier périphérique, hci0.
On peut alors envoyer notre commande, directement dans la "socket hci", grâce à un appel à la fonction send
. On attend ensuite que des évènement arrivent en bloquant sur recv
. Notez que l'on pourrait également utiliser poll
ou select
pour surveiller cette socket.
On récupère alors dans buf
, les évènements en provenance de la hci. Pour bien faire, il faudrait ici valider la taille des trames reçues, chose facile puisque cette dernière en présente dans les premiers octets de chacune d'elles.
Pour rappel, buf[0]
est censé contenir 4, pour les évènements, mais on ne le teste pas ici. On teste directement la valeur de buf[1]
qui doit contenir le code de l'évènement.
Après la commande, on reçoit un évènement "statut de commande" (EVT_CMD_STATUS
). Si le quatrième octet, qui est le code d'erreur éventuel, est nul, la boucle peut se poursuivre, sinon, on arrête tout ici, il y a eu un problème.
On boucle alors en attendant l'évènement "fin de recherche" (EVT_INQUIRY_COMPLETE
). Pour chaque évènement "périphérique trouvé" (EVT_INQUIRY_RESULT
), on affiche l'adresse et la classe du périphérique en lisant les bons octets de la trame (Voir le numéro 78 de linux magazine pour la présentation du protocole).
Enfin, on ferme cette socket devenue inutile.
On teste ça tout de suite :
$ gcc -o inquiry_raw inquiry_raw.c -lbluetooth $ ./inquiry_raw Commande en cours Périphérique trouvé: * Adresse : 00:0e:6d:4e:52:ef * Classe : 0x500204 $
On va maintenant s'attaquer à la réalisation d'une application client/serveur de temps. On va offrir la possibilité de récupérer l'heure actuelle via bluetooth en codant un serveur. On s'attaquera ensuite naturellement au client permettant d'aller la lire.
Le protocole utilisé dans cet exemple est très simple. Une fois la connexion établie, le client envoie le caractère 'T' au serveur. Ce dernier lui retourne la réponse en 5 octets.
Le premier octet correpond à la commande envoyée, donc 'T'. Les trois octets suivants représentent l'heure courante. Un octet pour les heures, un pour les minutes et un pour les secondes. Le cinquième octet est la somme des quatre octets précédants, modulo 255 bien sûr puisqu'on n'a qu'un octet (soit dit en passant, au pire 'T'+23+59+59 ne fait que 225 donc le modulo est inutile ici).
Les codes qui suivent font l'impasse sur les contrôles de codes de retour des fonctions appelées. Ce n'est naturellement pas quelque chose à faire mais celà rend les codes plus lisibles. Utilisez les pages man pour savoir comment interpréter les retours des fonctions utilisées. La plupart du temps, une erreur est signalée par -1
et le code d'erreur se trouve dans la variable errno
.
include <sys/types.h> #include <sys/socket.h> #include <bluetooth/bluetooth.h> #include <bluetooth/hci.h> #include <bluetooth/l2cap.h> #include <time.h> static uint16_t timeserver_psm = 0xaa77; int main(void) { int sock, client; struct sockaddr_l2 addr; int addrlen; struct hci_dev_info di; unsigned char buff[256]; struct tm* heure; time_t seconds; sock = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); addr.l2_family = AF_BLUETOOTH; bacpy(&addr.l2_bdaddr, BDADDR_ANY); addr.l2_psm = htobs(timeserver_psm); bind(sock, (struct sockaddr *)&addr, sizeof(addr)); listen(sock, 10); addrlen = sizeof(addr); client = accept (sock, (struct sockaddr *)&addr, &addrlen); read (client, buff, 1); if (buff[0]=='T') { seconds = time(NULL); heure = localtime(&seconds); buff[0] = 'T'; buff[1] = heure->tm_hour; buff[2] = heure->tm_min; buff[3] = heure->tm_sec; buff[4] = 'T' + buff[1] + buff[2] + buff[3]; write (client, buff, 5); } close (client); close (sock); return 0; }
La première ligne que nous n'avons pas encore rencontrée (juste après les #includes
) définit le psm utilisé par le serveur. Je vais m'attarder quelque peu sur ce point de détail. Pour garder l'analogie avec les sockets et la programmation réseau retenue par les développeurs de bluez pour nous fournir une API de développement bluetooth, je dirais que le psm en L2CAP est l'équivalent du numéro de port en programmation UDP/TCP. A ceci près tout de même que le psm doit répondre à certaines contraintes :
Notre PSM étant choisi, on peut s'attaquer au reste du code.
On commence par créer une socket. Comme pour la socket HCI, on choisit la famille AF_BLUETOOTH
. En revanche, le type devient ici SOCK_SEQPACKET
(comme dans le cas de l'UDP) et le protocole, BTPROTO_L2CAP
.
On définit ensuite une structure sockaddr_l2
:
l2_family
répète AF_BLUETOOTH
.bacpy
pour copier BDADDR_ANY
dans l2_bdaddr
, ce qui consiste, à l'instar de INADDR_ANY
en programmation réseau, à signifier que l'on va répondre sur toutes les addresses bluetooth de la machine.l2_psm
contient notre PSM défini plus haut, que l'on n'oublie pas de retourner si besoin à l'aide de la fonction htobs
(comme le numéro de port est retourné avec htons
en programmation réseau classique)Je ne détaille pas le reste du code. Il est exactement équivalent à la programmation réseau habituelle. Je ne vais pas vous rabâcher des concepts déjà plusieurs fois évoqués dans le présent magazine et un peu partout sur internet.
Un serveur est quelque chose de complètement inutile si aucun client ne s'y connecte. Nous allons donc écrire notre client, qui récupère l'heure sur le serveur et l'affiche. On pourrait imaginer une sortie différente, pouvant être directement utilisée avec le programme date, pour mettre le système à l'heure automatiquement. Il vous suffit pour ça de changer la chaîne de formatage du printf
à la fin du code ci-dessous, que nous allons examiner maintenant.
#include <sys/types.h> #include <sys/socket.h> #include <bluetooth/bluetooth.h> #include <bluetooth/l2cap.h> #include <time.h> static uint16_t timeserver_psm = 0xaa77; int main(int argc, char** argv) { int serveur; struct sockaddr_l2 addr; unsigned char buff[5]; unsigned char h, m, s; serveur = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); addr.l2_family = AF_BLUETOOTH; baswap(&addr.l2_bdaddr, strtoba(argv[1])); addr.l2_psm = htobs(timeserver_psm); connect (serveur, (struct sockaddr *)&addr, sizeof(addr)); buff[0] = 'T'; write (serveur, buff, 1); read (serveur, buff, 5); if (buff[4] - (unsigned char)(buff[0] + buff[1] + buff[2] + buff[3])) { printf ("ATTENTION: Somme de contrôle incorrecte ...\n"); } h = buff[1]; m = buff[2]; s = buff[3]; printf ("Il est %02d:%02d:%02d\n", h, m, s); close (serveur); return 0; }
On retrouve le psm
de l'application qui, comme un port réseau, doit être connu par les clients et les serveurs.
On crée une socket de façon identique avec les mêmes options que dans le cas du serveur.
On remplit alors une structure sockaddr_l2
dans laquelle tout ce qui change par rapport à celle utilisée dans l'appel à bind dans le code du serveur est le champ l2_bdaddr
, dans lequel on copie inversée l'adresse du serveur. Un appel à la fonction connect
se charge d'effectuer la connexion L2CAP.
Une fois connecté, on utilise les fonctions read
, write
avant de mettre fin à la connexion par un appel à close
.
$ gcc -o timeserver timeserver.c -lbluetooth $ ./timeserver
$ gcc -o timeserverclient timeserverclient.c -lbluetooth $ ./timeserverclient 00:02:72:b3:46:d8 Il est 14:57:52
Qui dit bluetooth dit souvent périphériques limités. On peut donc parfois être amenés à réduire le MTU, le nombre d'octets de données par trames pour des raisons de taille de buffer réduits d'émission/réception ou de puissance de traitement des données. MTU signifie Maximum Trasfer Unit, il vaut 672 octets par défaut. Il est souvent établi à 48 ou 64 au minimum sur les périphériques à très faibles ressources car 48 est le minimum acceptable pour les trames de création/configuration de la connexion et 64 car c'est la première puissance de 2 immédiatement supérieure à ce miminum de 48.
Comme on l'a vu dans la description de la norme, on peut spécifier à l'aide des trames de configuration les tailles de 2 MTU dans une communication, à savoir en émission et en réception pour chaque périphérique mais la taille en émission de l'une devant bien évidemment correspondre à la taille en récpetion de l'autre et inversement.
Une machine sous linux peut spécifier la taille désirée en modifiant une structure l2cap_options
plus exactement les champs omtu
et imtu
(outgoing et incoming MTU) de cette dernière.
Cette struture est ensuite validée via l'appel setsockopt
. Une instance de hcidump lancée au moment de cette négociation montrera cette négociation via les trames L2CAP sur le channel 1 avec les identifiants de commandes L2CAP 4 et 5 faisant un aller-retour entre les périphériques pour négocier les MTU.
Inversement, on utilisera getsockopt
pour récupérer le MTU négocié, de façon à pouvoir adapter la taille des paquets envoyés ou de dimensionner les buffers d'une application, par exemple.
Ci dessous, on fixe un MTU sortant à 64 octets (sans contrôle d'erreur, c'est mal):
struct l2cap_options l2_opts; int optlen = sizeof(l2_opts); getsockopt( s, SOL_L2CAP, L2CAP_OPTIONS, &l2_opts, &optlen ); ... l2_opts.omtu = 64; err = setsockopt( s, SOL_L2CAP, L2CAP_OPTIONS, &l2_opts, optlen );
Un bon point de départ pour en savoir d'avantage sur la programmation L2CAP est de se tourner vers les sources du l'utilitaire l2test, dans les outils fournis par le paquet bluez-utils, à retrouver par exemple sur http://www.bluez.org/.
Transformer un programme utilisant L2CAP en un clone utilisant RFCOMM est extrêmement simple.
Il faut d'abord remplacer #include <bluetooth/l2cap.h>
par #include <bluetooth/rfcomm.h>
.
Il faut remplacer SOCK_SEQPACKET
par SOCK_STREAM
(comme on le ferait pour passer d'UDP à TCP en programmation réseau) et BTPROTO_L2CAP
par BTPROTO_RFCOMM
pour la création de la socket.
Enfin, on n'utilise plus de structure sockaddr_l2
mais sockaddr_rc
dans lesquelles on ne vas pas choisir le psm L2CAP mais le canal RFCOMM.
C'est à peu près tout ... Je vous livre le code ainsi modifié du serveur, pour exemple, avec les transformations en gras, ci-dessous.
#include <sys/types.h> #include <sys/socket.h> #include <bluetooth/bluetooth.h> #include <bluetooth/hci.h> #include <bluetooth/rfcomm.h> #include <time.h> static uint8_t timeserver_channel = 1; int main(void) { int sock, client; struct sockaddr_rc addr; int addrlen; struct hci_dev_info di; unsigned char buff[256]; struct tm* heure; time_t seconds; sock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); addr.rc_family = AF_BLUETOOTH; bacpy(&addr.rc_bdaddr, BDADDR_ANY);; addr.rc_channel = timeserver_channel; bind(sock, (struct sockaddr *)&addr, sizeof(addr)); listen(sock, 10); addrlen = sizeof(addr); client = accept (sock, (struct sockaddr *)&addr, &addrlen); read (client, buff, 1); if (buff[0]=='T') { seconds = time(NULL); heure = localtime(&seconds); buff[0] = 'T'; buff[1] = heure->tm_hour; buff[2] = heure->tm_min; buff[3] = heure->tm_sec; buff[4] = 'T' + buff[1] + buff[2] + buff[3]; write (client, buff, 5); } close (client); close (sock); return 0; }
Pour vérifier de façon originale que notre serveur fonctionne, on va prendre le téléphone utilisé la dernière fois. Il a le support bluetooth et comprend le Python. Nokia a eu la bonne idée d'ajouter bluetooth au module socket standard de Python, on va donc le tester.
Il s'agit à peu de choses près de code Python tout à fait traditionnel :
import socket sock = socket.socket(socket.AF_BT, socket.SOCK_STREAM) bd_addr = "00:02:72:B3:46:D8" port = 1 sock.connect((bd_addr, port)) sock.send("T") data = sock.recv(5) print "Il est %d:%d:%d" % (ord(data[1]), ord(data[2]), ord(data[3])) sock.close()
Il nous faut installer ce script python sur le téléphone. Enregistrons le sur l'ordinateur sous le nom timeclient.py
.
Pour l'envoyer au téléphone (reprenez le numéro précédent du magazine pour de plus amples informations), on utilise la ligne de commande suivante en adaptant l'adresse du périphérique :
$ ussp-push 00:0e:6d:4e:52:ef@9 timeclient.py timeclient.py
Le téléphone reçoit ce fichier comme un nouveau message. Quand on tente de l'ouvrir, il nous propose directement de l'installer comme nouveau script python. C'est ce qu'on voulait, on valide donc ce choix.
On doit ensuite lancer le serveur sur l'ordinateur linux.
$ ./rctimeserver.c
On relance ensuite python sur le téléphone. On choisit ensuite Run script dans le menu Options et on y sélectionne my\timeclient.py.
On obtient logiquement l'affichage suivant :
Ne tentez pas d'utiliser le code vu ci-dessus sur votre linux. Il ne fonctionnera pas. Pour utiliser bluetooth dans le langage python sous linux, on utilise PyBluez.
RFCOMM est obligatoire pour être compatible avec les périphériques Windows ayant un support bluetooth intégré (comprendre fournie par Microsoft uniquement, à savoir à partir de XP SP2 pour les PC), que ce soient des ordinateurs de bureau, des tablettes ou des assistants de poche. L'API windows fournie par Microsoft ne fournissant pas d'accès à la couche L2CAP.
Python pour Series60 est dans le même cas. Il ne permet d'utiliser les sockets bluetooth que pour la couche RFCOMM. Mais il supporte par ailleurs de très utiles fonctions ayant trait à la recherche de périphériques, au SDP ou encore à OBEX.
RFCOMM permet, un fois la connexion effectuée, d'utiliser de la programmation utilisant le port série virtuel ainsi créé. Cela assure un temps très court de portage des applications utilisant le port série, pour de l'acquisition de données notamment. Un grand nombre d'applications se sont ainsi vues portées vers bluetooth relativement facilement. Capteurs divers et variés, lecteurs de code barre, ...
L2CAP est lui beaucoup plus simple à implémenter et permet déjà de l'échange de données. Quand on implémente soi même la pile bluetooth et qu'il n'y a pas d'autres contraintes, c'est la solution préférée.
J'espère que ces quelques articles d'introduction auront permis de démystifier la technologie bluetooth. Il resterait énormément à dire, sur la sécurité, les modes d'économie d'energie, le SDP ou BNEP. Je vous invite à consulter la norme, disponible gratuitement et à consulter l'implémentation bluez dont les sources sont à votre disposition.
Bluetooth en est maintenant à la version 2 et offre plus de possibilités maintenant que celles présentées dans cet article, volontairement simplifié pour des raisons de volume mais aussi parce que la majorité des équipements bluetooth en circulation sont conformes aux versions 1.1 et 1.2 de la norme, pas à la version 2. Parmi ces améliorations, on trouve notamment des rapports de qualité de signal, dès la recherche de périphériques alors que celà nécessite une connexion avec des périphériques 1.1. Dans le cas d'une recherche de meilleur point d'accès, c'est une fonctionnalité non négligeable. La couche L2CAP a reçu son lot d'améliorations dans la norme et dans bluez (notamment autour de la qualité de service, "QoS") mais je ne vous en dis pas plus et vous laisse découvrir la suite, si elle vous intéresse, par vous mêmes.
Enfin, je n'ai parlé que de bluez, sans même mentionner les autres piles bluetooth disponibles pour linux. La plus connue des alternatives étant affix, développée par Nokia. Il y a 2 raisons à cela. D'abord, bluez est la pile officielle du noyau, celle contenue dans les sources que l'on trouve sur kernel.org et est donc le standard sous linux. La seconde raison, c'est que pour leur périphérique linux sorti récemment (et dont je vous reparlerai prochainement), le N770, Nokia utilise Bluez pour le support linux, pas Affix.
a+