In diesem Beitrag entwickeln wir eine Lösung mit einer App, die wie in der Abbildung aussieht. Die App sendet den eingegebenen Text über Bluetooth an den Raspberry Pi, wo er auf der Konsole ausgegeben und an das Android Target zurückgeschickt wird.
Was wird gebraucht?
Für unser Projekt benötigen wir nicht viel:- Einen Bluetooth-Dongle. Ich habe mir mal für gut 5€ einen bei Amazon gekauft.
- Ein Android-Smartphone mindestens mit der Android Version 2.2.
- Ein Raspberry Pi Modell B
- Eine SD-Karte mit dem aktuellen Raspbian-Image
- Während der Entwicklung braucht unser Raspberry einen Internet-Anschluss. Den Pi betreiben wir hier headless, also ohne GUI über ein Terminal.
- Ein Entwicklungsrechner, etwa in ein Form eines PCs, der über einen SSH oder Telnet-Client verfügt. Diesen Rechner bezeichnen wir im folgenden auch als Host.
- Ein für die Android-Entwicklung vorkonfiguriertes Eclipse.
- Zwei Micro-USB Kabel. Eines für die Stromversorgung des Pi, eines für die Anbindung des Android-Targets an den Host.
Die Software-Beispiele sind einfach, man sollte aber schon einmal eine einfache Hello World App entwickelt haben.
Aufrüsten für Bluetooth
Bluetooth hat gegenüber WiFi den Vorteil, dass man keine Infrastruktur in Form eines Access-Point benötigt. Bluetooth-fähige Geräte können Ad-Hoc vernetzt werden. Bluetooth bietet außerdem einen Discovery-Service, der uns darüber informiert, welche Bluetooth-Gegenstellen es in der Reichweite unseres Gerätes gibt. Damit sich nicht jeder mit unserem Bluetooth-Gerät verbinden kann, gibt es das so genannte Pairing: Nur Geräte, die sich gegenseitig trauen, können Nutzdaten austauschen. Das soll erst einmal als Grundlagenwissen über diese hochkomplexe Protokollfamilie reichen.Wir stecken den Bluetooth USB-Dongle an, starten den Pi und melden uns etwa per SSH an. Dann bringen wir den Pi mit
sudo apt-get update
sudo apt-get upgrade
softwareseitig auf den neuesten Stand.
Anschließend machen wir das Betriebssystem bluetoothfähig:
sudo apt-get install bluetooth
Damit wir überhaupt mit Bluetooth arbeiten können, müssen wir die Standardkonfiguration ändern und in die Datei /etc/bluetooth/main.conf die Zeile
DisablePlugins = pnat
einfügen. Anschließend starten wir den Bluetooth Service neu:
service bluetooth restart
Wir prüfen vorsichtshalber, ob der Service läuft:
service bluetooth status
Jedes Bluetooth-Gerät ist mit einer eigenen Adresse ausgestattet, die wir wie folgt ermitteln:
hcitool dev
Mit der Anweisung hcitool können wir auch die sichtbaren Bluetooth-Geräte in unserer Umgebung finden:
hcitool scan
Neben der Adresse sehen wir hier den lesbaren Namen des Gerätes. An den meisten Geräten kann konfiguriert werden, ob sie über Bluetooth-Discovery erkannt werden sollen. Android-Geräte werden standardmäßig nicht erkannt, sondern müssen etwa über die System-Einstellungen zumindest temporär sichtbar gemacht werden. Wir probieren das aus und sollten das Android-Gerät jetzt beim Scan auf unserem Pi sehen.
Vertraulichkeiten austauschen
Bevor zwei Geräte Daten austauschen können, müssen sie zum Zeichen ihres Vertrauens einen Schlüssel austauschen. Den Austausch kann jeder der beiden auslösen. Wir wählen hier die Variante, dass der Pi den Schlüssel anfordert. Als Schlüssel wählen wir 4711 und starten einen Prozess, der Schlüssel annimmt und mit 4711 vergleicht:bluetooth-agent 4711 &
Am Android-Gerät sollte es in den Bluetooth-Einstellungen zwei Listen geben:
- Eine Liste der Geräte, für die der Schlüsselaustausch - auch Pairing genannt - bereits erfolgreich durchgeführt wurde und
- Eine Liste mit allen anderen erkannten Geräten.
Unser Raspberry Pi ist noch in keiner der beiden Listen verzeichnet, da er noch nicht gepaired wurde und standardmäßig unsichtbar ist. Mit
hciconfig hci0 piscan
machen wir ihn sichtbar. Er sollte jetzt im Verzeichnis der sichtbaren Geräte auf unserem Android-Gerät auftauchen. Bei vielen Android-Derivaten muss man dazu den zugehörigen Listeneintrag länger berühren und wird dann nach dem Schlüssel gefragt. Das kann sich aber auf anderen Android-Geräten anders verhalten. Sobald wir den Schlüssel eingegeben haben, wird das Pairing durchgeführt und abgeschlossen.
Software für den Raspberry
Es gibt verschiedene Möglichkeiten, um Verbindungen zwischen Bluetooth-Geräten herzustellen. Wir wählen hier eine Variante, bei der unser Pi sichtbar bleibt.Die Entwicklung von Software für den Datenaustausch erinnert sehr an Socket-Programmierng. Wir benötigen dazu ein entsprechendes Python-Paket
sudo apt-get install python-bluetooth
Der folgende Code ist ein Bluetooth-Server, der fortwährend auf Nachrichten wartet, diese entgegennimmt, sie auf die Standardausgabe schreibt und ein Echo an den Sender zurückschickt. Weitere Beispiele findet man in zahlreichen Tutorials.
from bluetooth import *
def initServer():
server_sock=BluetoothSocket( RFCOMM )
server_sock.bind(("",PORT_ANY))
server_sock.listen(1)
uuid = "00001101-0000-1000-8000-00805F9B34FB"
advertise_service(server_sock, "Echo Server",
service_id = uuid,
service_classes = [ uuid, SERIAL_PORT_CLASS ],
profiles = [ SERIAL_PORT_PROFILE ]
)
return server_sock
def getClientConnection(server_sock):
print "Waiting for connection"
client_sock, client_info = server_sock.accept()
print "accepted connection from ", client_info
return client_sock
def manageConnection(socket):
try:
while True:
data = socket.recv(1024)
if len(data) == 0: break
print "received [%s]" % data
socket.send("Echo from Pi: [%s]\n" % data)
except IOError:
pass
server=initServer()
while True:
client=getClientConnection(server)
manageConnection(client)
client.close()
server.close()
print "terminating..."
Bluetooth ist wie gesagt, sehr umfassend und bietet verschiedene Protokolle, so genannte Profile. Hier arbeiten wir mit RFCOMM, einem Profil, das die serielle Kommunikation nutzt. Der Port auf dem das Gerät lauscht, muss der Sender nicht kennen. Die genutzte UUID ist eine Konvention, die zur Identifikation eines RFCOMM-Sockets benutzt werden kann.
Wenn wir die Datei bluetooth_server.py nennen, können wir den Server auf unserem Pi von der Kommandozeile aus starten:
python bluetooth_server.py
Und jetzt die App
Auf der Android-Seite wollen wir eine möglichst einfache App entwickeln, die die Bluetooth-Funktionalität implementiert. Dazu definieren wir unsere GUI wie in Android üblich in einer Layout-Datei. Der XML-Code repräsentiert die App wie wir sie oben in der Abbildung sehen.<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<EditText
android:id="@+id/message_et"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/send_bn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/message_et"
android:layout_below="@+id/message_et"
android:layout_marginTop="22dp"
android:onClick="onSend"
android:text="Send To Pi" />
</RelativeLayout>
Die Activity initialisiert zunächst die beiden Widgets vom Typ Button und EditText und überprüft anschließend, ob Bluetooth eingesetzt werden kann. Nur wenn das der Fall ist, wird der Button aktiviert und wir können Nachrichten versenden.
import java.util.Set;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends Activity {
private final static String TAG = "MainActivity";
private BluetoothAdapter mBluetoothAdapter;
private BluetoothDevice mDevice;
private EditText mMessageET;
private Button mSendBN;
private void findRaspberry() {
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter
.getBondedDevices();
for (BluetoothDevice device : pairedDevices) {
if(device.getName().equals("raspberrypi-0"))
this.mDevice=device;
}
}
private void initBluetooth() {
Log.d(TAG, "Checking Bluetooth...");
if (mBluetoothAdapter == null) {
Log.d(TAG, "Device does not support Bluetooth");
mSendBN.setClickable(false);
} else{
Log.d(TAG, "Bluetooth supported");
}
if (!mBluetoothAdapter.isEnabled()) {
mSendBN.setClickable(false);
Log.d(TAG, "Bluetooth not enabled");
}
else{
Log.d(TAG, "Bluetooth enabled");
}
}
public void onSend(View view) {
String message = mMessageET.getText().toString();
new MessageThread(mDevice, message).start();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMessageET = (EditText) findViewById(R.id.message_et);
mSendBN = (Button) findViewById(R.id.send_bn);
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
initBluetooth();
findRaspberry();
if (mDevice == null)
mSendBN.setClickable(false);
}
}
Um die User Experience nicht durch einen blockierenden UI-Thread zu beeinträchtigen wird der Versuch bestimmte - typischerweise aufwändige - Operationen im UI-Thread auszuführen seit dem Android API Level 11 mit einer Exception bestraft. Diese Aufgaben sollen in einem eigenen Thread ablaufen. Dazu gibt es verschieden Möglichkeiten. Wir arbeiten hier mit dem klassischen Ansatz eine Unterklasse der Klasse Thread zu implementieren und einen Thread starten, sobald der Send-Button ausgelöst wird.
Dieser Thread besorgt sich einen Client-Thread für die Verbindung zum Bluetooth Server, sendet die Nachricht und wartet auf Antwort vom Server. Der entsprechende Status wird jeweils protokolliert.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.UUID;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
public class MessageThread extends Thread {
private final static String TAG="MessageThread";
private final static String MY_UUID ="00001101-0000-1000-8000-00805f9b34fb";
private BluetoothSocket mSocket=null;
private String mMessage;
public MessageThread(BluetoothDevice device, String message) {
Log.d(TAG,"Trying to send message...");
this.mMessage=message;
try {
UUID uuid = UUID.fromString(MY_UUID);
mSocket = device.createRfcommSocketToServiceRecord(uuid);
} catch (IOException e) {
e.printStackTrace();
}
}
private void manageConnectedSocket(BluetoothSocket socket) throws IOException {
Log.d(TAG,"Connection successful");
OutputStream os=socket.getOutputStream();
PrintStream sender = new PrintStream(os);
sender.print(mMessage);
Log.d(TAG,"Message sent");
InputStream is=socket.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(is));
Log.d(TAG,"Received: " reader.readLine());
}
public void run() {
BluetoothAdapter.getDefaultAdapter().cancelDiscovery();
try {
mSocket.connect();
manageConnectedSocket(mSocket);
mSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Im Manifest müssen wir noch die geeigneten Rechte einfordern:
<uses-permission android:name="android.permission.BLUETOOTH" />
Wenn wir uns die Ausgabe auf dem Raspberry Pi anschauen, sehen wir, wenn wir über die App den Text Hello Pi verschickt haben, die folgende Ausgabe:
Waiting for connection
accepted connection from ('4F:B0:3A:45:3A:6A', 1)
received [Hello Pi]
Waiting for connection
Noch nicht perfekt
Dieser Beitrag will die grundsätzliche Vorgehensweise für die Bluetooth-Kommunikation zwischen einem Pi und einem Raspberry Pi anhand einfacher Code-Beispiele vorstellen. Hier gibt es sicher noch zahlreiche Verbesserungsmöglichkeiten:
- Das Python-Programm arbeitet mit nur einem Thread und kann daher nur mit einem Anwender kommunizieren.
- Androidseitig ist das Exception-Handling noch sehr rudimentär, Statusmeldungen werden nur protokolliert aber nicht am Gerät angezeigt.
- Der Name raspberrypi-0 ist als Text im Quellcode hinterlegt. Besser wäre es, den Anwender aus einer Liste wählen zulassen.
- Die App unterstützt weder Pairing noch die Aktivierung von Bluetooth. Beides muss der Anwender hier im Vorfeld in den System-Einstellungen konfigurieren.
Hallo,
AntwortenLöschensehr ausführlicher Code. Leider funktioniert das Beispiel bei mir nicht.
Beim Aufruf von mSocket.connect(); bekomme ich eine Timeout-Fehlermeldung.
Wenn ich anstatt device.createRfcommSocketToServiceRecord(uuid); folgenden Code verwende, stellt das Programm die Verbidnung zwar her, jedoch kommen keine gesendeten Daten an.
Method m = device.getClass().getMethod("createRfcommSocket", new Class[] {int.class});
BluetoothSocket tmp = (BluetoothSocket) m.invoke(device, 1);
Haben Sie eine Idee, woran das liegen könnte.
Viele Grüße und Danke schonmal!
Hallo,
AntwortenLöschender Code funktioniert super, jedoch kann man den Port nur einmal verwenden, er springt direkt von terminating....zu Waiting for Connection und wirft dann einen Fehler aus, nachdem der Socket von der HandyApp geschlossen wurde.
Ist es möglich den Socket zu resetten damit man eine neue Verbindung aufbauen kann.
VG
Danke! <3
AntwortenLöschenSuper das ..... gibt es eine Version 2.0 ?
AntwortenLöschen