Communiquer en Bluetooth avec un système Arduino sous Qt.


La majorité des ordinateurs et smartphones sont équipés de l'interface Bluetooth pour échanger des fichiers ou déporter des services (souris, voix, .etc.). De plus en plus, cette interface est également utilisée dans le milieu industriel, notamment pour piloter des micro systèmes, sans fil, à partir d'un smartphone ou d'une tablette.

Ce post traite de la communication entre une micro système basé sur une carte Arduino, et une application Qt sur Android.

RFCOMM est un protocole natif de Bluetooth qui se prête parfaitement à ce type d'application. Une fois les périphériques apairés, il émule tout simplement une liaison série RS232 sans fil. De plus, pour les linuxiens dont je suis, il est directement supporté par le système avec la commande rfcomm.

D'autres protocoles sont également disponibles sur Bluetooth, pour des applications plus spécifiques telles que le partage de fichier par exemple. Peut être qu'un jour, je trouverai le courage d'écrire un post sur ceux ci.

Jusqu'à présent, la bibliothèque Qt ne permettait pas de gérer les protocoles Bluetooth de façon native pour les applications Desktop, IOS ou Android. Bien sûr les informaticiens ont contourné le problème avec des solutions hybrides consistant à effectuer des appels à l'API BlueZ sous linux, Windows, ou Java pour Android au moyen de la classe Qt, QAndroidJniObject, par exemple.

Depuis la version 5.3, les développeurs ont intégré les fonctionnalités Bluetooth à la bibliothèque Qt. C'était l'occasion pour moi de commencer à en tester certaines et de vous en faire profiter.

Partie matérielle

Pour effectuer un liaison de façon assez simple, j'ai décidé de mettre en œuvre ma carte Arduino préférée, la Nano, à laquelle j'ai connecté un module Bluetooth HC05. Ce module, basé sur un chipset CSR BC417143, a l'avantage d'être très bon marché (moins de 5$ à l'unité sur les sites chinois.

Le cablage est très simple, il consiste principalement à alimenter le module (vcc - gnd) et à connecter l'interface série (rx/tx - tx/rx). Le module HC05 possède également une pin key ermettant de basculer du mode AT (configuration), key au niveau haut au mode Communication, key au niveau bas.

A la mise sous tension, si la pin key est au niveau haut, le module démarre en mode AT à 38400 bauds. La résistance et le condensenteur sont là pour assurer un forçage au niveau bas 0 à la mise sous tension afin d'éviter cela.

cablage

Partie logicielle

Arduino

L'Arduino Uno ne possédant qu'une liaison série réservée au déploiement et bien utile pour sortir des logs, on peut utiliser la bibliothèque SoftSerial ou NewSoftSerial qui permet d'implémenter d'autres liaisons séries attachées à des pins de base.

#include <NewSoftSerial.h>
#include <Arduino.h>

#define ARDUINO_LED 13          // arduino nano on board led
#define MODULE_KEY 4            // the key pin of the HC05 module (AT mode)
#define AT_INIT             // AT configuration on startup

NewSoftSerial btSerial(7, 8);       // connexion to HC05 (rx on pin 7 & tx on pin 8, arduino side!)

String inputString = "";            // a string to hold incoming data
boolean stringComplete = false;     // whether the string is complete

unsigned long tc, tp;           // the time, for timeout processing
int arduinoledState = 0;        // the current state of the led

// function waiting for a specific string received
bool waitFor(const char *rep);

La fonction de setup:

// SETUP  FUNCTION
void setup()
{
    pinMode(ARDUINO_LED, OUTPUT);           // the on board led pin
    pinMode(MODULE_KEY, OUTPUT);            // the HC05 Key pin (switch to AT mode when HIGH)
    digitalWrite(MODULE_KEY, LOW);          // communication mode

    inputString.reserve(200);           // reserve 200 char max

    Serial.begin(9600);             // on board Serial
    btSerial.begin(9600);               // SoftSerial connected to HC05 BTmodule
        tp = millis();                  // get time 

#ifdef AT_INIT          // AT configuration of the HC05, to make once time

  // programmation du module
  digitalWrite(MODULE_KEY, HIGH);           // switch to AT mode
  Serial.println("AT mode configuration:\r\n");

  Serial.print("AT: ");                 // verify we are in AT mode
  btSerial.print("AT\r\n");
  if (!waitFor("OK\r\n")) Serial.println("time out error AT");

  Serial.print("UART: ");               // serial communication parameters
  btSerial.print("AT+UART=9600,0,0\r\n");
  if (!waitFor("OK\r\n")) Serial.println("time out error AT+UART");

  Serial.print("NAME: ");               // bluetooth device name
  btSerial.print("AT+NAME=hc05-arduino-nano\r\n");
  if (!waitFor("OK\r\n")) Serial.println("time out error AT+NAME");

  Serial.print("PSWD: ");               // password for pairing
  btSerial.print("AT+PSWD=1234\r\n");
  if (!waitFor("OK\r\n")) Serial.println("time out error AT+PSWD");

  Serial.print("ROLE: ");               // device in slave mode
  btSerial.print("AT+ROLE=0\r\n");
  if (!waitFor("OK\r\n")) Serial.println("time out error AT+ROLE");

  Serial.print("RMAAD: ");              // Delete all authenticated devices in the pair list
  btSerial.print("AT+RMAAD\r\n");
  if (!waitFor("OK\r\n")) Serial.println("time out error AT+RMAAD");

  Serial.print("CMODE: ");              // connection mode
  btSerial.print("AT+CMODE=0\r\n");
  if (!waitFor("OK\r\n")) Serial.println("time out error AT+CMODE");

  digitalWrite(MODULE_KEY, LOW);            // leave AT mode & switch back to communication mode
#endif

  Serial.println("Ready to communicate in RFCOMM mode\r\n");
  }

la fonction loop:

//  THE LOOP FUNCTION
void loop(){

// wait for a complete line from bluetooth device
while (btSerial.available()) {
    char inChar = (char)btSerial.read();        // get the new byte:
    inputString += inChar;              // add it to the inputString:
    if (inChar == '\n') {               // if the incoming character is a newline, set a flag
        stringComplete = true;          // so the main loop can do something about it:
        break;
        }
    }

if (stringComplete){                    // a command is available to proceed
        stringComplete = false;
        if (inputString == "ON\n")arduinoledState = 1;          // ON turn led on
        else if (inputString=="OFF\n")arduinoledState=0;        // OFF turn led off
        // and more commands you want...
        inputString = "";                       // clear the string
        btSerial.print("?");                        // send a prompt char
        }

digitalWrite(ARDUINO_LED, arduinoledState);                 // turn the on board led
}

Lors de la phase de configuration en mode AT, la fonction waitFor suivante permet d'attendre la réponse du module avant de procéder à d'autres commandes. Si la réponse attendue est bien arrivée la fonction renvoie la valeur booléenne true, si elle n'est pas arrivée au bout de 2 secondes, la fonction retourne la valeur false.

nota: La definition AT_INIT peut être supprimée, ou commentée, pour le pas refaire la configuration du module à chaque redémarrage.

//  The waitFor a string function with time out
bool waitFor(const char *rep){
unsigned long t0 = millis(), t;
int i=0, j=strlen(rep);
char c;

for (i = 0 ; i<j ; ){
        if (btSerial.available()){
                    c=(char)btSerial.read();
                    Serial.print(c);
                    if (c==rep[i])i++;
                    }

        t = millis();
        if (t-t0>2000)break;
        }
    return i==j ;
    delay(10);
    }

Vérification de la connexion en mode commande

Si vous êtes sous linux, vous pouvez dés à présent vérifier le bon fonctionnement du module avec les commandes suivantes:

hcitool  dev

pour afficher l'état de votre interface Bluetooth locale

hcitool  scan

pour afficher les périfériques Bluetooth visibles en portée. Un périférique nommé *hc05-arduino-nano doit apparaître dans la liste ci-dessous, c'est le module HC05 de l'Arduino. Vous pouvez vous y connecter ave la commande:

sudo rfcomm bind 0 xx:xx:xx:xx

ou xx:xx:xx:xx est l'adresse de votre module HC05

Puis envoyer des octets par l'interface Bluetoth:

echo "something" >/dev/comm0

ou recevoir des données:

sudo cat /dev/rfcomm0

enfin vous déconnecter:

sudo rfcomm release 0

Si toutes les commandes ci-dessus fonctionnent, vous pouvez maintenant passer à l'application Qt.

Si vous êtes sous Windows, j'ignore s'il existe des commandes équivalentes. Vous allez surement devoir charger une application spécifique sur un site inconnu avec peut être quelques virus et spams au passage pour faire ceci, mais tout va bien!

Application sous Qt

Vous pouvez télécharger le projet complet

Juste pour donner quelques explications, pour le reste il suffit de lire les commentaires:

Au niveau du fichier de projet (.pro), vous remarquerez la fonctionnalité bluetooth à l'application:

QT       += core gui bluetooth

Dans la déclaration de la classe d'interface MainWindow nous allons utiliser et instancier quelques classes spécifiques:

QBluetoothLocalDevice qui représente l'interface Bluetooth de la machine, une QList de QBluetoothAddress pour récupérer les adresses des périphériques connectés éventuellement, QBluetoothDeviceDiscoveryAgent qui représente le processus de découverte des périphériques Bluetooth proches (et visibles!), enfin QBluetoothSocket, le socket permettant de communiquer.

Pour le reste, il s'agit d'un code classique de classe d'interface avec:

Ses déclarations

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QDebug>


#define MYARDUINO_ADDRESS "20:13:05:21:11:19"
#define MYARDUINO_NAME "hc05-arduino-nano"

Son constructeur

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // Check if Bluetooth is available on this device
    if (!localDevice.isValid()) {
                QMessageBox::critical(this, "Erreur fatale","Bluetooth local inactif!");
                return;
                }

    // Turn Bluetooth on
    localDevice.powerOn();

    // Read local device name
    localDeviceName = localDevice.name();

    // Make it visible to others
    localDevice.setHostMode(QBluetoothLocalDevice::HostDiscoverable);

    // Create a discovery agent and connect to its signals
    discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
    connect(discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),
                this, SLOT(deviceDiscovered(QBluetoothDeviceInfo)));

    // Start a discovery
    discoveryAgent->start();

    // Get connected devices, not used here!
    remotes = localDevice.connectedDevices();

    // Connect to service
    socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol);
    connect(socket, SIGNAL(connected()), this, SLOT(socketConnected()));
    connect(socket, SIGNAL(readyRead()), this, SLOT(dataReady2Read()));

    //    interface
    this->setCursor(Qt::WaitCursor);
    ui->toolButton->setEnabled(false);
}

ses différents SLOTS gérant les évènements

//____________________________________________________________________________________________________
//                      bluetooth device discovery
void MainWindow::deviceDiscovered(const QBluetoothDeviceInfo device)
{
qDebug() << "Found new device:" << device.name() << '(' << device.address().toString() << ')';
QString br ="Found: "+device.address().toString();
ui->textEdit->append(br);                   // show on interface

if (localDevice.pairingStatus(device.address())== QBluetoothLocalDevice::Paired) qDebug()<<"paired";

// device information not used here
int classes = device.serviceClasses();
QList<QBluetoothUuid> uidd = device.serviceUuids();
int rssi = device.rssi();

// we proceed only our remote device (Arduino/HC05 address), nothing for others...
if (device.address().toString()!= MYARDUINO_ADDRESS) return;

// this is my Arduino device
socket->connectToService(device.address(), 1);      //   works under Android but not on Desktop
// here is a misterious ????
// socket->connectToService(device.address(),QBluetoothUuid(QBluetoothUuid::SerialPort));      // only on Desktop

socket->open(QIODevice::ReadWrite);
if (socket->isOpen()){          // write on interface
                ui->textEdit->append("open "+device.address().toString());
                }

this->setCursor(Qt::ArrowCursor);
}

//____________________________________________________________________________________________________
//                  SLOT when data ready on bluetooth socket
void MainWindow::dataReady2Read(){
    QByteArray data;
    data = socket->readAll();
    // juste show on interface
    ui->textEdit->append("receive:"+QString(data));
}

//____________________________________________________________________________________________________
//                  SLOT when socket Connected
void MainWindow::socketConnected(){
   // show on interface
   ui->textEdit->append("Connected to: "+socket->peerAddress().toString()+":"+socket->peerPort());
   ui->toolButton->setIcon(QIcon(":/interOff.png"));
   socket->write("OFF\n");
   // enable the Button
   ui->toolButton->setEnabled(true);
}

//____________________________________________________________________________________________________
//              SLOT button pushed
void MainWindow::on_toolButton_clicked()
{
    static bool state = false;

if (!socket->isOpen() || !socket->isWritable())return;      // a problem

if (state) {        // make the remote led on, writing "ON"
        ui->toolButton->setIcon(QIcon(":/interOn.png"));
        socket->write("ON\n");
        ui->textEdit->append("Send: on");
        }
else    {           // make the remote led off, writing "OFF"
    ui->toolButton->setIcon(QIcon(":/interOff.png"));
    socket->write("OFF\n");
    ui->textEdit->append("Send: off");
    }
state = ! state;    // get the remote led state
}

et enfin son destructeur (je fais partie de la vieille école, je libère systématiquement la mémoire allouée!)

//____________________________________________________________________________________________________
//  destructor
MainWindow::~MainWindow()
{
    if (socket && socket->isOpen()) {
                        socket->close();
                        delete socket;
                        }
    delete ui;
}

Voilà, si tout va bien, quand vous appuyerez sur le bouton, sur l'interface, la led sur la carte Arduino doit s'allumer et s'éteindre .

Il ne vous reste plus qu'à rajouter d'autres commandes, en plus de on et off et de les raccrocher à d'autres fonctionalités ou d'autres capteurs connectés à l'Arduino.

Amusez vous bien !