Kivy-Android et Arduino en Bluetooth Low Energy (BLE)

(actualisé le ) par wlaidet

L’ensemble du projet :

Voici l’ensemble du projet. Vous trouverez dans ce zip :

  • Le fichier .ino à transférer dans l’Arduino (Uno) ;
  • Le fichier .java permettant de surcharger la classe BluetoothGattCallback ;
  • Le fichier main.py contenant le code de l’application à compiler avec buildozer ;
  • Le fichier buildozer.spec contenant les informations de l’application Android, les droits BLUETOOTH et BLUETOOTH_ADMIN ainsi que le lien vers le fichier .java ;
Projet_complet

Je vous conseille de lire les remarques suivantes et l’ensemble des explications afin de comprendre un peu mieux le fonctionnement et les éventuelles modifications que vous pourriez avoir à faire.

Voici l’apk. C’est un zip donc il faut l’extraire et autoriser les sources inconnues sur votre Android.

Une vidéo de démonstration :

Remarques importantes :

  • Il existe deux types de Bluetooth (pour faire simple). Le Bluetooth classique (mode EDR) et le Low Energy (mode BLE) ;
  • Cette application n’est valable que sous Android (à partir de 4.4.... peut-être 4.3 ?). Pas besoin de rooter la tablette.
  • Nous avons acheté un Grove BLE v1 qui n’est pas dual-mode (il ne fait que BLE). Le service qui nous intéresse est le FFE0 qui contient la caractéristique FFE1 (Voir les UUID dans l’application Kivy) ;
  • Il faut ouvrir les connexions Bluetooth et appairer le Grove BLE avant de lancer l’application (Mot de passe : "000000") ;
  • Si vous partez en mode EDR, vous pouvez regarder ici : https://gist.github.com ;
  • Le fait que le mode EDR ne soit pas supporté par ce périphérique provoque des gels des applications qui tentent de se connecter dans ce mode (Applications AppInventor par exemple) ;
  • Je n’ai trouvé qu’une seule discussion concernant Kivy et le BLE : groups.google
  • Un site qui décrit bien les possibilités : http://developer.android.com ;
  • J’ai rencontré deux difficultés : implémenter une classe java abstraite et comprendre les valeurs que recevait le Arduino. (Un grand merci à Pierre et Yo !)

Commençons....

Adapter la class java BluetoothGattCallback pour pouvoir l’implémenter ensuite :

Créez un fichier que vous pouvez appelez BluetoothGattImplem.java :

BluetoothGattImplem.java
package org.myapp;

import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothGattService;
import android.util.Log;

public class BluetoothGattImplem extends BluetoothGattCallback {

      public interface OnBluetoothGattCallback {
       // ... all the methods from BluetoothGattCallback here ...like:
         void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic);

         void onConnectionStateChange(BluetoothGatt gatt, int status, int newState);

         void onServicesDiscovered(BluetoothGatt gatt, int status);

         void onCharacteristicRead(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status);
     
    }
    // private storage for the callback object
    private OnBluetoothGattCallback callback = null;

    // method to set the callback object
    public void setCallback(OnBluetoothGattCallback callback) {
        this.callback = callback;
    }
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        if (this.callback != null)
            this.callback.onCharacteristicChanged(gatt, characteristic);
    }
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        if (this.callback != null)
            this.callback.onConnectionStateChange(gatt, status, newState);
    }
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        if (this.callback != null)
            this.callback.onServicesDiscovered(gatt, status);
    }
    public void onCharacteristicRead(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status) {
        if (this.callback != null)
            this.callback.onCharacteristicRead(gatt,characteristic,status);
    }
}

Télécharger

Application Python-Kivy :

Pour communiquer avec le Grove BLE il faut suivre un processus très précis :

  • Il faut ajouter le fichier java dans le dossier du projet (le même que celui où vous allez mettre le main.py suivant ;
  • Ajouter certains paramètres dans le fichier buildozer.spec à savoir, les autorisations Bluetooth et la prise en compte du fichier java pour pyjnius. Voici le fichier buildozer.spec :
buildozer.spec
  • Se connecter avec un ConnectGatt et attendre la connexion ;
  • Découvrir les services du BLE et attendre la fin de cette recherche ;
  • Attraper la Charactéristic qui permet d’envoyer des données au BLE ;
  • Mettre une valeur dans cette Charactéristic. La méthode utilisée est setValue(nombre, format, offset). Le format est à 17 ici pour Uint8.
  • Envoyer cette valeur au BLE ("writeCharacteristic").

Cette application comprend une interface utilisateur minimum :

  • Un bouton de connexion (qui recherche aussi la Charactéristic) ;
  • Un TextInput qui ne prend que des nombres (On envoye des chiffres !) ;
  • Un bouton Send pour envoyer le contenu du TextInput s’il est valable.

N’oubliez pas deux choses dans cette application :

  • Le Grove BLE doit être appairé et se nommer "HMSoft".
  • Il n’y a pas de LEscan dans cette application.... à faire...
  • Le nom du BLE doit être HMSoft. Changez ce nom dans l’application avant de compliler si vous n’avez pas le même nom.
main.py

Le code :

'''
Bluetooth/Pyjnius example
=========================
This was used to send some bytes to an arduino BLE.
The app must have BLUETOOTH and BLUETOOTH_ADMIN permissions.
Connect your device to your phone, via the bluetooth menu. After the
pairing is done, you'll be able to use it in the app.
'''


import kivy
kivy.require('1.8.0')

from jnius import PythonJavaClass, java_method
from jnius import autoclass
from jnius import cast
from kivy.lang import Builder
from kivy.app import App
from kivy.logger import Logger
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout
import time
import struct

BluetoothAdapter = autoclass('android.bluetooth.BluetoothAdapter')
BluetoothDevice = autoclass('android.bluetooth.BluetoothDevice')
BluetoothSocket = autoclass('android.bluetooth.BluetoothSocket')
BluetoothGatt= autoclass('android.bluetooth.BluetoothGatt')
BluetoothGattCallback = autoclass('android.bluetooth.BluetoothGattCallback')
UUID = autoclass('java.util.UUID')
List = autoclass('java.util.List')
Context = autoclass('android.content.Context')
PythonActivity = autoclass('org.renpy.android.PythonActivity')
BluetoothGattService=autoclass('android.bluetooth.BluetoothGattService')
activity = PythonActivity.mActivity
btManager = activity.getSystemService(Context.BLUETOOTH_SERVICE)
Intent = autoclass('android.content.Intent')
BluetoothProfile = autoclass('android.bluetooth.BluetoothProfile')
Service=autoclass('android.app.Service')
etatconnexion=0

class PyBluetoothGattCallback(PythonJavaClass):
    __javainterfaces__ =["org/myapp/BluetoothGattImplem$OnBluetoothGattCallback"]
    __javacontext__ = 'app'

    @java_method('(Landroid/bluetooth/BluetoothGatt;II)V')
    def onConnectionStateChange(self, gatt, status, newstate):
        global etatconnexion
        Logger.info('%s' % newstate)
        etatconnexion=newstate
   
    @java_method('(Landroid/bluetooth/BluetoothGatt;I)V')
    def onServicesDiscovered(self, gatt, status):
        global servicesdiscovered
        Logger.info('%s' % status)
        servicesdiscovered=status#0 si discovered


BluetoothGattImplem = autoclass('org/myapp/BluetoothGattImplem')
pycallback = PyBluetoothGattCallback()
bg = BluetoothGattImplem()
bg.setCallback(pycallback)

def try_connect(name):
    global etatconnexion,servicesdiscovered
   
    servicesdiscovered=1
    BluetoothAdapter=btManager.getAdapter()
   
    paired_devices = BluetoothAdapter.getDefaultAdapter().getBondedDevices().toArray()
   
    if len(paired_devices)==0: #Existe-t-il des peripheriques appaires
        return None, None
   
    for device in paired_devices:
        if device.getName() == name:
            break
   
    if device.getName()!=name: #Existe-t-il un peripherique du nom 'HMSoft'
        return None, None
   
    BluetoothGatt=None
   
    BluetoothGatt = device.connectGatt(Service, 0, bg)
   
    compteur=0
    while etatconnexion!=2:
        compteur+=1
        time.sleep(0.1)
        if compteur==100:#Delai de connexion depasse
            Logger.info('%s' % "Toto1")
            Logger.info('%s' % etatconnexion)
            return None, None
   
    BluetoothGatt.discoverServices()#Il faut que ca marche pour recup un service
   
    compteur=0
    while servicesdiscovered!=0:
        compteur+=1
        time.sleep(0.1)
        if compteur==100:#Delai de decouverte des services depasse
            return None, None
   
    service = BluetoothGatt.getService(UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"))
    try:
        characteristic = service.getCharacteristic(UUID.fromString("0000ffe1-0000-1000-8000-00805F9B34FB"))
    except:
        return None, None
   
    return BluetoothGatt, characteristic

class BluetoothApp(App):
    def build(self):
        #On créé une disposition pour l'affichage:
        Layout=BoxLayout(orientation='vertical',spacing=20,padding=(200,20))
        self.BoutonConnect=Button(text='Connect')
        self.BoutonConnect.bind(on_press=self.connect)
        #On ajoute le bouton dans l'affichage:
        Layout.add_widget(self.BoutonConnect)
        #un textinput:
        self.Input1 = TextInput(text="",font_size=30)
        Layout.add_widget(self.Input1)
        self.BoutonSend=Button(text='Send')
        self.BoutonSend.bind(on_press=self.send)
        #On ajoute le bouton dans l'affichage:
        Layout.add_widget(self.BoutonSend)
        self.gatt=None
        self.charac=None
       
        return Layout
   
    def connect(self,instance):
        try:
            BluetoothGatt.disconnect()
        except:
            pass
        instance.background_color=[1,1,0,1]
        instance.text="Wait for connexion"
        self.gatt, self.charac = try_connect('HMSoft')
        if self.charac!=None:
            instance.background_color=[0,1,0,1]
            instance.text="connected"
        else:
            instance.background_color=[1,0,0,1]
            instance.text="echec de connexion : un nouvel essai"

    def send(self, cmd):
        global etatconnexion
        if self.charac!=None:
            try :
                nb=int(self.Input1.text)
            except:
                cmd.text="Send : Il faut un nombre"
                self.Input1.text=""
            if etatconnexion==2:
                for chiffre in self.Input1.text:
                    self.charac.setValue(int(chiffre),17,0)
                    BluetoothGatt.writeCharacteristic(self.charac)
            else:
                self.BoutonConnect.background_color=[1,0,0,1]
                self.BoutonConnect.text="echec de connexion : un nouvel essai"

if __name__ == '__main__':
    BluetoothApp().run()

Télécharger

Côté Arduino :

  • Branchez le BLE sur les broches 2 et 3 (D2 sur le GroveShield) ;
  • Branchez le Grove_RGB_LCD sur un port I2C ;
  • Envoyez le code suivant ;
  • N’hésitez pas à débrancher puis rebrancher le ArduinoUno ;

Remarques :

  • La recherche du bon BaudRate fut difficile (nous sommes ici en 38400 pour le BLE) ;
  • Un problème de bit de poids fort qui nous a imposé d’enlever 80 (en hexa) à la valeur lue. (En effet, l’aspect signé ou non signé du nombre n’a rien changé... Si vous avez une idée ?)
  • L’application Kivy envoie un par un les chiffres qui composent le nombre. La raison étant qui la lecture sur le Arduino s’arrête à 63. On obtient 0 pour 64 puis 1 pour 65. De même, 0 pour 640 puis 1 pour 641... Si quelqu’un a une idée ?
  • Ce code affiche chaque chiffre envoyé seconde par seconde.
ble11_chiffres.ino
#include <SoftwareSerial.h>

#define RxD 2
#define TxD 3

SoftwareSerial bleShield(RxD,TxD);


//Imports des librairies pour le Grove_LCD:
#include <Wire.h>
#include "rgb_lcd.h"
//Définition de la sortie pour afficher sur le LCD
rgb_lcd lcd;
//Déclaration des variables
const int colorR = 255;//Rouge
const int colorG = 0;
const int colorB = 0;

unsigned char tab[256];
int index = 0;
boolean dataAvailable = false;

void setup()
{
  Serial.begin(19200);
  pinMode(RxD, INPUT);//R
  pinMode(TxD, OUTPUT);//T
  bleShield.begin(38400);
  bleShield.print("AT+CLEAR");
  lcd.begin(16, 2);
  lcd.setRGB(colorR, colorG, colorB);
  lcd.print("Bonjour !");
  delay(1000);
}

void loop()
{  
  delay(200);
 
  while(bleShield.available()>0)//On lit tous les chiffres et on stock dans tab
  {
    tab[index++] = bleShield.read()-0x80;
  }
  if (index > 0)
  {
    for(int i = 0 ; i < index ; i++)//On parcourt tab pour afficher les chiffres dans l'ordre
    {
      int chiffre = tab[i];//conversion en int
      lcd.setCursor(0, 1);// mettre le curseur sur colonne 0, ligne 1
      lcd.print(chiffre);
      delay(1000);//attente de 1seconde par affichage
    }
   
    index = 0;
  }
}

Télécharger