Funksteckdosen mit Android fernsteuern

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.

Ziele

In diesem Projekt werden wir eine einfach Android App entwickeln, die die Funktionalität der eigentlichen Fernbedienung nachbildet.

Schritt 1: Die GUI erstellen

Nach dem Erstellen eines neuen "Android Application Project" namens "Power Outlet Control" in Eclipse beginnen wir mit der Erstellung der GUI:

App 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 fünf Elemente sind ein Knopf für den Aufbau und das Trennen der Verbindung sowie vier Knöpfe in einem "Linear Layout (Horizontal)" für das Drücken der verschiedenen Taster 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 a_on;
    private Button a_off;
    private Button b_on;
    private Button b_off;

    @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);
        a_on = (Button)findViewById(R.id.a_on);
        a_off = (Button)findViewById(R.id.a_off);
        b_on = (Button)findViewById(R.id.b_on);
        b_off = (Button)findViewById(R.id.b_off);
    }
}

Schritt 2: Bricks und Bricklets erkennen

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.

Schritt 3: Taster auslösen

Die Verbindung ist hergestellt und das Industrial Quad Relay Bricklet wurde gefunden, aber es fehlt noch die Logik um einen der Taster auf der Fernbedienung auszulösen wenn einer der Knöpfe geklickt wurde.

Gemäße der Hardware-Aufbau Beschreibung ist die Fernbedienung wie folgt mit den Relais verbunden:

Signal Relais
A 0
B 1
ON 2
OFF 3

Um "A ON" auf der Fernbedienung auszulösen müssen also die Relais 0 und 2 des Industrial Quad Relay Bricklets geschlossen werden. Dies wird durch die Bitmaske (1 << 0) | (1 << 2) repräsentiert.

OnClickListener werden den Trigger-Knöpfen hinzugefügt. Diese erzeugen und starten 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) {
        // [...]

        a_on.setOnClickListener(new TriggerClickListener((1 << 0) | (1 << 2)));
        a_off.setOnClickListener(new TriggerClickListener((1 << 0) | (1 << 3)));
        b_on.setOnClickListener(new TriggerClickListener((1 << 1) | (1 << 2)));
        b_off.setOnClickListener(new TriggerClickListener((1 << 1) | (1 << 3)));
    }

    class TriggerAsyncTask extends AsyncTask<Void, Void, Void> {
        private int selectionMask;

        TriggerAsyncTask(int selectionMask) {
            this.selectionMask = selectionMask;
        }

        protected Void doInBackground(Void... params) {
            relay.setMonoflop(selectionMask, 15, 500);
            return null;
        }
    }

    class TriggerClickListener implements OnClickListener {
        private int selectionMask;

        TriggerClickListener(int selectionMask) {
            this.selectionMask = selectionMask;
        }

        public void onClick(View v) {
            new TriggerAsyncTask(selectionMask).execute();
        }
    }
}

Der Aufruf von setMonoflop(selectionMask, 15, 500) schließt die ausgewählten Relais für 0,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 Funksteckdosen mittels deren gehackter Fernbedienung zu steuern.

Es fehlt noch ein Disconnect-Knopf und die Trigger-Knöpfe kann auch geklickt werden obwohl keine Verbindung besteht. Es fehlt also noch etwas mehr GUI-Logik!

Schritt 4: Weitere 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 die Trigger-Knöpfe sollten 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);
        a_on.setEnabled(false);
        a_off.setEnabled(false);
        b_on.setEnabled(false);
        b_off.setEnabled(false);
    }

    // [...]

    @Override
    protected void onPostExecute(Void result) {
        // [...]

        connect.setEnabled(true);
        a_on.setEnabled(true);
        a_off.setEnabled(true);
        b_on.setEnabled(true);
        b_off.setEnabled(true);
    }
}
class DisconnectAsyncTask extends AsyncTask<Void, Void, Void> {
    // [...]

    @Override
    protected void onPreExecute() {
        connect.setEnabled(false);
        a_on.setEnabled(false);
        a_off.setEnabled(false);
        b_on.setEnabled(false);
        b_off.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!

Schritt 5: Fehlerbehandlung und Reporting

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
}
  • SUCCESS: Die Verbindung wurde hergestellt und ein Industrial Quad Relay Bricklet mit passender UID wurde gefunden.
  • NO_CONNECTION: Die Verbindung konnte nicht hergestellt werden.
  • NO_DEVICE: Die Verbindung wurde hergestellt, aber es wurde kein Industrial Quad Relay Bricklet mit passender UID gefunden.

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, 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 (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);
    a_on.setEnabled(true);
    a_off.setEnabled(true);
    b_on.setEnabled(true);
    b_off.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 betätigen mittels eines Industrial Quad Relay Bricklets.

Schritt 6: Konfiguration und Zustand speichern

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.

Schritt 7: Alles zusammen

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.poweroutletcontrol;

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 a_on;
    private Button a_off;
    private Button b_on;
    private Button b_off;

    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);
            a_on.setEnabled(false);
            a_off.setEnabled(false);
            b_on.setEnabled(false);
            b_off.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);
                a_on.setEnabled(true);
                a_off.setEnabled(true);
                b_on.setEnabled(true);
                b_off.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);
            a_on.setEnabled(false);
            a_off.setEnabled(false);
            b_on.setEnabled(false);
            b_off.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> {
        private int selectionMask;
        
        TriggerAsyncTask(int selectionMask) {
            this.selectionMask = selectionMask;
        }
        
        protected Void doInBackground(Void... params) {
            try {
                relay.setMonoflop(selectionMask, 15, 500);
            } 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 {
        private int selectionMask;
        
        TriggerClickListener(int selectionMask) {
            this.selectionMask = selectionMask;
        }
        
        public void onClick(View v) {
            new TriggerAsyncTask(selectionMask).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);
        a_on = (Button)findViewById(R.id.a_on);
        a_off = (Button)findViewById(R.id.a_off);
        b_on = (Button)findViewById(R.id.b_on);
        b_off = (Button)findViewById(R.id.b_off);

        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());
        a_on.setOnClickListener(new TriggerClickListener((1 << 0) | (1 << 2)));
        a_off.setOnClickListener(new TriggerClickListener((1 << 0) | (1 << 3)));
        b_on.setOnClickListener(new TriggerClickListener((1 << 1) | (1 << 2)));
        b_off.setOnClickListener(new TriggerClickListener((1 << 1) | (1 << 3)));
        
        a_on.setEnabled(false);
        a_off.setEnabled(false);
        b_on.setEnabled(false);
        b_off.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();
    }
}