Die Kommunikation über den BluetoothAdapter möchte ich in diesem Tutorial beschreiben.
Es gibt im Internet und vor allem bei YouTube sehr gute Videos welche die Kommunikation über Bluetooth Low Energy erläutern, diese Art der Kommunikation ist jedoch aufwändiger ABER zukunftssicherer, obwohl hier sich die API auch bei Bluetooth LE geändert hat, denn einige Methoden sind auch dort deprecated.
Ziel
Mein Ziel ist es eine Verbindung zwischen einem Android-Gerät (Android Version 4.3) und einem Arduino UNO mit dem Bluetoothmodul HC-06 herzustellen.
Dazu habe ich mir eine kleine Wetterstation aufgebaut, welche im Tutorial Arduino Lektion 22: Bluetooth Wetterstation beschrieben ist.
In diesem Tutorial möchte ich auf den Aufbau einer Bluetoothverbindung eingehen und wie ich die Android-App Bluetooth-Terminal entwickelt habe.
Zugang zum Quellcode
Den Quellcode der Android App habe ich Quellcode offen auf GitHub unter https://github.com/StefanDraeger/bluetoothterminal veröffentlicht.
Hinweise zum verwenden von Virtuellen Geräten
Virtuelle Geräte können nicht verwendet werden, da Android das virtualisieren von Bluetoothschnittstellen nicht unterstützt.
Berechtigungen
Für die Verwendung des Bluetooth Adapters im Android-Gerät müssen die Berechtigungen
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
in der AndroidManifest.xml gesetzt werden.
Mit diesen Berechtigungen können wir den Bluetooth Adapter starten und Verbindungen zu Geräten (BluetoothDevices) aufbauen.
Prüfen auf Bluetooth Unterstützung
Nicht jedes Android-Gerät hat eine Bluetoothschnittstelle, somit muss beim Starten der App geprüft werden, ob das verwendete Gerät Bluetooth unterstützt.
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mBluetoothAdapter == null) { Toast.makeText(getApplicationContext(), getString(R.string.msg_no_bluetooth_support), Toast.LENGTH_LONG); }
Als Erstes holt man sich den Bluetooth Adapter und prüft diesen auf NULL, wenn dieser NULL ist, wird Bluetooth nicht von dem Gerät unterstützt. Es wird dann eine entsprechende Nachricht auf dem Display ausgegeben. Theoretisch kann man hier auch die App beenden.
Aktivieren der Bluetoothschnittstelle
Im Normalfall ist die Bluetoothschnittstelle nicht aktiviert, daher muss für geprüft werden in welchem Status sich der Bluetooth Adapter befindet.
if (!mBluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, 0); }
Wenn der Bluetooth Adapter nicht verfügbar ist (aber !=null) dann soll diese Schnittstelle aktiviert werden. Zum Aktivieren wird die Anwendung verlassen und der Dialog von Android wird angezeigt.
Nachdem die Bluetoothschnittstelle aktiviert wurde, können die gekoppelten und nicht gekoppelten Geräte gesucht werden.
Suchen von Geräten
Bluetoothgeräte können 2 Status haben, “gekoppelt” und “nicht gekoppelt”. Als Erstes möchte, ich erläutern wie man gekoppelte Geräte findet.
Auflisten von gekoppelten Geräten
Die gekoppelten Geräte kann man bequem aus dem Bluetooth Adapter lesen. Dazu ruft man die Methode getBondedDevices() auf. Diese Methode liefert ein Set mit den Bluetooth Devices.
Wenn der Bluetooth Adapter nicht aktiviert ist, wird ein leeres Set geliefert.
private void findBondedBluetoothDevices() { Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); deviceList.clear(); if (deviceArrayAdapter != null) { deviceArrayAdapter.clear(); } for (BluetoothDevice device : pairedDevices) { XBluetoothDevice xBluetoothDevice = createXBluetoothDevice(device); deviceList.add(xBluetoothDevice); } setFoundLabel(); }
In der Methode findBondedBluetoothDevices() durchlaufe ich das Set mit einer ForEach Schleife und kapsel dieses in einem WrapperObjekt “XBluetoothDevice” aber dazu später mehr.
Suchen von nicht gekoppelten Geräten
Das Suchen von nicht gekoppelten Geräten übernimmt ein BroadcastReceiver welcher in der Activity registriert werden muss. Was ein BroadcastReceiver ist, wird in dem Wikiartikel https://de.wikibooks.org/wiki/Googles_Android/_BroadcastReceiver sehr gut erläutert.
Der BroadcastReceiver zum suchen von nicht gekoppelten Bluetoothgeräten.
Im Konstruktor wird die ausführende Activity übergeben. Diese wird benötigt damit auf den ApplicationContext zugegriffen und der ProgressDialog angezeigt werden kann. Die Methode onReceive(); wird aufgerufen, wenn ein nicht gekoppeltes Bluetoothgerät gefunden wurde. Hier wurden nicht alle Status abgefragt da ich hier nicht mit Bluetooth LE (LowEnergy) arbeite (zum Thema Bluetooth Low Energy wird es später ein ausführliches Tutorial geben.)
Ablauf:
Wenn der Suchlauf gestartet wird (Status BluetoothAdapter.ACTION_DISCOVERY_STARTED) wird der ProgressDialog mit einer Nachricht und einem Spinner angezeigt.
Der Status BluetoothAdapter.ACTION_FOUND wird gesetzt, wenn ein Gerät gefunden wurde, in diesem Fall wrappe ich das gefundene Gerät (BluetoothDevice) in mein XBluetoothDevice und füge es meiner Activity hinzu. Der Suchlauf wird hier nicht abgebrochen.
Wenn keine weiteren Geräte gefunden wurden, wird der Status BluetoothAdapter.ACTION_DISCOVERY_FINISHED geliefert, in diesem Fall schließe ich den Dialog und aktualisiere das Label welches die gefundenen Geräte auf der Activity repräsentiert.
import android.app.ProgressDialog; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.widget.Toast; public class BluetoothBroadcastReceiver extends BroadcastReceiver { private ProgressDialog progressDialog; private MainActivity activity; private Context ctx; public BluetoothBroadcastReceiver(MainActivity activity) { this.activity = activity; this.ctx = activity.getApplicationContext(); } public void onReceive(Context context, Intent intent) { handleBluetoothState(intent); handleAction(intent); } private void handleBluetoothState(Intent intent) { int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); switch (state) { case BluetoothAdapter.STATE_OFF: showToast(R.string.bluetooth_off); break; case BluetoothAdapter.STATE_ON: showToast(R.string.bluetooth_on); break; case BluetoothAdapter.STATE_TURNING_ON: showToast(R.string.bluetooth_turning_on); break; case BluetoothAdapter.STATE_TURNING_OFF: showToast(R.string.bluetooth_turning_off); break; } } private void showToast(int stringResId) { Toast.makeText(ctx, ctx.getString(stringResId), Toast.LENGTH_LONG).show(); } private void handleAction(Intent intent) { String action = intent.getAction(); switch (action) { case BluetoothAdapter.ACTION_DISCOVERY_STARTED: progressDialog = new ProgressDialog(activity); progressDialog.setTitle(this.ctx.getString(R.string.searchForNewBluetoothDevices_titel)); progressDialog.setMessage(this.ctx.getString(R.string.searchForNewBluetoothDevices_msg)); progressDialog.setIndeterminate(true); progressDialog.show(); break; case BluetoothAdapter.ACTION_DISCOVERY_FINISHED: progressDialog.cancel(); activity.setFoundLabel(); break; case BluetoothDevice.ACTION_FOUND: BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); XBluetoothDevice xBluetoothDevice = activity.createXBluetoothDevice(device); activity.getDeviceList().add(xBluetoothDevice); break; } } }
Das Starten des Suchlaufs für nicht gekoppelte Geräte übernimmt folgende Methode, welche ich in einem Menü aufrufe.
private void findBluetoothDevices() { IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_FOUND); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); registerReceiver(mReceiver, filter); mBluetoothAdapter.startDiscovery(); setReciverIsRegisterd(true); }
In der Methode onDestroy() der Activity muss der registrierte BroadcastReceiver wieder unregistriert werden.
if (isReciverIsRegisterd()) { this.unregisterReceiver(mReceiver); }
Koppeln von Bluetoothgeräten
Da bisher nur das nicht gekoppelte Gerät gefunden wurde, muss dieses um es verwenden zu können gekoppelt werden. Dazu benötigt man die Adresse des Gerätes.
private void boundBluetoothDevice() { if (selectedBluetoothDevice != null) { try { Method m = selectedBluetoothDevice.getDevice().getClass().getMethod("createBond", (Class[]) null); m.invoke(selectedBluetoothDevice.getDevice(), (Object[]) null); } catch (Exception e) { Log.e(TAG, e.getMessage()); } } }
Da mein Zielgerät ein Samsung Galaxy S3 ist und dieses mit der Version Andorid 4.3 (SDK 19) läuft muss ich hier mit Reflexion arbeiten denn die Implementierung ist vorhanden aber das Interface bietet keine öffentlichen Methoden dafür.
Entkoppeln von Geräten
Nachdem ein Gerät gekoppelt wurde, kann dieses auf fast dem gleichen Wege wieder entkoppelt werden.
private void unboundBluetoothDevice() { try { Method m = selectedBluetoothDevice.getDevice().getClass().getMethod("removeBond", (Class[]) null); m.invoke(selectedBluetoothDevice.getDevice(), (Object[]) null); } catch (Exception e) { Log.e(TAG, e.getMessage()); } }
Bluetoothkommunikation
Nachdem Bluetoothgeräte gefunden wurden, kann nun mit diesen eine Verbindung aufgebaut werden. Mein Ziel ist es mit dem Bluetooth Modul HC-06 und einem Arduino UNO eine Verbindung aufzubauen. Wenn man sich mit diesem Thema auseinandersetzt und im Internet nach einer Lösung forscht, findet man viele brauchbare Ideen.
Aufbau der Verbindung zum Gerät
Für den Aufbau der Verbindung benutze ich einen AsyncTask, dieser Task wird gestartet und läuft im Hintergrund. Wenn dieser Task die Verbindung aufgebaut hat, kehrt er an der aufrufenden Stelle zurück und liefert in diesem Fall einen BluetoothSocket.
private void createConnection() { try { this.socket = new ConnectionAsyncTask(targetXBluetoothDevice.getDevice(), this).execute(null, null, null).get(); if (this.socket != null && this.socket.isConnected()) { setStatus(ConnectionStatus.Connected); connectionThread = new ConnectionThread(this, this.socket); connectionThread.start(); } else { setStatus(ConnectionStatus.ConnectionFailed); } } catch (Exception e) { Log.e(TAG, e.getMessage()); e.printStackTrace(); } }
Wenn ein gültiges Objekt (d.h. nicht null und die Verbindung wurde aufgebaut) zurückgeliefert wurde, starte ich einen java.lang.Thread welcher auf dieser Connection lauscht und eingehende Datenströme entgegennimmt und verarbeitet.
public class ConnectionThread extends Thread { ..... @Override public void run() { while (isRunThread() && (this.connectedBluetoothSocket != null && this.connectedBluetoothSocket.isConnected())) { try { BufferedReader reader = new BufferedReader(new InputStreamReader(getConnectedInputStream())); final StringBuilder sb = new StringBuilder(); sb.append(reader.readLine()); Log.i(TAG, sb.toString()); terminal.runOnUiThread(new Runnable() { @Override public void run() { Message message = terminal.generateNewMessage(sb.toString(), false); terminal.addMessage(message); } }); } catch (IOException e) { setRunThread(false); Log.e(TAG, e.getMessage()); final String msgConnectionLost = "Connection lost:\n" + e.getMessage(); terminal.runOnUiThread(new Runnable() { @Override public void run() { terminal.setStatus(ConnectionStatus.ConnectionFailed); Log.i(TAG, msgConnectionLost); } }); } } } }
Wenn es hier zu einem Fehler kommt (Exception) so wird der Status “ConnectionFailed” auf der Oberfläche angezeigt.
Des Weiteren möchte ich diesen Thread von außen auch wieder stoppen können, dazu habe ich eine Boolsche Variable angelegt welche in der run Methode abgefragt wird.
while (isRunThread() .....
Dieses hat den Vorteil das man sicher einen Thread “beenden” kann.
Ein Thread sollte niemals beendet werden, sonder soll sich selber beenden.
Lesen von Datenströmen
Die Datenströme der Bluetoothschnittstelle liest man üben den BluetoothSocket und dem InputStream.
Hier gibt es zwei Möglichkeiten wie man diese verarbeitet:
Das lesen eines Byte Arrays
Eine weit verbreitete Lösung ist das schreiben in ein Byte Array:
byte[] buffer = new byte[4096]; int bytes; while (true) { bytes = connectedInputStream.read(buffer); final String readValue = new String(buffer, bytes); }
Gehen wir die Zeilen durch und schauen was hier passiert:
Zeile 1:
Es wird eine Variable byte[] einen maximalen Platz von 4096 Bytes reserviert (ca. 4 MB), dieser Wert kann natürlich beliebig verändert werden oder man nimmt aus dem InputStream mit der Methode available(). Da man hier nicht mit einem java.lang.InputStream arbeitet, sondern mit einem BluetoothInputStream ist die Implementierung hier anders, denn diese Methode liefert (jedenfalls in meinen Tests) immer min. 0 zurück.
Zeile 2:
Die Variable bytes wird angelegt damit der später in der while Schleife nicht immer eine neue Variable angelegt werden muss, dieses hat einen minimalen Resourcenvorteil.
Zeile 3:
Eröffnung der while Schleife es wird hier ein Boolean.True übergeben, für das Beispiel ist es ausreichend jedoch kann man hier diese nicht von außen steuern.
Zeile 4:
In der Zeile 4 werden die Daten aus dem InputStream gelesen und die Bytes in der Variable bytes zwischen gespeichert.
Zeile 5:
Die gelesenen Bytes werden nun lesbar in ein String umgewandelt, dafür nutzt man den Konstruktor der Klasse java.lang.String mit den Parametern für
- byte Array,
- gelesene bytes
Als Ergebnis bekommt man nun den Wert als String.
Zeile 6:
Als letztes wird nun noch die zuvor geöffnete while Schleife geschlossen.
Warum ich von dieser Methode abgewichen bin
Diese Methode funktioniert und liefert die Werte jedoch ist es nicht zuverlässig das alle Werte sofort und mit einem rutsch gelesen werden, d.h. in meinem Fall der Wetterstation sende ich folgenden String “Temperatur – 25 °C”, jedoch bekomme ich diesen abgehackt d.h. es kommt der Wert “Temp” und dann “eratur” und so weiter…. Für meinen Fall ist das nicht brauchbar und ich habe mich für die Lösung mit einem BufferedReader entschieden.
BufferedReader zum lesen der Datenströme
Mit folgender Lösung kann man die gesamten Daten mit einem rutsch lesen.
while (isRunThread() && (this.connectedBluetoothSocket != null && this.connectedBluetoothSocket.isConnected())) { BufferedReader reader = new BufferedReader(new InputStreamReader(getConnectedInputStream())); final StringBuilder sb = new StringBuilder(); sb.append(reader.readLine()); }
Schauen wir uns auch hier die einzelnen Zeilen genauer an:
Zeile 1:
Eröffnen der while Schleife sowie prüfen ob
- der Thread weiterhin ausgeführt werden soll,
- eine Bluetoothconnection besteht
Zeile 2:
Erzeugen eines BufferedReaders und Zuweisen eines InputStreamReader aus dem InputStream des BluetoothSocket.
Zeile 3:
Erzeugen eines StringBuilders, für das spätere hinzufügen von den Textwerten aus dem BufferedReader.
Zeile 4:
Hinzufügen einer Textzeile aus dem BufferedReader zu dem StringBuilder.
Lösung für die Verwendung eines SDK > Version 18
Die oben dargestellte Lösung ist für ein Android SDK 18 gedacht, wenn das Zielgerät jedoch ein neueres ist, so kann man auch die Neuerungen von Java nutzen.
Da die Klassen InputStreamReader und BufferedReader das Interface Closeable implementieren können diese als Resource im try catch Block verwendet werden, dieses macht ein Schließen im finally Block überflüssig.
try (InputStreamReader inputStreamReader = new InputStreamReader(getConnectedInputStream()); BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {...}
Darstellen der gelesenen Werte aus dem Datenstrom auf der GUI
Nun kann man den Wert aus dem StringBuilder weiterverwenden und auf der GUI darstellen.
Dazu übergibt man dem Thread die Activity und kann dann auf den UI Thread wie folgt zugreifen:
activity.runOnUiThread(new Runnable() { @Override public void run() { Message message = activity.generateNewMessage(sb.toString(), false); activity.addMessage(message); } });
In der Activity habe ich eine Hilfsmethode generateNewMessage diese bekommt als Parameter die empfangene Nachricht und einen Booleschen Wert, ob diese Nachricht empfangen oder gesendet wurde.
Servus Stefan,
leider ist der Quellcode der Android App nicht auf GitHub. Ist es möglich, dass du den Quellcode zur Verfügung stellst.
Danke und Gruß
Yilmaz
Hallo Yilmaz,
sorry, das hatte ich wohl damals glatt vergessen.
Ich habe gerade auf meinem PC geschaut, aber leider finde ich die Sourcen nicht mehr.
Gruß
Stefan