Site : tvaira.free.fr

Mise en oeuvre d’un périphérique Bluetooth BLE sous Qt/Android

Version PDF de ce document.

Magic Blue LED

C’est une ampoule LED E27 à intensité variable pilotable en Bluetooth :

Achat :

Applications gratuites :

Pré-requis

Lire :

Documentation :

Bluetooth LE et Qt5

Pour utiliser l’API Qt Bluetooth, il faudra commencer par ajouter le module dans le fichier de projet .pro :

QT += bluetooth

L’API fournit les classes Qt suivantes :

Communication Bluetooth LE et Magic Blue LED

Reverse engineering :

Test :

nRF Connect for Mobile :

Outil gatttool :

$ sudo gatttool -i hci0 -b F8:1D:78:63:0D:96 --primary
attr handle = 0x0001, end grp handle = 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle = 0x0008, end grp handle = 0x0008 uuid: 0000fff0-0000-1000-8000-00805f9b34fb
attr handle = 0x0009, end grp handle = 0x000b uuid: 0000ffe5-0000-1000-8000-00805f9b34fb
attr handle = 0x000c, end grp handle = 0xffff uuid: 0000ffe0-0000-1000-8000-00805f9b34fb

$ sudo gatttool -i hci0 -b F8:1D:78:63:0D:96 --characteristics
handle = 0x0002, char properties = 0x02, char value handle = 0x0003, uuid = 00002a00-0000-1000-8000-00805f9b34fb
handle = 0x0004, char properties = 0x02, char value handle = 0x0005, uuid = 00002a01-0000-1000-8000-00805f9b34fb
handle = 0x0006, char properties = 0x02, char value handle = 0x0007, uuid = 00002a04-0000-1000-8000-00805f9b34fb
handle = 0x000a, char properties = 0x0c, char value handle = 0x000b, uuid = 0000ffe9-0000-1000-8000-00805f9b34fb
handle = 0x000d, char properties = 0x10, char value handle = 0x000e, uuid = 0000ffe4-0000-1000-8000-00805f9b34fb

$ sudo gatttool -i hci0 -b F8:1D:78:63:0D:96 --char-write-req --value=56ff000000f0aa --handle=0x000b --listen

Capture : wireshark-gatttool.pcapng

Wireshark :

Capture : btsnoop_hci_led.pcap

Application Qt pour Android

L’application Qt est scindée en deux parties :

  • une interface graphique en QML et,
  • une partie pour gérer le Bluetooth BLE et la communication sous la forme de deux classes C++.

On utilisera le kit de développement Android for armeabi-v7a (GCC 4.9, Qt 5.10.1 for Android armv7).

Le fichier .pro intègre les modules qml, quicket bluetooth :

TEMPLATE = app

QT += qml quick bluetooth
CONFIG += c++11

SOURCES += main.cpp \
    ClientBLE.cpp \
    appareilble.cpp

RESOURCES += qml.qrc

HEADERS += \
    ClientBLE.h \
    appareilble.h

Remarque : Ici, on n’ajoute pas de fichier AndroidManifest.xml personnalisé. C’est Qt qui va le générer à partir des classes que l’on utilise. On peut voir dans le fichier AndroidManifest.xml généré par Qt les spécificités pour le Bluetooth (qu’il faudra donc conserver si un jour on intègre son propre fichier AndroidManifest.xml) : QtAndroidBluetooth.jar et QtAndroidBluetooth-bundled.jar, la classe QtBluetoothBroadcastReceiver et les permissions

<?xml version='1.0' encoding='utf-8'?>
<manifest ...>
...
   <meta-data android:name="android.app.load_local_jars" android:value="jar/QtAndroid.jar:jar/QtAndroid-bundled.jar:jar/QtAndroidBearer.jar:jar/QtAndroidBearer-bundled.jar:jar/QtAndroidExtras.jar:jar/QtAndroidExtras-bundled.jar:jar/QtAndroidBluetooth.jar:jar/QtAndroidBluetooth-bundled.jar"/>
   <meta-data android:name="android.app.static_init_classes" android:value="org.qtproject.qt5.android.bluetooth.QtBluetoothBroadcastReceiver:org.qtproject.qt5.android.bluetooth.QtBluetoothBroadcastReceiver"/>

...
   <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />   
</manifest>

Pour la partie C++, on aura deux classes :

  • ClientBLE : pour rechercher les périphériques Magic Blue Led et communiquer avec
  • AppareilBLE : pour fournir le nom et l’adresse (sous la forme de propriétés Qt) d’un périphérique Magic Blue Led

Le fichier main.cpp instanciera un objet ClientBLE, l’associera au document QML puis chargera le tout :

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>

#include "ClientBLE.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;

    ClientBLE *clientBLE = new ClientBLE();
    engine.rootContext()->setContextProperty("ClientBLE", clientBLE);

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

Dans la classe ClientBLE, il faut commencer par découvrir les périphériques Magic Blue Led à proximité. Pour cela il faut créer une instance de QBluetoothDeviceDiscoveryAgent, fixer un timeout pour la recherche, connecter les signaux/slots et appeler start() :

m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent();

m_discoveryAgent->setLowEnergyDiscoveryTimeout(5000);

// Slot pour la recherche d'appareils BLE
connect(m_discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)), this, SLOT(ajouterAppareil(QBluetoothDeviceInfo)));
connect(m_discoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), this, SLOT(rechercheErreur(QBluetoothDeviceDiscoveryAgent::Error)));
connect(m_discoveryAgent, SIGNAL(finished()), this, SLOT(rechercheTerminee()));

m_discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);

Le slot ajouterAppareil() aura pour rôle de filtrer les périphériques Magic Blue Led puis de les stocker dans une liste d’objets AppareilBLE.

void ClientBLE::ajouterAppareil(const QBluetoothDeviceInfo &info)
{
    // Bluetooth Low Energy ?
    if (info.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration)
    {
        // Magic Blue Led ?
        if(info.name().startsWith("LEDBLE"))
        {
            AppareilBLE *a = new AppareilBLE(info.name(), info.address().toString(), this);
            m_devices.append(a);
            m_appareilDetecte = true;
        }
    }
}

Le slot rechercheTerminee() est déclenchée à la fin de la recherche et émet les signaux vers l’interface QML. Le slot rechercheErreur() sera lui utilisé en cas d’erreur : la plus probable étant la désactivation du Bluetooth sur le terminal mobile Android.

void ClientBLE::rechercheTerminee()
{
    m_etatRecherche = false;
    emit recherche();
    emit detecte();
    emit appareilsUpdated();
}

void ClientBLE::rechercheErreur(QBluetoothDeviceDiscoveryAgent::Error erreur)
{
    m_etatRecherche = false;
    emit recherche();
    emit detecte();
    emit appareilsUpdated();
}

Pour interfacer la recherche assurée par la classe et la partie QML, on aura besoin :

  • de trois propriétés : appareilDetecte, etatRecherche et listeAppareils
    • associées à trois attributs : m_appareilDetecte, m_etatRecherche et m_devices
    • et notifiées par trois signaux : detecte(), recherche() et appareilsUpdated()
  • et de deux méthodes appelables par la GUI QML : rechercher() et arreter()

La déclaration partielle de la classe ClientBLE pour la partie recherche de périphériques Magic Blue Led :

class ClientBLE : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool appareilDetecte MEMBER m_appareilDetecte NOTIFY detecte)
    Q_PROPERTY(bool etatRecherche MEMBER m_etatRecherche NOTIFY recherche)
    Q_PROPERTY(QVariant listeAppareils READ getAppareils NOTIFY appareilsUpdated)

public:
    ClientBLE();
    ~ClientBLE();
    Q_INVOKABLE void rechercher();
    Q_INVOKABLE void arreter();
    QVariant getAppareils();

protected slots:
    void ajouterAppareil(const QBluetoothDeviceInfo&);
    void rechercheTerminee();
    void rechercheErreur(QBluetoothDeviceDiscoveryAgent::Error);

private:
    QList<QObject*> m_devices; // liste de périphériques Magic Blue
    QBluetoothDeviceDiscoveryAgent  *m_discoveryAgent; // pour la recherche des périphériques
    bool m_etatRecherche; // état de la recherche
    bool m_appareilDetecte; // indique si au moins un périphérique a été détecté

signals:
    void recherche();
    void detecte();
    void appareilsUpdated();
};

La GUI QML est construite autour d’un élément Window et une disposition avec des ColumnLayout et RowLayout. La recherche est gérée par un ToggleButton (on fera de même pour la connexion, qui n’est possible que si au moins un périphérique Magic Blue Led est détecté). Comme on a fixé un timeout de 5 s pour la détection des périphériques, on ajoutera un BusyIndicator (qu’on utilisera aussi pour la connexion).

La définition partielle de la partie QML dédiée à la recherche de périphériques Magic Blue Led :

import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 2.2
import QtQuick.Extras 1.4
import QtQuick.Layouts 1.3

Window {
    id: window
    title: qsTr("Magic Blue LED BLE")
    visible: true
    width: Screen.desktopAvailableWidth
    height: Screen.desktopAvailableHeight
    color: "#272822"
    
    property bool recherche: ClientBLE.etatRecherche
    onRechercheChanged: {
        if (ClientBLE.etatRecherche)
        {
            boutonRecherche.text = "Arrêter";
            boutonRecherche.checked = true;
            indicateur.running = true;
            message.text = qsTr("Recherche en cours")
        }
        else
        {
            boutonRecherche.text = "Rechercher";
            boutonRecherche.checked = false;
            indicateur.running = false;
            message.text = qsTr("Recherche finie")
        }
    }
    
    property bool detecte: ClientBLE.appareilDetecte
    onDetecteChanged: {
        if (!ClientBLE.appareilDetecte)
        {
            boutonConnexion.enabled = false;
            message.text = qsTr("Aucun Magic Blue Led trouvé !")
        }
    }

    ... 
    
    ToggleButton {
            id: boutonRecherche;
            width: Screen.desktopAvailableWidth/6
            height: Screen.desktopAvailableHeight/6
            text: ClientBLE.etatRecherche ? qsTr("Arrêter") : qsTr("Rechercher");
            checked: ClientBLE.etatRecherche;
            onClicked: {
                indicateur.running = true;
                ClientBLE.etatRecherche ? ClientBLE.arreter() : ClientBLE.rechercher();
            }
     }
     
     ...    
}

On utilisera un ListView, associée à la QList, pour afficher le nom et l’adresse des Magic Blue détectés :

ListView {
        id: listeAppareils
        width: parent.width
        anchors { fill: parent; margins: 2 }
        spacing: 5
        model: ClientBLE.listeAppareils

         // l'affichage des éléments de la liste
        delegate: Rectangle {
            id: appareil
            anchors.horizontalCenter: parent.horizontalCenter
            height: 80
            width: parent.width/4
            color: "lightsteelblue"
            border.width: 2
            border.color: "#cecece"
            radius: 5

            MouseArea {
                anchors.fill: parent
                onClicked: {
                    listeAppareils.currentIndex = index;
                    message.text = model.modelData.nom;
                    boutonConnexion.enabled = true;
                }
            }
            
            // le nom et l'adresse des périphériques Bluetooth
            Text {
                id: deviceName
                font.pointSize: 20
                anchors.horizontalCenter: parent.horizontalCenter
                color: "#A6A6A6"
                horizontalAlignment: Text.AlignHCenter
                elide: Text.ElideMiddle
                width: parent.width
                wrapMode: Text.Wrap
                text: model.modelData.nom
                anchors.top: parent.top
                anchors.topMargin: 5
            }
            Text {
                id: deviceAddress
                font.pointSize: 20*0.7
                anchors.horizontalCenter: parent.horizontalCenter
                color: "#A6A6A6"
                horizontalAlignment: Text.AlignHCenter
                elide: Text.ElideMiddle
                width: parent.width
                wrapMode: Text.Wrap
                text: model.modelData.adresseMAC
                anchors.bottom: appareil.bottom
                anchors.bottomMargin: 5
            }
        }                
}

Si l’utilisateur clique sur un des périphériques Magic Blue détecté, son nom sera affiché dans un Text et on pourra s’y connecter afin de le piloter.

Le principe de communication avec un périphérique Bluetooth BLE utilisé ensuite dans l’application a déjà été décrite dans cet exemple : Mise en oeuvre du Bluetooth BLE sous Qt.

L’API Qt permet de créer des connexions avec des périphériques, de découvrir leurs services, ainsi que de lire et d’écrire des données stockées sur le périphérique à partir d’une instance de la classe QLowEnergyController.

La méthode connecterAppareil(const QString &adresseServeur) est appelée au moment de la demande connexion et on lui passe en argument l’adresse MAC du périphérique BLE à joindre. On connecte les slots dont on a besoin : connexion/déconnexion et les services à découvrir. On démarre avec connectToDevice().

Ensuite, la méthode ajouterService() va être déclenchée pour chaque service découvert. On appelle alors createServiceObject() en lui passant l’UUID du service pour créer une instance de ce service. On appelle notre méthode connecterService() pour découvrir les caractéristiques du service. On connecte les slots dont on a besoin et on lance la découverte avec discoverDetails(). Pour chaque caractéristique découverte, le slot serviceDetailsDiscovered() sera déclenché.

Pour piloter la Magic Blue Led, on fournit des méthodes write() :

void ClientBLE::write(const QByteArray &data)
{
    if(m_service && m_characteristic.isValid())
    {
        if (m_characteristic.properties() & QLowEnergyCharacteristic::Write)
        {            
            if(data.length() <= MAX_SIZE)
            {
                m_service->writeCharacteristic(m_characteristic, data, QLowEnergyService::WriteWithoutResponse);
            }
        }
    }
}

// RGB
void ClientBLE::write(int rouge, int vert, int bleu, int white/*=0*/)
{
    Q_UNUSED(white)
    QByteArray datas(7, 0);

    datas[0] = 0x56;
    datas[1] = static_cast<char>(rouge); //RR
    datas[2] = static_cast<char>(vert); // GG
    datas[3] = static_cast<char>(bleu); // BB
    datas[4] = 0x00; // WW
    datas[5] = 0xf0;
    datas[6] = 0xaa;

    write(datas);
}

// On/Off
void ClientBLE::write(bool etat)
{
    QByteArray datas(3, 0);

    datas[0] = 0xcc;
    if(etat)
        datas[1] = 0x23;
    else
        datas[1] = 0x24;
    datas[2] = 0x33;

    write(datas);
}

On obtient :

On lance la recherche :

On séléctionne la Magic Blue Led, on se connecte et on la pilote :

Code source : ClientLedBLE.zip

Retour au sommaire