Aplicació Bluetooth Low Energy per a Android
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ó:
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:
- Android SDK Tools (última versió)
- Android SDK Platform-tools
- Android SDK Build-tools
- API amb la qual volem treballar (BLE utilitza la 18 com a mínim)
Extras → Android Support Library
Extras → Google Repository
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:
manifests: El fitxer AndroidManifest.xml conté informació necessària per a compilar (nom, versió mínima de la SDK, permissos...)
java: Directori del codi de l'aplicació, des de l'activitat principal definida per defecte fins a mòduls o classes declarats per l'usuari
res: Conté carpetes amb els següents recursos:
drawable: Gràfics
layout: Disseny de la interfície gràfica d'usuari
menu: Defineix el menú desplegable des de l'aplicació
mimap/ic_launcher.png: Icona d'arrancada
values: El fitxer strings.xml declara el text de l'aplicació, facilitant la modificació de l'estil, gestió de l'idioma...
Per a provar l'exemple des de l'emulador o dispositiu físic cal prémer Run → Run 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: Layout, interacció amb l'usuari, debugging...
- Comunicació Bluetooth: Funcions per a associar-se i activar l'enviament de mostres del radino
- Representació gràfica: Mostra de les dades per pantalla en temps real
- Emmagatzematge: Recull de mostres i emmagatzematge (intern o a un servidor)
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:
- Botó per a connectar / desconnectar el radino
- Gràfica per a representar les dades
- Botó per a iniciar / parar l'adquisició de mostres
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>
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:
El resultat serà comprovat en la finestra de logcat quan s'executi l'aplicació:
Bluetooth Low Energy per a Android
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:
BluetoothManager: S'encarrega de la gestió del sistema Bluetooth del mòbil. Permet obtenir el BluetoothAdapter
BluetoothAdapter: Ofereix les funcionalitats bàsiques de Bluetooth (buscar dispositius, associar-los...)
BluetoothDevice: Representació d'un dispositiu Bluetooth remot a partir d'una adreça BLE (p.ex. "00:11:22:33:44:55")
BluetoothGatt: Implementa les funcionalitats de comunicació GATT. Per a connectar-se amb un altre dispositiu és necessari un callback
BluetoothGattCallback: Implementa diverses funcions que s'executen quan hi ha esdeveniments al BluetoothGatt
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:
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:
- Connexió establerta: L'aplicació s'ha connectat al perifèric i es pot iniciar el procés de detecció de serveis
- Serveis descoberts: Una vegada descoberts els serveis es poden enviar comandes GATT, p. ex. habilitar les notificacions
- 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:
- timestamp (long): Indica l'instant en què s'ha pres la mostra, servirà també a l'hora de representar-la gràficament
- sensor (int): Identificador del sensor
- x, y, z (doubles): Valor dels eixos
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 }
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):
Pel que fa al radino cal utilitzar el programa desenvolupat durant l'assignatura d' Enginyeria de Sistemes, el funcionament del qual es basa en:
Enviament d' Advertisments sol·licitant connexió
- Un cop s'ha establert la connexió, espera a què s'activin les notificacions
Quan s'han habilitat les notificacions pren mostres de l'acceleròmetre i les envia pel canal UART over BLE (TX)
Representació gràfica
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 library → Create, tal i com es mostra en la figura de la dreta.
La representació d' AChartEngine es basa en les següents estructures:
Series: Conjunt de mostres referents a una variable
Dataset: Agrupació de series per a una gràfica
Renderer: Objecte encarregat de la representació d'una instància de series
MultiRenderer: Objecte encarregat de representar un dataset
Chart: Gràfica en si
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:
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:
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:
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()":
A dins la funció doInBackground caldrà:
- Inicialitzar i configurar la connexió http
- Llegir el fitxer a transmetre
- Enviar les dades obtingudes del fitxer
- 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:
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:
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/