ex0ns

about
A Computer Science Student's blog, about programming, algorithms, hack and a lot more.

[C] Détecteur de proximité

28 Jun 2014

Nous revoilà plongé dans le fantastique monde du Bluetooth et de Bluez. Il y a quelque temps j'avais commencé un projet de capteur de proximité, longtemps (1 an) abandonné, j'ai eu une soudaine envie de le continuer.

Pour résumer, le but de cet outil est simple: associer des scripts à la détection/arrêt de détection d'un appareil bluetooth, dans l'optique, par exemple, de verrouiller votre ordinateur lorsque vous vous en éloignez (téléphone en poche), ou alors de démarrer ThunderBird lorsque vous entrez dans la pièce. Il aurait probablement été plus court et efficace de développer ce genre d'outil dans un autre langage, mais il s'agit très bon entrainement pour faire un peu de C.

Jusque là, tout fonctionne très bien si ce n'est quelques détéctions infructueuses de temps en temps, le daemon fonctionne tout à fait correctement, cependant, je voulais rajouter un niveau de difficulté (et donc d'interêt) au projet en y intégrant une notion de distance, les scripts ne se baserai plus sur une détection binaire, mais pourrait être lancés en fonction de la proximité.

Phase de recherche

Rien ne permet cependant de déterminer avec précision la distance à un objet à partir du bluetooth, et cela à cause des interférences, des objets présents dans l'environnement. Pour autant, il existe un indicateur caractérisant la puissance d'un signal, à partir duquel nous pouvons (essayer) d'interpoler une distance plus ou moins (très peu) précise, il s'agit du Received Signal Strength Indication (RSSI). Cet indicateur semble parfait pour réaliser ce que je veux, bien que peu précis. Pour autant, ce n'est pas si facile, en effet, afin de récupérer la valeur du RSSI pour un périphérique bluetooth, il faut être pairé avec le périphérique, c'est ce que propose la fonction : hci_read_rssi. Cependant, compte tenu de la nature du projet je ne pouvais pas me permettre de pairer les périphériques (imagez un même téléphone devant être lié à plusieurs ordinateur, ou bien un ordinateur connecté à plusieurs périphériques). Il faut donc trouver un moyen de récuperer la valeur du RSSI sans pairer les périphériques, et c'est là que ça tout ce complique...

En effet, rien dans le code source de Bluez semble indiquer qu'il est possible d'effectuer une telle opération, et la documentation ne nous aide pas beaucoup nous plus. Cependant, des petits indices laissent penser qu'il existe un moyen de récupérer la valeur du RSSI au moment du scan, par exemple la structure inquiry_info_with_rssi définie dans hci.h

typedef struct {
       bdaddr_t      bdaddr;
       uint8_t       pscan_rep_mode;
       uint8_t       pscan_period_mode;
       uint8_t       dev_class[3];
       uint16_t      clock_offset;
       int8_t        rssi;
} __attribute__ ((packed)) inquiry_info_with_rssi;

Après de nombreuses heures de recherche, je n'ai rien trouvé de plus probant sur le sujet que la mailing list de Bluez (lien, dans lequel la fonction inquirywithrssi est nommée, le code fournit à la fin ne compile en tout cas pas chez moi mais la technique que je vais vous présenter ici en est très fortement inspirée.

Inquiry with rssi

Dans la suite de cet article, je vais vous présenter ma fonction int hci_inquiry_with_rssi(int dev_id, int len, int num_rsp, const uint8_t lap, inquiry_info_with_rssi *ii) , dont le but est le même que hci_inquiry, mais qui doit en plus, se charger de retourner les informations concernant le RSSI. Pour cela, il nous faut le code source de la fonction hci_inquiry (diponible ici. En bref, le code créer une socket de type bluetooth, la remplie avec les paramètres passés à la fonction puis fait un appel à IOCTL afin de transmettre l'information au driver de la carte bluetooth (par l'intermediaire du kernel), notre code est très ressemblant à l'exception même de IOCTL, que nous n'utiliserons pas. Sans plus tarder, voici le début de la fonction:

int hci_inquiry_with_rssi(int dev_id, int len, int num_rsp, const uint8_t *lap, inquiry_info_with_rssi **ii){
    int nrsp = num_rsp, dd, num, nrinfo = 0;
    struct hci_filter flt;
    inquiry_cp cp;
    struct pollfd p;
    unsigned char buf[HCI_MAX_EVENT_SIZE];
    unsigned char *ptr;
    hci_event_hdr *hdr;
    inquiry_info_with_rssi *start_info, *stop_info, *info;
    extended_inquiry_info *start_e_info, *stop_e_info, *e_info; // Petit détail dont je parlerai à la fin


    uint8_t default_lap[3] = { 0x33, 0x8b, 0x9e }; // Je n'ai pas trouvé la signification de ces valeurs, si une âme charitable est prête à m'expliquer !!!

    if(nrsp <= 0){ // Nombre de réponse invalide
        num_rsp = 0;
        nrsp = 255;
    }

    if(dev_id < 0){ // Périphérique bluetooth
        dev_id = hci_get_route(NULL);
        if(dev_id < 0){
            errno = ENODEV;
            return -1;
        }
    }

    dd = hci_open_dev(dev_id); // Ouverture de la socket sur ce périphérique
    if(dd < 0){
        return dd;
    }

    hci_filter_clear(&flt);
    hci_filter_all_events(&flt);
    hci_filter_set_ptype(HCI_EVENT_PKT, &flt);

    if (setsockopt(dd, SOL_HCI, HCI_FILTER, &flt, sizeof(flt)) < 0) // Ajoute les options prédéfinies à la socket
        return -1;

Pour ce qui suit, je dois avouer être dans le flou, je ne comprend pas la structure "inquiry_cp", ce que le champ 'lap' représente, il ne s'agit donc que d'une pâle copie de la source de hci_inquiry:

memset(&cp, 0, sizeof(cp));
if(lap  != NULL )
    memcpy(&(cp.lap), lap, 3);
else
    memcpy(&(cp.lap), default_lap, 3);
cp.length  = len;
cp.num_rsp = nrsp;

hci_send_cmd(dd, OGF_LINK_CTL, OCF_INQUIRY, INQUIRY_CP_SIZE, &cp);

p.fd = dd;
p.events = POLLIN | POLLERR | POLLHUP;

Maintenant que la socket est ouverte avec les options souhaitées, nous devons récupérer les évènements sont déclanchés sur cette dernière, c'est ce que nous faisons avec la fonction poll et la structure pollfd, que nous plaçons dans une boucle, dont voici le code:

while(1){
    p.revents = 0;
    if (poll(&p, 1, POLL_TIMEOUT) > 0) { // Retourne le nombre d'évenements déclanchés sur les sockets liée à la structure pollfd
        len = read(dd, buf, sizeof(buf));

        if (len < 0)
            continue; /* Malformed packet */
        else if (len == 0)
            break; /* EOF */

        hdr = (void *) (buf + 1);
        ptr = buf + (1 + HCI_EVENT_HDR_SIZE);
        len -= (1 + HCI_EVENT_HDR_SIZE);

        switch (hdr->evt) {
            case EVT_INQUIRY_RESULT:
            break;
            case EVT_INQUIRY_RESULT_WITH_RSSI:
                num = ptr[0];
                start_info = (void*) ptr + 1;
                stop_info = start_info + num;
                for(info = start_info; info < stop_info; info++){
                    nrinfo = add_device(ii, info, nrinfo);
                }
            break;

            case EVT_EXTENDED_INQUIRY_RESULT:
                num = ptr[0];
                start_e_info = (void*)ptr+1;
                stop_e_info = start_e_info + num;
                for(e_info = start_e_info; e_info < stop_e_info; e_info++){
                    nrinfo = add_device(ii, (inquiry_info_with_rssi*) e_info, nrinfo);
                }
            break;

            case EVT_INQUIRY_COMPLETE:
            return nrinfo;
            break;
        }
    }
}

Une fois que nous avons recupéré le packet dans le buffer grâce à read, ils nous faut retrouver les informations à l'aide des différentes structures telles que: hci_event_hdr, contenant le type d'évènement qui s'est déroulé (dans notre cas, ce sont les INQUIRY_RESULT* qui nous intéressent).

Il existe plusieurs types de retour pour ces évènements, qui dépendent de la technologie utilsée par les périphériques (version bluetooth par exemple), les deux types de retour qui nous interessent sont EVT_INQUIRY_RESULT_WITH_RSSI et EVT_EXTENDED_INQUIRY_RESULT" car tout deux contiennent un champs dédié à la valeur du RSSI, le premier octet contenu dans la variable ptr (contenu du paquet) est le nombre de réponses reçues, il suffit de parcourir les données à l'aide des structures adéquates inquiry_info_with_rssi et extended_inquiry_info, la fonction add_device est la suivante:

int add_device(inquiry_info_with_rssi **list, inquiry_info_with_rssi *ii, int nrinfo){
    char addr[18];
    int found = 0, i;
    for(i = 0; i < nrinfo; i++){
        if(bacmp(&(((*list)+i)->bdaddr), &(ii->bdaddr)) == 0){
            found = 1;
            break;
        }
    }
    if(!found){
        ba2str(&(ii->bdaddr), addr);
        printf("Adding device %s\n", addr);
        memcpy((*list)+nrinfo, ii, sizeof(inquiry_info_with_rssi));
        nrinfo++;
    }
    return nrinfo;
}

Cette fonction prend en paramètre la liste des périphériques découverts jusque là, le dernier résultat du scan, ainsi que le nombre de périphériques déjà découvert (la taille de list), le but est de parcourir cette liste et de n'y ajouter que les périphériques qui n'y sont pas encore présent. Si le périphérique est déjà dans la liste, on ne fait rien, mais on pourrait très bien mettre à jour la valeur du RSSI afin d'avoir la valeur la plus récente possible à la fin du scan (exercice laissé au lecteur, tient en 4 lignes). À la fin, la fonction hci_inquiry_with_rssi retourne le nombre de périphériques détéctés le reste des informations ont étées ajoutées dans le tableau inquiry_info_with_rssi **ii passé en paramètre (exactement comme la fonction hci_inquiry de Bluez).

Et voilà ! Nous avons maintenant la possibilitée de récupérer la valeur RSSI simplement grâce à un scan. J'espère qu'il n'y avait pas une fonctionnalité caché de Bluez qui permet de faire ca en quelques lignes, cependant si vous le connaissez, n'hésitez par à le partager.

Sources