Étude du réseau Bluetooth : j'ai implanté un module Bluetooth Low Energy de chez STMElectronics sur le circuit imprimé du Timer (Minuteur). Le module STxxx communique via une interface série SPI. La liaison s'effectue de la manière suivante :
MOSI | 0x0A, | 0x00, | 0x00, | 0x00, | 0x00 |
MISO | 0x02, | 0x7F, | 0x00, | 0x0A, | 0x00 |
Le byte 0x0A indique que l’on veut transmettre des informations. Les quatre bytes suivants sont des dummy. Le module répond par un byte 0x02 pour indiquer qu’il est prêt, suivi de deux bytes indiquant le nombre d’octets que l’on peut envoyer au module, dans cet exemple 128 (0x007F). Les deux bytes suivants représentent le nombre d’octets contenus dans le buffer de réception du module, dans ce cas (0x000A) 10 octets.
Lorsque l’on envoie une commande HCI ou ACI au module Bluetooth, le premier byte indique le type de paquet. Il en existe cinq types.
Ensuite, nous avons 2 bytes pour l’OpCode. Celui-ci est divisé en deux parties : le Groupe (OGF),
représenté par les 6 bits de poids fort, et la Commande (OCF), par les 10 bits restants.
Cela est suivi d’un byte représentant le nombre de paramètres de la commande, et enfin les
paramètres.
L’OpCode est transmis en mode LITTLE ENDIAN, c’est-à-dire l'octet de poids faible pour le premier byte suivi de l'octet de poids fort pour le second byte. Exemple : pour un OGF = 0x04 (Information paramètres) et un OCF = 0x01 (hci_read_local_version_information), la combinaison des deux 0001’0000’0000’0001 = 0x1001. L’OpCode sera donc transmis en 0x01, 0x10. Ci-dessous les différents types de groupes OGF.
Pour connaître l'OCF, consultez le document du fournisseur ST pour les commandes HCI et les commandes ACI. Ci-dessous, une petite partie de la liste.
Cette commande hci n’a pas de paramètre.
En bleu, l’en-tête (write) ; en rouge,
HCI_COMMAND_PKT (Packet) ; en vert, l’OpCode (Little Endian) ;
et en violet, le nombre total de paramètres en bytes. Il est
parfois nécessaire d’envoyer plusieurs fois l’en-tête tant que le module ne répond pas par
0x02 pour indiquer qu’il est prêt et que le byte suivant est égal à zéro, ce qui représente la taille
du buffer de réception.
Réponse à la commande ci-dessus.
Le module Bluetooth répond qu’il est prêt et qu’il a 0x0F
(15) bytes dans son buffer. Après les cinq bytes d’en-tête, les trois suivants
0x04, 0x0E, 0x0C représentent : 0x04 indique qu’il s’agit
d’un paquet de type Event, le byte suivant 0x0E indique le type d’Event (voir la liste ci-dessous),
et le dernier 0x0C représente le nombre d’octets qui suivent.
On retrouve dans la réponse le HCI_COMMAND_PKT et l’OpCode de la commande
hci_read_local_version_information en vert. Les derniers bytes
fournissent les informations suivantes :
Cette commande comporte plusieurs paramètres :
On reconnaît l’en-tête d’écriture suivi du HCI_COMMAND_PKT (0x01) et de l’OpCode (0xFD02). En violet, c’est le nombre de paramètres en octets ; dans cet exemple, 19 octets (0x13).
Réponse à la commande aci ci-dessus.
Dans la réponse, on reconnaît le HCI_EVENT_PKT (0x04) suivi du type (0x0E) et le nombre d’octets de paramètres (0x06). Ensuite, on retrouve l’OpCode de la commande ACI (0xFD02).
Service Handle. Lorsque ce service est ajouté, un descripteur est attribué par le serveur
pour ce service. Le serveur alloue également une plage de descripteurs pour ce service à
partir de serviceHandle à "serviceHandle + max_attr_records - 1".
Ci-dessous la liste des types d’événements.
Le module Bluetooth reçoit des paquets de données, comme dans le système Ethernet. Ces paquets doivent être assemblés et ils n’arrivent pas forcément dans l’ordre. Pour résoudre ce problème, il faut utiliser une liste chaînée. (La liste chaînée ne fait pas partie de cette étude !)
Les principales étapes suivantes sont requises pour configurer correctement les périphériques BlueNRG-1 et BlueNRG-2.
aci_hal_write_config_data()
aci_gatt_init()
aci_gap_set_io_capability()
aci_gap_set_authentication_requirement()
aci_gatt_add_service()
aci_gatt_add_char()
aci_gatt_add_char_desc()
Maintenant que nous sommes en mesure de communiquer avec le module BlueNRG, il est important de comprendre
le principe de fonctionnement du protocole de communication.
Les adresses suivantes sont prises en charge par les périphériques BlueNRG-1 et BlueNRG-2 :
Les adresses MAC publiques (adresses de 6 octets, soit 48 bits) identifient de manière unique un périphérique BLE
et sont définies par l'Institut des ingénieurs électriciens et électroniciens (IEEE).
Les trois premiers octets de l'adresse publique identifient l'entreprise qui a émis l'identifiant et sont connus
sous le nom d'identificateur unique d'organisation (OUI). Un identificateur unique d'organisation (OUI) est un numéro
de 24 bits acheté auprès de l'IEEE.
Cet identifiant permet de réserver un bloc d’adresses publiques possibles (jusqu’à 2^24, provenant des 3 octets
restants de l’adresse publique) à l’usage exclusif de l’entreprise possédant un OUI spécifique.
Si l'utilisateur souhaite programmer son adresse MAC personnalisée, il doit la stocker dans un emplacement Flash
spécifique du périphérique, réservé uniquement au stockage de l'adresse MAC. Ensuite, lors de la mise sous tension
du périphérique, il doit programmer cette adresse dans la radio en appelant une API spécifique de la pile BLE.
La commande API BLE pour définir l'adresse MAC est la suivante :
aci_hal_write_config_data()
Cette commande doit être envoyée aux périphériques BlueNRG avant le démarrage de toute opération BLE (après l'appel à l'API BlueNRG_Stack_Initialization pour l'initialisation de la pile BLE). L'exemple de pseudocode suivant montre comment définir une adresse publique :
uint8_t bdaddr[] = {0x12, 0x34, 0x00, 0xE1, 0x80, 0x02};
ret = aci_hal_write_config_data(CONFIG_DATA_PUBADDR_OFFSET,
CONFIG_DATA_PUBADDR_LEN,
bdaddr);
Le Generic Access Profile (GAP) définit les rôles des composants maîtres et esclaves, comme les messages de signalement ou la découverte du réseau. Il y a deux méthodes de signalement dans le GAP :
Alors que ces deux types de messages ont le même format composé de 31 octets de données, seuls les messages de signalement sont obligatoires et envoyés à intervalles réguliers (plus la période est longue, plus la consommation est faible).
Lorsqu'ils les reçoivent, les autres composants peuvent demander l’émission d’un message de réponse au scan (scan response packet) avec d’éventuelles données supplémentaires.
La diffusion de messages de signalement personnalisés est la méthode utilisée par
l’iBeacon et la norme Eddystone. À chaque fois qu’une connexion est établie avec
un composant esclave, les liaisons se font grâce à un service GATT avec ses
caractéristiques. Les messages de signalement cessent tant que la connexion n’est
pas terminée.
La couche GAP introduit 4 rôles :
Le broadcaster s'oppose au scanner, le peripheral s'oppose au central. Ce qu'il est
fondamental de comprendre avec le Bluetooth Low Energy, c'est qu'il y a deux façons
pour que deux appareils communiquent ensemble.
Le mode dit connecté, ou le mode advertising.
C'est ainsi que l'on fait le lien avec les deux types d'événements et les deux types
de canaux radio introduits précédemment.
En mode advertising, un appareil émet des trames sur les 3 canaux
d'advertising et ces trames sont accessibles à tout autre appareil à l'écoute sur ces 3 canaux.
En mode connecté, un lien est établi entre deux appareils et eux
seuls peuvent alors communiquer ensemble sur des canaux connus négociés ; le lien peut
éventuellement être authentifié et encrypté pour accroître la sécurité.
Le rôle de Broadcaster est destiné à des applications qui ne font qu'émettre. Les appareils supportant ce rôle envoient des événements d'advertising pour diffuser des données. Ce rôle ne supporte pas le mode connecté.
Le rôle de Scanner est dédié à des applications qui ne font que recevoir. Il s'agit du rôle dual de l'Observer. Un appareil basé sur ce rôle reçoit des données diffusées lors d'événements d'advertising. Ce rôle ne supporte pas le mode connecté.
Le rôle Peripheral est destiné à des appareils qui supportent une (ou plusieurs) connexions et sont moins complexes et plus contraints qu'un appareil avec le rôle Central. Un appareil avec un rôle Peripheral a besoin d'un Controller esclave (par opposition à maître).
Le rôle Central supporte plusieurs connexions avec différents appareils Peripheral ; un tel appareil est l'initiateur des connexions et a besoin d'un Controller maître. Il est doté de fonctionnalités plus complexes et plus coûteuses que le Peripheral.
Le Generic Attribute Profile définit comment le BLE transfère les données entre
maîtres et esclaves dans les deux sens.
Il définit des profiles qui regroupent un ensemble de
services. Chaque service a des
caractéristiques qui correspondent aux données.
Les modes de fonctionnement peuvent changer quand on passe du GAP au GATT.
Le GATT dispose de deux modes de fonctionnement : client ou serveur.
Au risque de faire vieux jeu, les périphériques esclaves sont appelés
« serveurs GATT » et les maîtres (plus puissants) sont des
« clients GATT ».
Souvenez-vous : le serveur détient les données, le client
veut les données. Toutes les connexions sont initiées par les clients.
À chaque nouvelle connexion, un client peut demander une liste des services fournis par le serveur. Le maître détient en effet en permanence une liste des services (pas forcément exhaustive) complétée grâce aux messages de signalement.
Le plus souvent, l'esclave est un serveur GATT et le
maître est client GATT. Autrement dit, le serveur expose
à un client des données, appelées attributes.
Les attributes,
tels que définis par le protocole ATT, sont formatés soit en services, soit en characteristics.
La valeur incluse dans le characteristic est la valeur que le client cherche en pratique
à récupérer, comme par exemple le nombre de battements cardiaques relevés par un
capteur placé près du cœur.
// Lecture de la version du logiciel du module (cette étape n’est pas nécessaire)
HCI_Read_Local_Version_Information();
// Remise à zéro du module
HCI_Reset();
// Après le reset, il est important d'écrire la configuration de l'adresse BDADDR
ACI_hal_write_config_data();
// Initialisation de la couche GATT
ACI_gatt_init();
// Initialisation de la couche GAP en tant que périphérique esclave
ACI_Gap_Init_slave_peripheral();
// Il est crucial de commencer par l'initialisation de la couche GATT !
// Lors de l’initialisation de la couche GAP, le rôle du module est défini dans les paramètres.
// Une fois le rôle défini, on obtient un handle pour le service et un handle pour les paramètres
// que l’on doit passer à la fonction.
ACI_gatt_update_char_value(...);
// Spécification des exigences d'authentification.
Dans l’exemple donné, il utilise la fonction :
ACI_gap_set_auth_requirement();
// Il existe également une autre fonction ACI_gap_set_authentication_requirement() ?
ACI_gap_set_authentication_requirement();
Si toutes ces étapes se sont bien déroulées, on peut afficher "SERVER : BLE Stack initialized !!".
Avant de détailler les services et les caractéristiques, il est maintenant nécessaire de créer une fonction
permettant au GATT Client de découvrir le GATT Server nouvellement créé. Les périphériques esclaves ont
deux modes de fonctionnement : en mode diffusion ou en mode liaison directe avec un composant maître.
Cette fonction vise à désactiver la réponse au scan (/* disable scan response */).
HCI_le_set_scan_resp_data(...)
/* Pour que le périphérique soit détectable par le maître, il faut faire appel à la fonction : */
ACI_gap_set_discoverable(...)
Une fois que l'appareil utilisé en tant que serveur GATT est rendu détectable, il devient visible pour le dispositif client GATT, permettant ainsi l'établissement d'une connexion BLE. Le dispositif client GATT utilise la commande GAP ACI suivante pour se connecter au serveur GATT en mode "Advertisement".
exemple:
ACI_gap_create_connection(0x4000, 0x4000,
PUBLIC_ADDR, bdaddr,
PUBLIC_ADDR, 9, 9, 0, 60, 1000, 1000);
La variable « bdAddr » est l’adresse du périphérique client GATT correspondant.
Lors de l'établissement de la connexion, le client retourne un événement avec les paramètres suivants : (EVT_LE_CONN_COMPLETE).
Voici la réponse du client ci-dessus ! La variable status est 0x00, indiquant que la connexion avec le serveur a réussi. Une fois établie, le client peut lire les caractéristiques des services offerts par le serveur.
Création d'un service sur le serveur : la fonction aci_gatt_add_serv(...)
Attention le nombre d'attribue et de trois par caractéristique et 1 pour le service !!
Aci_gatt_add_service(UUID_TYPE_128,
uuid,
PRIMARY_SERVICE,
10,
timeServHandle);
Dans l'exemple ci-dessus il y a 3 caractéristiques et 1 service (time).
Sur le dispositif client génère l'évènement suivant pour lire une caractérisitique d'un service du serveur.
Sur le périphérique serveur GATT, l’API suivante doit être appelée pour la mise à jour la valeur caractéristique :
Aci_gatt_update_char_value(chatServHandle,
TXCharHandle,
0,
len,
(tHalUint8 *) data);
Sur le dispositif client GATT, l’API suivante doit être invoquée pour écrire dans une caractéristique :
Aci_gatt_write_without_response(connection_handle,
RX_HANDLE + 1,
len,
(tHalUint8 *) data);
Ici, 'data' représente la valeur de l'attribut pointé par le handle RX_HANDLE contenu dans le handle de connexion 'connection_handle'. 'connection_handle' est le descripteur renvoyé lors de la création de la connexion, spécifié comme paramètre dans EVT_LE_CONN_COMPLETE.