[WikiItic] [TitleIndex] [WordIndex

Aplicació Bluetooth Low Energy per a Android

Directoris aplicació

Aquesta pàgina forma part de l'assignatura d'Integració de Sistemes i detalla els passos que cal seguir per a comunicar un smartphone Android amb dispositius radino a través del protocol Bluetooth Low Energy (BLE). L'aplicació Android s'encarregarà de gestionar la comunicació BLE, l'adquisició de mostres i la representació gràfica per pantalla.

Per a executar l'aplicació en un dispositiu físic cal que aquest disposi de Bluetooth v4. Pel que fa al radino s'utilitzarà el programa realitzat en l'assignatura d'Enginyeria de Sistemes.

Instal·lació d'Android Studio

Android Studio és l'entorn de desenvolupament (IDE) oficial de Google basat en el software IntelliJ IDEA. La plataforma conté editor, compilador, debugger, emulador i altres eines que faciliten el desenvolupament d'aplicacions. Per a utilitzar-lo des d'Ubuntu cal disposar de les llibreries de java. En cas de no tenir-les es poden instal·lar amb:

   1 apt-get install openjdk-7-jdk

Android Studio s'obté de la pàgina oficial. Encara que s'ofereixi la possibilitat d'instal·lar només la SDK per a utilitzar-la amb un altre IDE és recomanable baixar la versió completa amb totes les eines. Una vegada obtingut el fitxer, cal descomprimir-lo (el directori /opt està reservat als programes no inclòsos en el repositori oficial) i executar l'script d'instal·lació:

   1 sudo cp android-studio-ide-141.2117773-linux.zip /opt
   2 cd /opt
   3 sudo unzip android-studio-ide-141.2117773-linux.zip
   4 cd android-studio/bin
   5 ./studio.sh

A continuació s'executarà l'instal·lador gràfic, en el qual es poden acceptar les opcions per defecte. Per a què el programa estigui disponible des del menú d'escriptori d'Ubuntu o línia de comandes podem activar les opcions Tools → Create Desktop Entry... i Tools → Create Command-line Launcher. Per a obtenir les eines específiques per a un determinat dispositiu o tipus d'aplicació cal entrar a Tools → Android → SDK Manager. Els paquets recomanables són:

Una eina addicional és l'emulador de dispositius mòbils Tools → Android → Android Device Monitor, el qual pot resultar útil per a testejar aplicacions simples en múltiples formats, però lenta en aplicacions complexes. També s'ha de tenir en compte que algunes funcionalitats, com per exemple la comunicació Bluetooth, només es poden testejar des d'un dispositiu físic.

Hello World

Una vegada finalitzat el procés d'instal·lació i configuració de l'IDE ja es pot crear un nou projecte: File → New → New Project. Primer es demana nom i directori de l'aplicació. Seguidament s'han d'escollir les característiques de la plataforma (per a Bluetooth v4 haurà de ser com a mínim la 18) i plantilla Blank Activity. Finalment s'especifica el nom de l'activitat i carpetes principals del projecte.

Un cop creat el projecte es pot navegar pels seus directoris des del panell de l'esquerra seleccionant les pestanyes Project i Android. L'estructura de qualsevol aplicació és la següent:

Directoris aplicació

Per a provar l'exemple des de l'emulador o dispositiu físic cal prémer RunRun app i seleccionar el dispositiu a Choose a running device. En el cas del mòbil s'han d'haver habilitat les opcions de desenvolupador i debugging USB.

Desenvolupament

Si el programa HelloWorld s'executa des de l' smartphone correctament, és hora de començar el desenvolupament de l'aplicació. El seu comportament serà definit en codi java i el disseny gràfic en llenguatge xml. Alguns components, com ara els botons, s'han de declarar en les dues parts. També s'utilitzen les carpetes de recursos per a definir strings, imatges...

El desenvolupament de l'aplicació d'Integració de Sistemes es pot dividir en els següents apartats:

Programa principal

Cada pantalla d'una aplicació Android s'anomena Activity i s'encarrega de gestionar la interfície d'usuari. Hi poden haver diverses activitats independents, encara que sempre n'hi haurà una de principal per a la primera pantalla, que normalment s'anomena MainActivity.java i té la següent estructura:

   1 package com.example.proves.tutorial;
   2 
   3 import android.app.Activity;
   4 import android.os.Bundle;
   5 
   6 public class MainActivity extends Activity {
   7 
   8   @Override
   9   protected void onCreate(Bundle savedInstanceState) {
  10     super.onCreate(savedInstanceState);
  11     setContentView(R.layout.activity_main);
  12   }
  13 }

El package és el nom que rep l'agrupació de les diferents classes de l'aplicació, seguidament apareixen els includes (Android Studio indica els que s'han d'afegir) i finalment el cos del programa. La funció onCreate és executada a l'iniciar l'aplicació.

Sobreescriure mètodes d'una activitat és útil per a realitzar accions segons el seu cicle de vida (onDestroy, onStop, onResume...).

La funció setContentView s'utilitza per a associar una vista (o layout) a l'activitat. Aquest es pot modificar des de res → layout → activity_main.xml.

Layout

En el layout es defineix l'aspecte que tindrà la interfície d'usuari i la disposició dels seus components. La pantalla inicial serà LinearLayout amb tres components:

Layout principal

   1 <?xml version="1.0" encoding="utf-8"?>
   2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   3     android:layout_width="match_parent"
   4     android:layout_height="match_parent"
   5     android:orientation="vertical" >
   6 
   7     <Button
   8         android:id="@+id/connect"
   9         android:layout_width="match_parent"
  10         android:layout_height="wrap_content"
  11         android:layout_weight="0.1"
  12         android:text="Connect" />
  13 
  14     <LinearLayout
  15         android:id="@+id/graph_layout"
  16         android:layout_width="match_parent"
  17         android:layout_height="wrap_content"
  18         android:layout_weight="1"
  19         android:orientation="vertical" />
  20 
  21     <Button
  22         android:id="@+id/startstop"
  23         android:layout_width="match_parent"
  24         android:layout_height="wrap_content"
  25         android:layout_weight="0.1"
  26         android:text="Start" />
  27 
  28 </LinearLayout>

Versió de render

Es pot determinar la mida dels components de forma numèrica o bé relativa (wrap_content s'adapta a l'objecte i match_parent l'amplia al contenidor). En cas que un objecte hagi d'ocupar més espai que un altre, se li pot assignar un pes diferent. També és important el seu identificador, ja que servirà per a referenciar-los des del codi java.

El resultat d'aquest codi xml es pot veure gràficament activant la pestanya inferior Design.

Si apareix el missatge "Rendering Problems: This version of the rendering library is more recent than your version of Android Studio. Please update Android Studio (Details)" cal activar una altra versió de renderització des de la pestanya superior de la finestra de disseny tal i com es mostra en la figura de la dreta.

Botons

Una vegada definits, els components gràfics es poden cridar des del MainActivity a través de l'identificador. Els botons són accessibles a tots els mètodes de forma global però els inicialitzem dins la funció onCreate. Per a gestionar quan s'ha apretat un botó s'utilitza un listener, del qual se'n pot modificar el mètode onClick:

   1 private Button btnConnect, btnStartStop;
   2 
   3 @Override
   4 public void onCreate(Bundle savedInstanceState) {
   5 
   6   super.onCreate(savedInstanceState);
   7   setContentView(R.layout.main);
   8   btnConnect = (Button) findViewById(R.id.connect);
   9   btnStartStop = (Button) findViewById(R.id.startstop);
  10 
  11   btnConnect.setOnClickListener(new View.OnClickListener(){
  12 
  13     @Override
  14     public void onClick(View v) {
  15       if (btnConnect.getText().equals("Connect")) {
  16         btnConnect.setText("Disconnect");
  17       }
  18       else{
  19         btnConnect.setText("Connect");}
  20       }
  21     });
  22    
  23     // Falta botó start/stop 
  24     ...

Debugging

Android Studio ofereix un mecanisme de log per a debuggar les aplicacions. La classe Log permet crear missatges accessibles des del logcat d'Android Studio. Existeixen cinc nivells de log: verbose(v), debug(d), information(i), warning(w) i error(e). Els missatges de log van associats a una etiqueta en la qual s'indica des de quin mòdul o funció s'ha cridat. Exemples de missatge informatiu, de debug o error són:

   1 Log.v("MyActivity", "Les dades són: " + dades);
   2 Log.d("ExampleCode", "El codi s'està executant...");
   3 Log.e("ClassTwo", "Hi ha hagut un greu error!");

El resultat serà comprovat en la finestra de logcat quan s'executi l'aplicació:

Directoris aplicació

Bluetooth Low Energy per a Android

Android Bluetooth classes En la comunicació BLE els dispositius poden pendre dos rols: perifèric o central. En aquest muntatge els radinos actuaran com a perifèrics i l'aplicació Android serà el node central. Els protocols utilitzats són Generic Access Profile (GAP) i Generic Attribute Profile (GATT). Per a més informació es poden consultar els tutorials d' OCW - Enginyeria de Sitemes i Adafruit o la pàgina de referència de Bluetooth Development portal.

Les classes o objectes d'Android que s'utilitzen per a treballar amb Bluetooth Low Energy són:

El manager i adapter fan referència al sistema Bluetooth del mòbil, per tant n'hi ha prou amb instanciar-los una sola vegada. El device, gatt i callback, en canvi, representen un perifèric concret i per tant s'han de crear objectes per a cadascun d'ells. La sintaxi és la següent:

   1 // Les variables s'han d'haver declarat anteriorment com a globals
   2 manager = (BluetoothManager) this.getSystemService(Context.BLUETOOTH_SERVICE);
   3 adapter = manager.getAdapter();
   4 device = adapter.getRemoteDevice(address);
   5 gatt = device.connectGatt(this, false, callback);

En cas de no conéixer l'adreça del perifèric es pot utilitzar l'aplicació oficial de Nordic Semiconductors.

Els paràmetres per a crear el gatt són el context de l'aplicació, l'habilitació de connexió automàticament i el callback que atendrà els esdeveniments. En aquesta aplicació intervenen tres esdeveniments:

  1. Connexió establerta: L'aplicació s'ha connectat al perifèric i es pot iniciar el procés de detecció de serveis
  2. Serveis descoberts: Una vegada descoberts els serveis es poden enviar comandes GATT, p. ex. habilitar les notificacions
  3. Canvi en una característica: Indica si s'ha rebut una notificació d'un perifèric

Per a crear el callback es pot utilitzar la classe ja existent BluetoothGattCallback. Només caldrà sobreescriure'n alguns mètodes:

   1 callback = new BluetoothGattCallback(){
   2 
   3   @Override
   4   public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
   5 
   6     if (newState == BluetoothProfile.STATE_CONNECTED){
   7       // S'ha establert la connexió amb el perifèric
   8       gatt.discoverServices();    
   9     }
  10   }
  11 
  12   @Override
  13   public void onServicesDiscovered(BluetoothGatt gatt, int status) {
  14     // S'han descobert els serveis del perifèric
  15     enableTXNotification();
  16   }
  17 
  18   @Override
  19   public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
  20     // S'ha rebut una notificació, el seu valor s'obté amb characteristic.getValue();
  21   }
  22 }

La funció enableTXNotification s'encarrega de detectar el servei UART i habilitar les notificacions de la característica de transmissió per a què el radino pugui enviar dades de forma autònoma:

   1 public void enableTXNotification() {
   2 
   3   final UUID UART_SERVICE = UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
   4   final UUID TX_CHAR = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
   5   final UUID CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
   6 
   7   BluetoothGattService UartService = gatt.getService(UART_SERVICE);
   8   if (UartService != null) {
   9     BluetoothGattCharacteristic TxChar = UartService.getCharacteristic(TX_CHAR);
  10     if (TxChar != null) {
  11       gatt.setCharacteristicNotification(TxChar, true);
  12       BluetoothGattDescriptor descriptor = TxChar.getDescriptor(CCCD);
  13       descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
  14       gatt.writeDescriptor(descriptor);
  15     }
  16   }
  17 }

Classe AccelData

Per a facilitar el tractament de les mostres rebudes en la funció onCharacteristicChanged es pot crear una classe específica per a l'acceleròmetre amb els següents atributs:

La classe AccelData comptarà amb mètodes per accedir i modificar els atributs. També tindrà la funció toString, útil en cas que es vulgui guardar el valor de la mostra en format de text pla.

   1 public class AccelData {
   2   private long timestamp;
   3   private double x;
   4   private double y;
   5   private double z;
   6   private int usuari;
   7   private int sensor;
   8 
   9   public AccelData(int sensor, long timestamp, double x, double y, double z) {
  10     this.timestamp = timestamp;
  11     this.x = x;
  12     this.y = y;
  13     this.z = z;
  14     this.usuari = 1;
  15     this.sensor = sensor;
  16   }
  17 
  18   public long getTimestamp() {
  19     return timestamp;
  20   }
  21   public void setTimestamp(long timestamp) {
  22     this.timestamp = timestamp;
  23   }
  24 
  25   ...
  26 
  27   public String toString(){
  28     return timestamp+","+sensor+","+x+","+y+","+z+";";
  29   }
  30 }

Les instàncies d' AccelData que es creiïn seran afegides a un ArrayList global amb l'objectiu de recuperar-les posteriorment.

   1   private ArrayList<AccelData> results = new ArrayList<>();;

Connexió / Desconnexió

Finalment falten les funcions per a connectar-se i desconnectar-se del perifèric, les quals s'hauran de cridadr des del listener del botó connect/disconnect:

   1 public boolean connect(final String address) {
   2 
   3   final BluetoothDevice device = adapter.getRemoteDevice(address);
   4   if (device == null) {
   5     Log.w("Ap", "Device not found.  Unable to connect.");
   6     return false;
   7   }
   8   gatt = device.connectGatt(getApplicationContext(), false, callback);
   9   return true;
  10 }

   1 public void disconnect() {
   2   if (adapter != null && gatt != null) {
   3     gatt.disconnect();
   4   }
   5 }

Habilitació Bluetooth

Abans de testejar l'aplicació BLE és necessari habilitar el Bluetooth del mòbil i escriure els permissos necessaris en el fitxer AndroidManifest.xml (al mateix nivell que l'etiqueta <application):

   1 <uses-permission android:name="android.permission.BLUETOOTH" />
   2 <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

Pel que fa al radino cal utilitzar el programa desenvolupat durant l'assignatura d' Enginyeria de Sistemes, el funcionament del qual es basa en:

  1. Enviament d' Advertisments sol·licitant connexió

  2. Un cop s'ha establert la connexió, espera a què s'activin les notificacions
  3. Quan s'han habilitat les notificacions pren mostres de l'acceleròmetre i les envia pel canal UART over BLE (TX)

Representació gràfica

Importar llibreria

AChartEngine és una llibreria de codi lliure dedicada a la representació de gràfiques per a Android. Per a utilitzar-la cal descarregar el fitxer .jar més recent i copiar-lo al directori app/libs de l'aplicació. A continuació s'ha d'importar des del navegador lateral clicant amb el botó dret i Add as libraryCreate, tal i com es mostra en la figura de la dreta.

La representació d' AChartEngine es basa en les següents estructures:

Per a implementar les funcions de representació és recomanable agrupar-les en una classe específica. Un exemple per a una sola sèrie de dades és el següent:

   1 public class GraphChart {
   2   private XYSeries xSeries = new XYSeries("X");
   3   private View graph;
   4   
   5   public GraphChart(Context context) {
   6     XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset();
   7     dataset.addSeries(xSeries);
   8     XYSeriesRenderer xRenderer = new XYSeriesRenderer();
   9     XYMultipleSeriesRenderer multiRenderer = new XYMultipleSeriesRenderer();
  10     multiRenderer.addSeriesRenderer(xRenderer);
  11     graph = ChartFactory.getLineChartView(context, dataset, multiRenderer);
  12 
  13   public void clear(){
  14     xSeries.clear();
  15   }
  16 
  17   public View getView(){
  18     return this.graph;
  19   }
  20 
  21   public void add(long t, double x){
  22     xSeries.add(t, x);
  23   }
  24 
  25   public void update() {
  26     ((GraphicalView) graph).repaint();
  27   }
  28 }

Un cop creada la classe la podem cridar des del MainActivity i afegir al layout:

   1   graph = new GraphChart(getBaseContext());
   2   graph.clear();
   3   layout = (LinearLayout) findViewById(R.id.graph_layout);
   4   layout.addView(graph.getView());

Actualització en temps real

Per a què sigui dinàmica caldrà afegir/borrar dades de la sèrie i actualitzar-la des de la funció onCharacteristicChanged:

   1 public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic){
   2   if (started){
   3     byte[] data = characteristic.getValue();
   4     
   5     ...
   6     
   7     graph.add(time, value_x);
   8     graph.update();
   9   }
  10 }

El paràmetre value_x s'ha de tractar segons el format amb què el radino enviï les dades (bytes, strings formatats...). El paràmetre time és utilitzat per a situar les mostres en l'eix X, el seu valor es pot obtenir de la funció System.currentTimeMillis().

Una vegada comprovat el funcionament de la gràfica en temps real es poden afegir els altres dos eixos al dataset i modificar la funció graph.add per a què accepti més paràmetres.

Personalització

L'aspecte i comportament de la gràfica es pot configurar i adaptar per a l'aplicació (consultar documentació). Alguns exemples són:

   1 public GraphChart(Context context) {
   2   ...
   3   xRenderer.setColor(Color.RED);
   4   xRenderer.setLineWidth(4);
   5   xRenderer.setDisplayChartValues(false);
   6   multiRenderer.setYAxisMin(-400);
   7   multiRenderer.setYAxisMax(400);
   8   multiRenderer.setZoomEnabled(false, false);
   9   ...

Emmagatzematge en fitxer

Tal i com s'explica en la secció AccelData, les mostres entrants són successivament acumulades en un ArrayList global. Aquest és un mètode poc eficient pel que fa al consum de memòria de programa. Per aquest motiu seran passades a un fitxer de text de forma periòdica o al finalitzar l'adquisició.

Java utilitza la classe BufferedWriter per a escriure cadenes de caràcters a un fitxer de sortida. És necessari inicialitzar-lo amb el path on es troba i a continuació ja s'hi poden escriure les mostres. El fitxer pot formar part de la memòria interna reservada per a l'aplicació o bé externa, per exemple, en una targeta SD:

   1 private void writeFile(){
   2 
   3   File sdCard = Environment.getExternalStorageDirectory();
   4   File file = new File(sdCard, "mybackup.txt");
   5   Writer output = null;
   6 
   7   try {
   8     output = new BufferedWriter(new FileWriter(file));
   9     for (AccelData data : results){
  10       output.write(data.toString());
  11     }
  12     if (output != null){
  13       output.close();
  14     }
  15   }
  16   catch (Exception e){
  17     Log.d("MyAct", "Exception: " + e.getMessage());
  18   }
  19 }

Transmissió de dades al servidor

Una opció per a connectar en xarxa local el dispositiu Android i un servidor és crear un punt d'accés wifi des de qualsevol dels dos dispositius.

La transmissió del fitxer es realitzarà amb una tasca asíncrona que treballi en background. Una opció és crear una classe específica sobreescrivint AsyncTask que ha de ser cridada des de la classe MainActivity amb el metode "execute()":

   1 public class ServerUploader extends AsyncTask<Void, Void, Void>{
   2 
   3   @Override
   4   protected Void doInBackground(Void... params){
   5     // Thread que s'executa en background, accepta el pas de paràmetres
   6   }
   7 }

A dins la funció doInBackground caldrà:

  1. Inicialitzar i configurar la connexió http
  2. Llegir el fitxer a transmetre
  3. Enviar les dades obtingudes del fitxer
  4. Tancar la connexió

Per a fitxers que accedeixin la mida màxima del buffer, el punt 2 i 3 s'hauran de realitzar de forma cíclica.

HTTP Post - Connexió persistent

El mètode POST permet l'intercanvi de missatges entre un client (aplicació) i un servidor (ordinador remot) a través del protocol HTTP. Es recomana utilitzar una connexió TCP persistent, també anomenada keep-alive, la qual permet l'enviament de múltiples missatges en una mateixa connexió. En quant al format de les dades, s'utilitzarà el tipus data-form, ja que facilita la transmissió de fitxers encapsulant-los amb un string de boundary. La sintaxi és la següent:

   1   String urlServer = "http://10.42.0.1/handle_data.php";
   2   String boundary = "*****";
   3 
   4   URL url = new URL(urlServer);
   5   HttpURLConnection connection = (HttpURLConnection) url.openConnection();
   6   connection.setDoOutput(true);
   7   connection.setRequestMethod("POST");
   8   connection.setRequestProperty("Connection", "Keep-Alive");
   9   connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);

Lectura del fitxer

La classe FileInputStream ofereix un canal per a llegir un fitxer determinat. Se'n poden fer successives lectures de n bytes (en funció dels bytes disponibles i la mida màxima del buffer de sortida).

   1   int bytesRead, bytesAvailable, bufferSize;
   2   byte[] buffer;
   3   int maxBufferSize = 1 * 1024 * 1024;
   4 
   5   File sdCard = Environment.getExternalStorageDirectory();
   6   File pathToOurFile = new File(sdCard, "dades.txt");
   7   FileInputStream fileInputStream = new FileInputStream(pathToOurFile);
   8 
   9   bytesAvailable = fileInputStream.available();
  10   bufferSize = Math.min(bytesAvailable, maxBufferSize);
  11   buffer = new byte[bufferSize];
  12   bytesRead = fileInputStream.read(buffer, 0, bufferSize);

Canal de sortida

Pel que fa a l'enviament de dades, cal obrir un DataOutputStream per a la connexió HTTP. En els primers missatges a enviar s'especifica el boundary, format de les dades i nom del fitxer a transmetre:

   1   DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
   2   outputStream.writeBytes("--" + boundary + "\r\n");
   3   outputStream.writeBytes("Content-Disposition: form-data; name=\"uploadedfile\";filename=\"" + pathToOurFile + "\"" + "\r\n");
   4   outputStream.writeBytes("\r\n");

Transmissió fitxer

Una vegada inicialitzats els canals d'entrada i sortida es pot processar el contingut del fitxer. Després cal enviar el missatge final de boundary i tancar les connexions:

   1   while (bytesRead > 0) {
   2     outputStream.write(buffer, 0, bufferSize);
   3     bytesAvailable = fileInputStream.available();
   4     bufferSize = Math.min(bytesAvailable, maxBufferSize);
   5     bytesRead = fileInputStream.read(buffer, 0, bufferSize);
   6   }
   7   
   8   outputStream.writeBytes("\r\n");
   9   outputStream.writeBytes("--" + boundary + "--" + "\r\n");
  10   fileInputStream.close();
  11   outputStream.flush();
  12   outputStream.close();

Per a debuggar la transmissió pot resultar útil recollir la resposta del servidor (abans de tancar la connexió) amb:

   1   Log.d("App", "Server response code: " + connection.getResponseCode());
   2   Log.d("App", "Server response msg: " + connection.getResponseMessage());

Servidor

El servidor és un portàtil amb Ubuntu 14.04 en el qual s'ha instal·lat Apache i php. L' script per a guardar el fitxer és handle_data.php:

   1 <?php
   2   $file_path = "uploads/";
   3      
   4   $file_path = $file_path . basename( $_FILES['uploaded_file']['name']);
   5   if(move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $file_path)) {
   6     echo "success";
   7   }
   8   else{
   9     echo "fail";
  10   }
  11 ?>

Nota: Per tal de que apache pugui afegir i modificar fitxers a la carpeta uploads cal modificar-ne els permissos; per exemple, declarant-lo com a propietari:

   1   sudo chown -Rf www-data:www-data /path_to_uploads/

2023-07-03 11:37