Für diese Projekt setzen wir voraus, dass das Android SDK eingerichtet ist und ein grundsätzliches Verständnis der Java Programmiersprache vorhanden ist.
Falls dies nicht der Fall ist sollte hier begonnen werden. Informationen über die Tinkerforge API sind dann hier zu finden.
Wir setzen weiterhin voraus, dass die Fernbedienung mit einem Industrial Quad Relay Bricklet verbunden wurde wie hier beschrieben.
Das vollständige Eclipse Projekt kann hier heruntergeladen werden.
In diesem Projekt werden wir eine einfach Android App entwickeln, die die Funktionalität der eigentlichen Fernbedienung nachbildet.
Nach dem Erstellen eines neuen "Android Application Project" namens "Garage Control" in Eclipse beginnen wir mit der Erstellung der GUI:
Die Grundlage ist ein "Linear Layout (Vertical)". Drei Textfelder ermöglichen die Eingabe von Host, Port und UID des Industrial Quad Relay Bricklets. Für den Port wird ein "Number" Textfeld verwendet, so dass Android die möglichen Eingaben in diesem Textfeld auf Zahlen beschränken wird. Die letzten beiden Elemente sind Knöpfe für den Aufbau und das Trennen der Verbindung sowie das Auslösen eines Tastendrucks auf der gehackten Fernbedienung.
Damit ist das Layout des GUIs fertig. Um auf die GUI Elemente von Java aus
zugreifen zu können bietet Android die findViewById()
Methode. Über diese
erhält man Referenzen auf die GUI Elemente, welche dann in Member-Variablen
der MainActivity
Klasse zugewiesen werden:
public class MainActivity extends Activity {
private EditText host;
private EditText port;
private EditText uid;
private Button connect;
private Button trigger;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
host = (EditText)findViewById(R.id.host);
port = (EditText)findViewById(R.id.port);
uid = (EditText)findViewById(R.id.uid);
connect = (Button)findViewById(R.id.connect);
trigger = (Button)findViewById(R.id.trigger);
}
}
Dieser Schritt ist ähnlich zu Schritt 1 des
Rauchmelder mit Java auslesen Projekts.
Einige Änderungen sind notwendig damit es in einem GUI Programm funktioniert.
Statt dem EnumerateListener
zum Erkennen des Industrial Quad Relay
Bricklets verwenden muss dessen UID angegeben werden. Dieser Ansatz erlaubt es
gezielt ein Industrial Quad Relay Bricklet auszuwählen, selbst wenn mehrere am
gleichen Host angeschlossen sind.
Die connect()
Methode soll nicht direkt aufgerufen werden, da dies einen
Moment dauern kann und in dieser Zeit die GUI nicht auf den Benutzer reagieren
könnte. Daher wird connect()
aus einem AsyncTask
aufgerufen werden, so
dass es im Hintergrund ausgeführt und die GUI nicht blockiert wird
public class MainActivity extends Activity {
// [...]
private IPConnection ipcon;
private BrickletIndustrialQuadRelay relay;
class ConnectAsyncTask extends AsyncTask<Void, Void, Void> {
private String currentHost;
private String currentPort;
private String currentUID;
@Override
protected void onPreExecute() {
currentHost = host.getText().toString();
currentPort = port.getText().toString();
currentUID = uid.getText().toString();
}
protected Void doInBackground(Void... params) {
ipcon = new IPConnection();
relay = new BrickletIndustrialQuadRelay(currentUID, ipcon);
ipcon.connect(currentHost, Integer.parseInt(currentPort));
return null;
}
}
}
Der ConnectAsyncTask
ist als Inner Class implementiert. Dadurch kann diese
direkt auf Member-Variablen der äußeren MainActivity
Klasse zugreifen.
Die onPreExecute()
Methode wird vor doInBackground()
aufgerufen und
speichert die Host, Port und UID Konfiguration von den GUI Elementen zwischen.
Dies ist notwendig, da die doInBackground()
Methode diese Information
benötigt, aber selbst nicht auf die GUI Elemente zugreifen darf. Jetzt kann
die doInBackground()
Methode die IPConnection
und
BrickletIndustrialQuadRelay
Objekte erzeugen und connect()
aufrufen.
Schlussendlich soll ein ConnectAsyncTask
erzeugt und gestartet werden, wenn
der Connect-Knopf geklickt wird. Ein OnClickListener
wird für diesen Zweck
dem Connect-Knopf hinzugefügt:
public class MainActivity extends Activity {
// [...]
class ConnectClickListener implements OnClickListener {
public void onClick(View v) {
new ConnectAsyncTask().execute();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// [...]
connect.setOnClickListener(new ConnectClickListener());
}
}
Host, Port und UID können jetzt eingestellt werden und ein Klick auf den Connect Knopf stellt die Verbindung her.
Die Verbindung ist hergestellt und das Industrial Quad Relay Bricklet wurde gefunden, aber es fehlt noch die Logik um einen Taster auf der Fernbedienung auszulösen wenn der Knopf geklickt wurde.
Ein OnClickListener
wird dem Trigger-Knopf hinzugefügt. Dieser erzeugt und
startet einen AsyncTask
der wiederum die setMonoflop()
Methode des
Industrial Quad Relay Bricklet aufruft, um einen Taster auf der Fernbedienung
zu drücken:
public class MainActivity extends Activity {
// [...]
private BrickletIndustrialQuadRelay relay;
@Override
protected void onCreate(Bundle savedInstanceState) {
// [...]
trigger.setOnClickListener(new TriggerClickListener());
}
class TriggerAsyncTask extends AsyncTask<Void, Void, Void> {
protected Void doInBackground(Void... params) {
relay.setMonoflop(1 << 0, 15, 1500);
return null;
}
}
class TriggerClickListener implements OnClickListener {
public void onClick(View v) {
new TriggerAsyncTask().execute();
}
}
}
Der Aufruf von setMonoflop(1 << 0, 15, 1500)
schließt das erste Relais für 1,5s und öffnet
es dann wieder.
Das ist es. Wenn wir diese drei Schritte zusammen in ein Projekt kopieren, dann hätten wir jetzt eine funktionierende App, die es ermöglicht vom Smartphone aus den Garagentoröffner mittels dessen gehackter Fernbedienung zu steuern.
Es fehlt noch ein Disconnect-Knopf und der Trigger-Knopf kann auch geklickt werden obwohl keine Verbindung besteht. Es fehlt also noch etwas mehr GUI-Logik!
Es gibt noch keinen Knopf um die Verbindung wieder zu trennen nachdem sie aufgebaut wurde. Der Connect-Knopf könnte dies tun. Wenn die Verbindung aufgebaut ist sollte er bei einem Klick die Verbindung wieder trennen:
class ConnectAsyncTask extends AsyncTask<Void, Void, Void> {
// [...]
@Override
protected void onPostExecute(Void result) {
connect.setText("Disconnect");
connect.setOnClickListener(new DisconnectClickListener());
}
}
Die onPostExecute()
Methode wird nach doInBackground()
aufgerufen. Sie
ändert den Text des Knopfes und setzt einen neuen OnClickListener
, der die
Verbindung trennen wird wenn der Knopf geklickt wird:
class DisconnectClickListener implements OnClickListener {
public void onClick(View v) {
new DisconnectAsyncTask().execute();
}
}
Die disconnect()
Methode soll nicht
direkt aufgerufen werden, da diese einen Moment dauern kann und in dieser Zeit
die GUI blockiert ist. Stattdessen wird disconnect()
in einem AsyncTask
aufgerufen, wodurch es im Hintergrund ausgeführt und die GUI nicht blockiert
wird:
class DisconnectAsyncTask extends AsyncTask<Void, Void, Void> {
protected Void doInBackground(Void... params) {
ipcon.disconnect();
return null;
}
@Override
protected void onPostExecute(Void result) {
connect.setText("Connect");
connect.setOnClickListener(new ConnectClickListener());
}
}
Sobald die Verbindung getrennt ist wird ein neuer OnClickListener
gesetzt
der einen ConnectAsyncTask
erzeugt und startet, um die Verbindung neu
aufzubauen wenn der Connect-Knopf geklickt wird:
class ConnectClickListener implements OnClickListener {
public void onClick(View v) {
new ConnectAsyncTask().execute();
}
}
Außerdem sollte der Benutzer nicht den Inhalt der Eingabefelder ändern können solange die Verbindung aufgebaut wird oder besteht und der Trigger-Knopf sollte nicht klickbar sein wenn keine Verbindung besteht.
Der ConnectAsyncTask
und der DisconnectAsyncTask
werden so erweitert,
dass sie die GUI Elemente abhängig von dem aktuellen Zustand der Verbindung
aktivieren oder deaktivieren:
class ConnectAsyncTask extends AsyncTask<Void, Void, Void> {
// [...]
@Override
protected void onPreExecute() {
// [...]
host.setEnabled(false);
port.setEnabled(false);
uid.setEnabled(false);
connect.setEnabled(false);
trigger.setEnabled(false);
}
// [...]
@Override
protected void onPostExecute(Void result) {
// [...]
connect.setEnabled(true);
trigger.setEnabled(true);
}
}
class DisconnectAsyncTask extends AsyncTask<Void, Void, Void> {
// [...]
@Override
protected void onPreExecute() {
connect.setEnabled(false);
trigger.setEnabled(false);
}
// [...]
@Override
protected void onPostExecute(Void result) {
host.setEnabled(true);
port.setEnabled(true);
uid.setEnabled(true);
connect.setEnabled(true);
// [...]
}
}
Das Programm ist noch nicht robust genug. Was passiert wenn die Verbindung nicht hergestellt werden kann? Was passiert wenn kein Industrial Quad Relay Bricklet mit passender UID gefunden werden kann?
Wir brauchen noch Fehlerbehandlung!
Es werden die gleichen Konzepte wie in Schritt 4 des Rauchmelder mit Java auslesen Projekts angewandt, allerdings mit Abwandlungen damit sie in einem GUI Programm funktionieren.
Wir können nicht einfach System.out.println()
für Fehlermeldungen
verwenden, da dies ein GUI Programm ist und kein Konsolenfenster hat.
Stattdessen werden Dialogboxen verwendet.
Der ConnectAsyncTask
muss die Benutzereingaben validieren bevor sie
verwendet werden. Mittels AlertDialog
werden mögliche Probleme gemeldet:
class ConnectAsyncTask extends AsyncTask<Void, Void, ConnectResult> {
// [...]
@Override
protected void onPreExecute() {
currentHost = host.getText().toString();
currentPort = port.getText().toString();
currentUID = uid.getText().toString();
if (currentHost.length() == 0 || currentPort.length() == 0 || currentUID.length() == 0) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage("Host/Port/UID cannot be empty");
builder.create().show();
cancel(true);
return;
}
}
// [...]
}
Der ConnectAsyncTask
bekommt auch einen ProgressDialog
um auf den
laufenden Verbindungsversuch hinzuweisen:
class ConnectAsyncTask extends AsyncTask<Void, Void, ConnectResult> {
// [...]
private ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
// [...]
progressDialog = new ProgressDialog(context);
progressDialog.setMessage("Connecting to " + currentHost + ":" + currentPort);
progressDialog.setCancelable(false);
progressDialog.show();
}
// [...]
}
Dann benötigt die doInBackground()
Methode noch eine Möglichkeit das
Ergebnis des Verbindungsversuchs mitzuteilen. Sie darf zwar nicht direkt mit
dem GUI interagieren, kann aber einen Wert zurückgeben, der dann an den Aufruf
der onPostExecute()
Methode übergeben wird. Wir verwenden hier ein enum
,
das die drei möglichen Ausgänge eines Verbindungsversuchs repräsentiert:
enum ConnectResult {
SUCCESS,
NO_CONNECTION,
NO_DEVICE
}
Mit der getIdentity()
Methode wird überprüft, ob die angegebene UID
wirklich zu einem Industrial Quad Relay Bricklet gehört. Falls das nicht der
Fall ist wird die Verbindung getrennt:
protected ConnectResult doInBackground(Void... params) {
ipcon = new IPConnection();
try {
relay = new BrickletIndustrialQuadRelay(currentUID, ipcon);
} catch(IllegalArgumentException e) {
return ConnectResult.NO_DEVICE;
}
try {
ipcon.connect(currentHost, currentPort);
} catch(java.net.UnknownHostException e) {
return ConnectResult.NO_CONNECTION;
} catch(IllegalArgumentException e) {
return ConnectResult.NO_CONNECTION;
} catch(java.io.IOException e) {
return ConnectResult.NO_CONNECTION;
} catch(com.tinkerforge.AlreadyConnectedException e) {
return ConnectResult.NO_CONNECTION;
}
try {
if (relay.getIdentity().deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER) {
ipcon.disconnect();
return ConnectResult.NO_DEVICE;
}
} catch (com.tinkerforge.TinkerforgeException e1) {
try {
ipcon.disconnect();
} catch (com.tinkerforge.NotConnectedException e2) {
}
return ConnectResult.NO_DEVICE;
}
return ConnectResult.SUCCESS;
}
Die onPostExecute()
Methode muss jetzt entsprechend reagieren. Als erstes
wird immer der ProgressDialog
ausgeblendet:
@Override
protected void onPostExecute(ConnectResult result) {
progressDialog.dismiss();
Im Falle, dass die Verbindung erfolgreich war bleibt die ursprüngliche Logik bestehen:
if (result == ConnectResult.SUCCESS) {
connect.setText("Disconnect");
connect.setOnClickListener(new DisconnectClickListener());
connect.setEnabled(true);
trigger.setEnabled(true);
}
Im Falle eines Fehler wird ein AlertDialog
mit der entsprechenden
Fehlermeldung angezeigt:
else {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
if (result == ConnectResult.NO_CONNECTION) {
builder.setMessage("Could not connect to " + currentHost + ":" + currentPort);
} else { // ConnectResult.NO_DEVICE
builder.setMessage("Could not find Industrial Quad Relay Bricklet [" + currentUID + "]");
}
builder.setCancelable(false);
Die Knöpfe für "Wiederholen" und "Abbrechen" und werden abhängig vom Ausgang des Verbindungsversuchs zum Dialog hinzugefügt:
builder.setPositiveButton("Retry", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
new ConnectAsyncTask().execute();
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
host.setEnabled(true);
port.setEnabled(true);
uid.setEnabled(true);
connect.setText("Connect");
connect.setOnClickListener(new ConnectClickListener());
connect.setEnabled(true);
dialog.dismiss();
}
});
Schlussendlich wird der Dialog erzeugt und angezeigt:
builder.create().show();
}
}
Die App kann sich zum eingestellten Host und Port verbinden und einen Taster auf der Fernbedienung des Garagentoröffners mittels eines Industrial Quad Relay Bricklets betätigen.
Die App speichert die Konfiguration noch nicht. Android bietet dafür die
SharedPreferences
Klasse an. In onCreate()
wird die Konfiguration
geladen:
protected void onCreate(Bundle savedInstanceState) {
// [...]
SharedPreferences settings = getPreferences(0);
host.setText(settings.getString("host", host.getText().toString()));
port.setText(settings.getString("port", port.getText().toString()));
uid.setText(settings.getString("uid", uid.getText().toString()));
}
In onStop()
wird die Konfiguration gespeichert:
protected void onStop() {
super.onStop();
SharedPreferences settings = getPreferences(0);
SharedPreferences.Editor editor = settings.edit();
editor.putString("host", host.getText().toString());
editor.putString("port", port.getText().toString());
editor.putString("uid", uid.getText().toString());
editor.commit();
}
Wenn die Orientierung wechselt dann wird die App beendet und mit der neuen
Orientierung neu gestartet. Dadurch verliert die App die aufgebaute Verbindung.
Daher wird der Verbindungszustand in onSaveInstanceState()
gespeichert:
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("connected", ipcon != null &&
ipcon.getConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED);
}
Und in onCreate()
wiederhergestellt:
protected void onCreate(Bundle savedInstanceState) {
// [...]
if (savedInstanceState != null && savedInstanceState.getBoolean("connected", false)) {
new ConnectAsyncTask().execute();
}
}
Jetzt wird die Konfiguration und der Zustand dauerhaft. auch über einen Neustart der App hinweg, gespeichert.
Das ist es! Die App für die gehackte Fernbedienung des Garagentoröffners ist fertig.
Das Hauptprogramm in einem Stück (Download):
package com.tinkerforge.garagecontrol;
import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import com.tinkerforge.BrickletIndustrialQuadRelay;
import com.tinkerforge.IPConnection;
import com.tinkerforge.TinkerforgeException;
import com.tinkerforge.NotConnectedException;
import com.tinkerforge.AlreadyConnectedException;
public class MainActivity extends Activity {
final Context context = this;
private IPConnection ipcon = null;
private BrickletIndustrialQuadRelay relay = null;
private EditText host;
private EditText port;
private EditText uid;
private Button connect;
private Button trigger;
enum ConnectResult {
SUCCESS,
NO_CONNECTION,
NO_DEVICE
}
class ConnectAsyncTask extends AsyncTask<Void, Void, ConnectResult> {
private ProgressDialog progressDialog;
private String currentHost;
private String currentPort;
private String currentUID;
@Override
protected void onPreExecute() {
currentHost = host.getText().toString();
currentPort = port.getText().toString();
currentUID = uid.getText().toString();
if (currentHost.length() == 0 || currentPort.length() == 0 || currentUID.length() == 0) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage("Host/Port/UID cannot be empty");
builder.create().show();
cancel(true);
return;
}
host.setEnabled(false);
port.setEnabled(false);
uid.setEnabled(false);
connect.setEnabled(false);
trigger.setEnabled(false);
progressDialog = new ProgressDialog(context);
progressDialog.setMessage("Connecting to " + currentHost + ":" + currentPort);
progressDialog.setCancelable(false);
progressDialog.show();
}
protected ConnectResult doInBackground(Void... params) {
ipcon = new IPConnection();
try {
relay = new BrickletIndustrialQuadRelay(currentUID, ipcon);
} catch(IllegalArgumentException e) {
return ConnectResult.NO_DEVICE;
}
try {
ipcon.connect(currentHost, Integer.parseInt(currentPort));
} catch(java.net.UnknownHostException e) {
return ConnectResult.NO_CONNECTION;
} catch(IllegalArgumentException e) {
return ConnectResult.NO_CONNECTION;
} catch(java.io.IOException e) {
return ConnectResult.NO_CONNECTION;
} catch(AlreadyConnectedException e) {
return ConnectResult.NO_CONNECTION;
}
try {
if (relay.getIdentity().deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER) {
ipcon.disconnect();
return ConnectResult.NO_DEVICE;
}
} catch (TinkerforgeException e1) {
try {
ipcon.disconnect();
} catch (NotConnectedException e2) {
}
return ConnectResult.NO_DEVICE;
}
return ConnectResult.SUCCESS;
}
@Override
protected void onPostExecute(ConnectResult result) {
progressDialog.dismiss();
if (result == ConnectResult.SUCCESS) {
connect.setText("Disconnect");
connect.setOnClickListener(new DisconnectClickListener());
connect.setEnabled(true);
trigger.setEnabled(true);
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
if (result == ConnectResult.NO_CONNECTION) {
builder.setMessage("Could not connect to " + currentHost + ":" + currentPort);
} else { // ConnectResult.NO_DEVICE
builder.setMessage("Could not find Industrial Quad Relay Bricklet [" + currentUID + "]");
}
builder.setCancelable(false);
builder.setPositiveButton("Retry", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
new ConnectAsyncTask().execute();
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
host.setEnabled(true);
port.setEnabled(true);
uid.setEnabled(true);
connect.setText("Connect");
connect.setOnClickListener(new ConnectClickListener());
connect.setEnabled(true);
dialog.dismiss();
}
});
builder.create().show();
}
}
}
class DisconnectAsyncTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected void onPreExecute() {
connect.setEnabled(false);
trigger.setEnabled(false);
}
protected Boolean doInBackground(Void... params) {
try {
ipcon.disconnect();
return true;
} catch(TinkerforgeException e) {
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
host.setEnabled(true);
port.setEnabled(true);
uid.setEnabled(true);
connect.setText("Connect");
connect.setOnClickListener(new ConnectClickListener());
}
connect.setEnabled(true);
}
}
class TriggerAsyncTask extends AsyncTask<Void, Void, Void> {
protected Void doInBackground(Void... params) {
try {
relay.setMonoflop(1 << 0, 15, 1500);
} catch (TinkerforgeException e) {
}
return null;
}
}
class ConnectClickListener implements OnClickListener {
public void onClick(View v) {
new ConnectAsyncTask().execute();
}
}
class DisconnectClickListener implements OnClickListener {
public void onClick(View v) {
new DisconnectAsyncTask().execute();
}
}
class TriggerClickListener implements OnClickListener {
public void onClick(View v) {
new TriggerAsyncTask().execute();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
host = (EditText)findViewById(R.id.host);
port = (EditText)findViewById(R.id.port);
uid = (EditText)findViewById(R.id.uid);
connect = (Button)findViewById(R.id.connect);
trigger = (Button)findViewById(R.id.trigger);
SharedPreferences settings = getPreferences(0);
host.setText(settings.getString("host", host.getText().toString()));
port.setText(settings.getString("port", port.getText().toString()));
uid.setText(settings.getString("uid", uid.getText().toString()));
connect.setOnClickListener(new ConnectClickListener());
trigger.setOnClickListener(new TriggerClickListener());
trigger.setEnabled(false);
if (savedInstanceState != null && savedInstanceState.getBoolean("connected", false)) {
new ConnectAsyncTask().execute();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
boolean connected = false;
if (ipcon != null) {
connected = ipcon.getConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED;
}
outState.putBoolean("connected", connected);
}
@Override
protected void onStop() {
super.onStop();
SharedPreferences settings = getPreferences(0);
SharedPreferences.Editor editor = settings.edit();
editor.putString("host", host.getText().toString());
editor.putString("port", port.getText().toString());
editor.putString("uid", uid.getText().toString());
editor.commit();
}
}