Table des matières

Arduino BLE : communication Bluetooth

1. Introduction

Ce document montre comment établir une communication Bluetooth LE entre un Arduino nano 33 BLE et un mobile (téléphone ou tablette, Android ou iOS). Du côté de l'arduino, nous utilisons la bibliothèque BluetoothLE (disponible en standard). Pour programmer le logiciel du mobile, nous utilisons l'environnement de développement Apache Cordova, qui permet de développer une application reposant sur des pages HTML associées à du code Javascript. Au moyen de l'environnement de développement de l'OS cible (Android Studio ou XCode pour iOS), cordova génère un code natif qui effectue la lecture des pages HTML et l'exécution du code Javascript (comme le ferait un navigateur web). L'application peut aussi accéder à des ressources normalement interdites à un navigateur, par l'intermédiaire de plugins (accès aux fichiers, à l'accéléromètre, etc.). Pour accéder à l'interface Bluetooth, nous utilisons le plugin cordova-plugin-ble-central, qui permet d'utiliser le mobile en centrale bluetooth, l'arduino se comportant comme un périphérique.

Le code présenté ici montre comment une information fournie par un capteur peut être envoyée au mobile et comment celui-ci peut envoyer à l'arduino une information permettant d'effectuer une configuration (choix d'un capteur, réglage d'un paramètre, etc.).

2. Notions sur la communication Bluetooth LE

Dans une liaison Bluetooth LE, on distingue le périphérique et la centrale. Le périphérique fournit des informations ou effectue des actions physiques, la centrale interroge un ou plusieurs périphériques pour centraliser ces informations et les exposer à l'utilisateur. Dans notre cas, le périphérique est l'arduino, qui collecte des informations physiques à l'aide de capteurs ou actionne des moteurs. La centrale est le mobile. Le rôle du mobile est principalement d'offrir une interface à l'utilisateur mais il peut aussi contribuer au traitement des données, sachant que la capacité de calcul (vitesse et mémoire) d'un téléphone mobile est très largement supérieure à celle d'un microcontrôleur.

Un périphérique Bluetooth peut délivrer un ou plusieurs services. Chaque service est identifié par un numéro GUID (128 bits). Il existe des services standard mais on doit en général définir son propre identifiant GUID (que l'on peut générer avec ce générateur). Chaque service comporte une ou plusieurs caractéristiques, chacune ayant son propre numéro GUID (proche en principe du GUID du service). Une caractéristique est une donnée (de taille pouvant aller jusqu'à 512 octets) qui peut être lue ou écrite aussi bien par le périphérique que par la centrale. Il existe aussi un mécanisme de notification, qui informe la centrale lorsque le périphérique a modifié une caractéristique. Du côté du périphérique, il est aussi possible de savoir si une centrale a modifié une caractéristique.

Les données associées à une caractéristique sont :

La norme Bluetooth LE comporte aussi un protocole d'annonce (advertising), qui permet au périphérique de fournir les informations sur les services qu'il délivre. Une centrale peut scanner les périphériques qui sont à sa portée et, pour chaque périphérique trouvé, récupérer l'annonce.

Voyons l'exemple du service développé ci-après. Il s'agit d'exposer la valeur fournie par le convertisseur A/N de l'arduino, sous la forme d'un entier 16 bits. Une valeur est envoyée à la centrale toutes les secondes et celle-ci peut changer la voie de conversion utilisée. Le schéma ci-dessous montre le service et les deux caractéristiques définies pour cette application.

service.svgFigure pleine page

Le périphérique délivre un seul service, intitulé ADC. La caractéristique intitulée ANALOG contient la valeur fournie par le convertisseur A/N. Il s'agit d'un convertisseur 12 bits donc 2 octets (16 bits) sont nécessaires. La centrale peut lire cette valeur (READ) mais peut aussi recevoir des notifications (NOTIFY). Lorsque la valeur est modifiée, le périphérique envoie une notification à la centrale, laquelle exécute en réponse une fonction qui récupère la valeur et agit en conséquence (par exemple affiche cette valeur sur l'écran). Ce mécanisme de notification permet à la centrale de lire la caractéristique seulement lorsqu'une nouvelle donnée est disponible. La caractéristique intitulée CHANNEL précise, sous la forme d'un entier 8 bits, la voie utilisée pour la conversion. La centrale écrit dans cette caractéristique lorsque l'utilisateur choisit de modifier la voie.

3. Programme Arduino

Le programme qui implémente le service ADC est réalisé au moyen de la bibliothèque ArduinoBLE.

On commence par importer la bibliothèque et on définit les identifiants GUID (pour le service et pour ses deux caractéristiques).

ArduinoBLE-ADC.ino
#include <ArduinoBLE.h>
#define SERVICE_UUID "19B10000-E8F2-537E-4F6C-D104768A1214"
#define ANALOG_CHARACT_UUID "19B10001-E8F2-537E-4F6C-D104768A1214"
#define CHANNEL_CHARACT_UUID "19B10002-E8F2-537E-4F6C-D104768A1214"
    	   

Voici la déclaration du service et des deux caractéristiques :

BLEService AnalogService(SERVICE_UUID);
BLECharacteristic AnalogChar(ANALOG_CHARACT_UUID, BLERead | BLENotify,2);
BLEByteCharacteristic ChannelChar(CHANNEL_CHARACT_UUID, BLEWrite | BLERead);   	   
    	   

La classe BLECharacteristic permet de définir une caractéristique de taille quelconque alors que la classe BLEByteCharacteristic (héritée de la première) permet de définir une caractéristique de 1 octet.

L'ajout d'un descripteur n'est pas en principe indispensable mais le plugin Cordova que nous utilisons exige la présence d'au moins un descripteur pour chaque caractéristique. Un descripteur est constitué d'un code 16 bits et d'une valeur. Les codes standard sont donnés sur bluetooth descriptors. Nous définissons un descripteur de code 2901 (characteristic user description), qui permet de définir une description de la caractéristique.

BLEDescriptor AnalogDescriptor("2901","analog");
BLEDescriptor ChannelDescriptor("2901","channel");    	      
    	      

La fonction setup initialise l'interface BLE, ajoute le descripteur à chaque caratéristique, ajoute les deux caratéristiques au service, ajoute le service, et active une annonce pour ce service :

uint8_t channel;
boolean write_adr;

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  channel = A0;
  if (!BLE.begin()) {
    Serial.println("échec du démarrage de BLE");
    while (1);
  }
  AnalogChar.addDescriptor(AnalogDescriptor);
  ChannelChar.addDescriptor(ChannelDescriptor);
  BLE.setLocalName("ADC");
  BLE.setAdvertisedService(AnalogService);
  AnalogService.addCharacteristic(AnalogChar);
  AnalogService.addCharacteristic(ChannelChar);
  BLE.addService(AnalogService);
  BLE.advertise();
  
}   	       
    	       

La fonction loop affiche l'adresse du périphérique puis détecte la présence d'une centrale connectée à ce périphérique. Si une centrale est connectée, on entre dans une boucle de traitement qui reste active tant que la centrale maintient la connexion. La fonction ChannelChar.written() est appelée afin de savoir si cette centrale a écrit dans la caractéristique CHANNEL. Si c'est le cas, on modifie la voie en conséquence. Toutes les secondes, la valeur fournie par le convertisseur A/N est écrite dans la caractéristique ANALOG, ce qui pour effet de déclencher une notification à laquelle la centrale doit répondre afin de récupérer cette valeur.

void loop() {
  if (write_adr) {
    Serial.print("Adresse : ");
    Serial.println(BLE.address());
  }
  BLEDevice central = BLE.central();
  if (central) {
    Serial.print("connected to central : ");
    Serial.println(central.address());
    while (central.connected()) {
        if (ChannelChar.written()) {
            Serial.println("channel char written");
            int c = ChannelChar.value();
            if ((c>=0)&&(c<=8)) {
              Serial.println(c);
              channel = A0+c;
            }
        }
        int x = analogRead(channel);
        uint8_t a[2];
        a[0] = (x >> 8) & 0xFF;
        a[1] = x & 0xFF;
        AnalogChar.writeValue(a,2);
        delay(1000);
    }
    Serial.print("Disconnected from central: ");
    Serial.println(central.address());
  }
 
}   	         
    	         

4. Programme Cordova

Apache Cordova permet de développer une application pour mobile (Android ou iOS) en utilisant HTML et javascript. Dans le cas présent, l'application comporte une seule page HTML, qui constitue l'interface utilisateur, et un script javascript qui gère la partie centrale de la liaison bluetooth, grace au plugin cordova-plugin-ble-central.

Pour la programmation de l'interface utilisateur, nous utilisons les bibliothèques javascript jquery et jquery mobile.

Voici comment se fait l'utilisation de cordova. Depuis une console de commande, on se place dans le dossier dans lequel on souhaite placer le projet puis on exécute :

cordova create nom_projet

Cela a pour effet de créer un dossier nom_projet contenant l'arborescence avec des fichiers par défaut. Si l'on souhaite développer pour Android (il faut avoir installé Android Studio), on exécute :

cordova platform add android

Pour notre application, nous avons besoin d'un plugin :

cordova plugin add cordova-plugin-ble-central

Pour compiler et exécuter l'application, relier le mobile à l'ordinateur par liaison USB (en mode développeur) puis exécuter :

cordova run android

Cela a pour effet d'installer l'application sur le mobile et de la lancer, après quoi elle bien sûr utilisable en autonomie sur le mobile.

Pour réaliser une application, il faudra au minimum modifier les fichiers suivants :

Les bibliothèques javascript et autres scripts seront ajoutés au dossier www/js. Les feuilles de style CSS sont ajoutées au dossier www/css. L'icône de l'application pourra être modifiée en changeant le fichier www/img/logo.png.

Voici l'unique page HTML de l'application, qui vient remplacer le fichier index.html créé par défaut :

index.html
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:;">
        <meta name="format-detection" content="telephone=no">
        <meta name="msapplication-tap-highlight" content="no">
        <meta name="viewport" content="initial-scale=1, width=device-width">
        <link rel="stylesheet"  href="css/jquery.mobile-1.4.5.min.css">
		<link rel="stylesheet"  href="css/index.css">
		<script src="js/jquery-1.8.0.min.js"></script>
		<script  src="js/jquery.mobile-1.4.5.min.js"></script>
        <title>Nano 33 BLE ADC</title>
    </head>
    <body>
        <div data-role=page id=home>
			    <div data-role=header>
				<h1>Nano 33 BLE ADC</h1>
				</div>
				<div data-role=content>
					<p id="message">Message</p>
					<p id="analogValue"></p>
					
					<div data-role=controlgroup data-type=horizontal>
							<select id='channel'>
								<option value='0' selected>Voie A0</option>
								<option value='1'>Voie A1</option>
								<option value='2'>Voie A2</option>
								<option value='3'>Voie A3</option>
							</select>
							<button type="button" id="channelButton">Valider</button>
							
					</div>
				</div>
		</div>
        <script type="text/javascript" src="cordova.js"></script>
        <script type="text/javascript" src="js/index.js"></script>
    </body>
</html>
    	      

Cette page contient le chargement des bibliothèques javascript, la définition de la page HTML (qui fait appel à jquery mobile) et enfin l'exécution des scripts cordova.js et js/index.js. Les éléments HTML de cette page sont :

Voici la description du script js/index.js, qui gère la communication bluetooth avec l'arduino. La programmation en javascript se fait essentiellement en écrivant des fonctions qui sont appelées en réponse à différents événements. Voici la structure de ce script :

var app = {
    initialize: function() {
        document.addEventListener('deviceready', this.onDeviceReady.bind(this), false);
    },
    onDeviceReady: function() {
        // à compléter
    }
};
app.initialize();

    	        

Il contient un objet comportant deux fonctions. La première (initialize) est appelée à l'exécution du script. Elle déclare la seconde fonction (onDeviceReady) comme devant être exécutée dès que l'affichage de la page HTML est terminé. Voyons à présent le détail de cette fonction, qui contient toutes les opérations de communication bluetooth et de mise à jour de la page HTML.

On commence par définir l'adresse MAC du périphérique (elle est affichée par le programme Arduino). La connaissance a priori de cette adresse évite l'étape de recherche des périphériques bluetooth. Bien sûr, cette méthode est inapplicable si l'on dispose de plusieurs périphériques et qu'on ne sait pas lequel on utilise. Pour activer la recherche automatique, on affecte 0 à la variable device_id. Le nom du service (service_name) servira pour la recherche. On définit les GUID, identiques à ceux définis dans le programme Arduino.

index.js
var app = {
    initialize: function() {
        document.addEventListener('deviceready', this.onDeviceReady.bind(this), false);
    },
    onDeviceReady: function() {
		var device_id = "FA:46:B0:27:DA:CD";
		//device_id = 0;
		var service_name = "ADC";
		
		const service_uuid = "19B10000-E8F2-537E-4F6C-D104768A1214";
		const analog_characteristic_uuid = "19B10001-E8F2-537E-4F6C-D104768A1214";
		const channel_characteristic_uuid = "19B10002-E8F2-537E-4F6C-D104768A1214";
		
		var device_array = [];
		var scan_result = "";
		var message = document.getElementById('message');
		var analogValue = document.getElementById('analogValue');
    	           

Voici le code exécuté lors de l'appel de la fonction onDeviceReady, qui se fait dès que l'affichage de la page HTML est effectué :

		if (device_id==0) {
			ble.scan([],120,scanSuccess,scanFailure);
			message.innerHTML = "start scan ...";
		}
		else {
			ble.connect(device_id,connectSuccess,connectFailure);
			message.innerHTML = "connect ....";
		}    	           
    	           

Si l'adresse du périphérique n'est pas connue, on lance une recherche des périphériques bluetooth, pendant une durée maximale de 120 secondes. Si l'adresse est connue, on se connecte à ce périphérique.

La fonction scanSuccess est appelée lorsqu'un périphérique est découvert. Dans cette fonction, on affiche sur la page HTML le nom et l'adresse de chaque périphérique. Si le nom correspond au nom recherché (défini dans service_name), on arrête la recherche.

		function scanSuccess(result) {
			device_array.push(result)
			scan_result += "</br>"+result.name;
			scan_result += "</br>"+result.id;
			message.innerHTML = scan_result;
			if (result.name==service_name) {	
				device_id = result.id;
				scanStop();
			}
		}    	            
    	            

Voici la fonction appelée en cas d'échec du scan (qui ne doit pas se produire à moins d'une défaillance matérielle) et la fonction qui permet d'arrêter le scan et d'effectuer la connexion au périphérique :

    	        function scanFailure(result) {
			message.innerHTML = "scan failure";
		} 
		function scanStop() {
			ble.stopScan();
			ble.connect(device_id, connectSuccess, connectFailure);
			message.innerHTML = "connect ....";
		}           
    	            

La fonction connectSuccess est appelée lorsque la connexion au périphérique a réussi. Dans cette fonction, on enregistre la centrale pour recevoir la notification émise lorsque la propriété ANALOG est modifiée par le périphérique. L'échec de la connexion est bien-sûr possible, en particulier lorsque le périphérique n'est pas sous tension ou est hors de portée.

		function connectSuccess() {
			message.innerHTML = "connect success";
			ble.startNotification(device_id,service_uuid,analog_characteristic_uuid,notify_success,notify_failure);
			
		}
		function connectFailure(result) {
			message.innerHTML = "connect error";
		}		
    	             

Lorsqu'une notification est envoyée par le périphérique, la fonction notify_success est appelée. Dans cette fonction, la valeur de la caractéristique est récupérée sous la forme d'un tampon que l'on convertit en tableau d'octets. Dans le cas présent, ce tableau a deux éléments. On affiche la valeur correspondante en décimal (on pourrait bien sûr la convertir en volts).

		function notify_success(buffer) {
			var view = new Uint8Array(buffer);
			var x = view[0]*0x100 + view[1];
			analogValue.innerHTML = x;
		}
		function notify_failure(reason) {
			message.innerHTML = "notify failure "+reason;
		}
    	              

Pour finir, voici les fonctions qui gèrent le changement de voie. La fonction changeChannel est appelée lorsque l'utilisateur appuie sur le bouton de validation. Dans cette fonction, on récupère la voie choisie dans le sélecteur à choix multiple puis on écrit sur la caractéristique CHANNEL.

		$("#channelButton").bind("click",changeChannel);
		function changeChannel() {
			var data = new Uint8Array(1);
			data[0] = $("#channel").val();
			ble.write(device_id,service_uuid,channel_characteristic_uuid,data.buffer,changeChannel_success,changeChannel_error);
		}
		function changeChannel_success() {
			message.innerHTML = "change channel success";
		}
		function changeChannel_error() {
			message.innerHTML = "change channel error";
		}
    }
};

app.initialize();  	               
    	               
Creative Commons LicenseTextes et figures sont mis à disposition sous contrat Creative Commons.