Funksteckdosen mit Windows Phone fernsteuern

Für diese Projekt setzen wir voraus, dass das Windows Phone SDK eingerichtet ist und ein grundsätzliches Verständnis der C# 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 Visual Studio Projekt kann hier heruntergeladen werden. Eine Demo-App basierend auf diesem Projekt steht im

Ziele

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

Schritt 1: Die GUI erstellen

Nach dem Erstellen eines neuen "Windows Phone App" namens "Power Outlet Control" in Visual Studio beginnen wir mit der Erstellung der GUI:

App GUI

An das bereits bestehende "LayoutRoot" Element wird ein "StackPanel" angehängt, das dann alle weiteren GUI Elemente aufnehmen wird. Drei "TextBoxes" ermöglichen die Eingabe von Host, Port und UID des Industrial Quad Relay Bricklets. Für die Port Textbox wird das Attribut InputScope="Number" gesetzt, dadurch wird Windows Phone den Inhalt dieser Textbox auf Zahlen beschränken. Unterhalb der Textboxen folgt eine "ProgressBar" (nicht sichtbar im Screenshot) die einen laufenden Verbindungsversuch anzeigen wird. Die letzten fünf Elemente sind ein "Button" für den Aufbau und das Trennen der Verbindung sowie vier "Button" in einem horizontalen StackPanel für das Drücken der verschiedenen Taster auf der gehackten Fernbedienung. Hier ein Auszug aus der MainPage.xaml Datei:

<phone:PhoneApplicationPage>
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <!-- [...] -->

        <StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <TextBlock TextWrapping="Wrap" Text="Host" VerticalAlignment="Top"/>
            <TextBox x:Name="host" Height="72" TextWrapping="Wrap" Text="192.168.178.46" VerticalAlignment="Top"/>
            <TextBlock TextWrapping="Wrap" Text="Port" VerticalAlignment="Top"/>
            <TextBox x:Name="port" Height="72" TextWrapping="Wrap" Text="4223" VerticalAlignment="Top"
                     InputScope="Number"/>
            <TextBlock TextWrapping="Wrap" Text="UID (Industrial Quad Relay Bricklet)" VerticalAlignment="Top"/>
            <TextBox x:Name="uid" Height="72" TextWrapping="Wrap" Text="cuw" VerticalAlignment="Top"/>
            <ProgressBar x:Name="progress" Height="10" VerticalAlignment="Top"/>
            <Button x:Name="connect" Content="Connect" VerticalAlignment="Top" Click="Connect_Click"/>
            <StackPanel Orientation="Horizontal" Height="100" HorizontalAlignment="Center">
                <Button x:Name="a_on" Content="A On" HorizontalAlignment="Center" Click="Aon_Click"/>
                <Button x:Name="a_off" Content="A Off" HorizontalAlignment="Center" Click="Aoff_Click"/>
                <Button x:Name="b_on" Content="B On" HorizontalAlignment="Center" Click="Bon_Click"/>
                <Button x:Name="b_off" Content="B Off" HorizontalAlignment="Center" Click="Boff_Click"/>
            </StackPanel>
        </StackPanel>
    </Grid>
</phone:PhoneApplicationPage>

Damit ist das Layout des GUIs fertig. Die initiale Konfiguration des GUIs wird im Konstruktor der MainPage Klasse vorgenommen. Die "ProgressBar" ist am Anfang nicht sichtbar und Indeterminate Mode wird aktiviert, da die Dauer eines Verbindungsversuchs unbekannt ist:

public partial class MainPage : PhoneApplicationPage
{
    public MainPage()
    {
        InitializeComponent();

        progress.Visibility = Visibility.Collapsed;
        progress.IsIndeterminate = true;
    }
}

Schritt 2: Bricks und Bricklets erkennen

Dieser Schritt ist ähnlich zu Schritt 1 des Rauchmelder mit C# auslesen Projekts. Einige Änderungen sind notwendig damit es in einem GUI Programm funktioniert. Statt dem EnumerateCallback 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 BackgroundWorker aufgerufen werden, so dass es im Hintergrund ausgeführt und die GUI nicht blockiert wird:

public partial class MainPage : PhoneApplicationPage
{
    private IPConnection ipcon = null;
    private BrickletIndustrialQuadRelay relay = null;
    private BackgroundWorker connectWorker = null;

    public MainPage()
    {
        // [...]

        connectWorker = new BackgroundWorker();
        connectWorker.DoWork += ConnectWorker_DoWork;
    }

    private void ConnectWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        string[] argument = e.Argument as string[];

        ipcon = new IPConnection();
        relay = new BrickletIndustrialQuadRelay(argument[2], ipcon);

        ipcon.Connect(argument[0], Convert.ToInt32(argument[1]));
    }

    private void Connect()
    {
        string[] argument = new string[3];

        argument[0] = host.Text;
        argument[1] = port.Text;
        argument[2] = uid.Text;

        connectWorker.RunWorkerAsync(argument);
    }
}

Der BackgroundWorker hat eine DoWork-Event das von einem anderen Thread ausgelöst wird sobald RunWorkerAsync() aufgerufen wurde. Die Host, Port und UID Konfiguration wird dann an das DoWork-Event übergeben. Dies ist notwendig, da die ConnectWorker_DoWork() Methode diese Informationen benötigt, selbst aber nicht auf die GUI Elemente zugreifen darf. Jetzt kann die ConnectWorker_DoWork() Methode die IPConnection und BrickletIndustrialQuadRelay Objekte erzeugen und Connect() aufrufen.

Schlussendlich soll der BackgroundWorker gestartet werden, wenn der Connect-Knopf geklickt wird. Dafür wird die Connect_Click() Methode an das Click-Event des Connect-Knopfes gebunden:

private void Connect_Click(object sender, RoutedEventArgs e)
{
    Connect();
}

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.

Dafür werden <X><Y>_Click() Methode an die Click-Events der Trigger-Knöpfe gebunden. Diese starten einen anderen BackgroundWorker der dann wiederum die SetMonoflop() Methode des Industrial Quad Relay Bricklet aufruft um den entsprechenden Taster auf der gehackten Fernbedienung zu drücken:

public partial class MainPage : PhoneApplicationPage
{
    // [...]

    private BackgroundWorker triggerWorker = null;

    public MainPage()
    {
        // [...]

        triggerWorker = new BackgroundWorker();
        triggerWorker.DoWork += TriggerWorker_DoWork;
    }

    private void TriggerWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        int selectionMask = (int)e.Argument;

        relay.SetMonoflop(selectionMask, 15, 500);
    }

    private void Aon_Click(object sender, RoutedEventArgs e)
    {
        triggerWorker.RunWorkerAsync((1 << 0) | (1 << 2));
    }

    private void Aoff_Click(object sender, RoutedEventArgs e)
    {
        triggerWorker.RunWorkerAsync((1 << 0) | (1 << 3));
    }

    private void Bon_Click(object sender, RoutedEventArgs e)
    {
        triggerWorker.RunWorkerAsync((1 << 1) | (1 << 2));
    }

    private void Boff_Click(object sender, RoutedEventArgs e)
    {
        triggerWorker.RunWorkerAsync((1 << 1) | (1 << 3));
    }
}

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:

public partial class MainPage : PhoneApplicationPage
{
    // [...]

    public MainPage()
    {
        // [...]

        connectWorker.RunWorkerCompleted += ConnectWorker_RunWorkerCompleted;
    }

    private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        connect.Content = "Disconnect";
    }
}

Die ConnectWorker_RunWorkerCompleted() Methode wird nach ConnectWorker_DoWork() aufgerufen. sie ändert den Text des Knopfes zu "Disconnect". Die Connect_Click() Methode entscheidet nun dynamisch was zu tun ist. Falls keine Verbindung besteht wird Connect() aufgerufen, andernfalls wird der BackgroundWorker für das Trennen der Verbindung gestartet:

private void Connect_Click(object sender, RoutedEventArgs e)
{
    if (ipcon == null || ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_DISCONNECTED)
    {
        Connect();
    }
    else
    {
        disconnectWorker.RunWorkerAsync();
    }
}

Der BackgroundWorker für das Trennen der Verbindung ruft die Disconnect() Methode im Hintergrund auf, da dies einen Moment dauern kann und während dieser Zeit die GUI blockiert wäre:

public partial class MainPage : PhoneApplicationPage
{
    // [...]

    private BackgroundWorker disconnectWorker = null;

    public MainPage()
    {
        // [...]

        disconnectWorker = new BackgroundWorker();
        disconnectWorker.DoWork += DisconnectWorker_DoWork;
        disconnectWorker.RunWorkerCompleted += DisconnectWorker_RunWorkerCompleted;
    }

    private void DisconnectWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        ipcon.Disconnect();
    }

    private void DisconnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        connect.Content = "Connect";
    }
}

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 connectWorker und der disconnectWorker werden so erweitert, dass sie die GUI Elemente abhängig von dem aktuellen Zustand der Verbindung aktivieren oder deaktivieren:

private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // [...]

    connect.IsEnabled = true;
    a_on.IsEnabled = true;
    a_off.IsEnabled = true;
    b_on.IsEnabled = true;
    b_off.IsEnabled = true;
}
private void DisconnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // [...]

    host.IsEnabled = true;
    port.IsEnabled = true;
    uid.IsEnabled = true;
    connect.IsEnabled = true;
}
private void Connect()
{
    host.IsEnabled = false;
    port.IsEnabled = false;
    uid.IsEnabled = false;
    connect.IsEnabled = false;
    a_on.IsEnabled = false;
    a_off.IsEnabled = false;
    b_on.IsEnabled = false;
    b_off.IsEnabled = false;

    // [...]
}

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 C# auslesen Projekts angewandt, allerdings mit Abwandlungen damit sie in einem GUI Programm funktionieren.

Wir können nicht einfach System.Console.WriteLine() für Fehlermeldungen verwenden, da dies ein GUI Programm ist und kein Konsolenfenster hat. Stattdessen werden Messageboxen verwendet.

Die Connect() Methode muss die Benutzereingaben validieren bevor sie verwendet werden. Mittels MessageBox werden mögliche Probleme gemeldet. Die "ProgressBar" wird eingeblendet um auf den laufenden Verbindungsversuch hinzuweisen:

private void Connect()
{
    if (host.Text.Length == 0 || port.Text.Length == 0 || uid.Text.Length == 0)
    {
        MessageBox.Show("Host/Port/UID cannot be empty", "Error", MessageBoxButton.OK);
        return;
    }

    progress.Visibility = Visibility.Visible;

    // [...]
}

private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    progress.Visibility = Visibility.Collapsed;

    // [...]
}

Dann benötigt die ConnectWorker_DoWork() Methode noch eine Möglichkeit das Ergebnis des Verbindungsversuchs mitzuteilen. Sie darf zwar nicht direkt mit dem GUI interagieren, kann aber einen Wert an den Result Member des DoWorkEventArgs Parameters zuweisen, der dann an den Aufruf der ConnectWorker_RunWorkerCompleted() 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:

private void ConnectWorker_DoWork(object sender, DoWorkEventArgs e)
{
    string[] argument = e.Argument as string[];

    ipcon = new IPConnection();

    try
    {
        relay = new BrickletIndustrialQuadRelay(argument[2], ipcon);
    }
    catch (ArgumentOutOfRangeException)
    {
        e.Result = ConnectResult.NO_DEVICE;
        return;
    }

    try
    {
        ipcon.Connect(argument[0], Convert.ToInt32(argument[1]));
    }
    catch (System.IO.IOException)
    {
        e.Result = ConnectResult.NO_CONNECTION;
        return;
    }
    catch (ArgumentOutOfRangeException)
    {
        e.Result = ConnectResult.NO_CONNECTION;
        return;
    }

    try
    {
        string uid;
        string connectedUid;
        char position;
        byte[] hardwareVersion;
        byte[] firmwareVersion;
        int deviceIdentifier;

        relay.GetIdentity(out uid, out connectedUid, out position,
                          out hardwareVersion, out firmwareVersion, out deviceIdentifier);

        if (deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER)
        {
            ipcon.Disconnect();
            e.Result = ConnectResult.NO_DEVICE;
            return;
        }
    }
    catch (TinkerforgeException)
    {
        try
        {
            ipcon.Disconnect();
        }
        catch (NotConnectedException)
        {
        }

        e.Result = ConnectResult.NO_DEVICE;
        return;
    }

    e.Result = ConnectResult.SUCCESS;
}

Die ConnectWorker_RunWorkerCompleted() Methode muss jetzt entsprechend reagieren. Als erstes wird immer die "ProgressBar`` ausgeblendet:

private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    ConnectResult result = (ConnectResult)e.Result;

    progress.Visibility = Visibility.Collapsed;

Im Falle, dass die Verbindung erfolgreich war bleibt die ursprüngliche Logik bestehen:

if (result == ConnectResult.SUCCESS)
{
    connect.Content = "Disconnect";
    connect.IsEnabled = true;
    a_on.IsEnabled = true;
    a_off.IsEnabled = true;
    b_on.IsEnabled = true;
    b_off.IsEnabled = true;
}

Im Fehlerfall wird eine MessageBox mit einer entsprechenden Meldung angezeigt:

else
{
    string message;
    MessageBoxResult retry;

    if (result == ConnectResult.NO_CONNECTION) {
        message = "Could not connect to " + host.Text + ":" + port.Text + ". Retry?";
    } else { // ConnectResult.NO_DEVICE
        message = "Could not find Industrial Quad Relay Bricklet [" + uid.Text + "]. Retry?";
    }

    retry = MessageBox.Show(message, "Error", MessageBoxButton.OKCancel);

Abhängig vom Ergebnis der MessageBox wird dann der Verbindungsversuch abgebrochen oder wiederholt:

        if (retry == MessageBoxResult.OK) {
            Connect();
        } else {
            host.IsEnabled = true;
            port.IsEnabled = true;
            uid.IsEnabled = true;
            connect.Content = "Connect";
            connect.IsEnabled = true;
        }
    }
}

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. Windows Phone bietet hierfür die IsolatedStorageSettings Klasse. In OnNavigatedTo() wird die gespeicherte Konfiguration wieder geladen und die Verbindung wiederhergestellt wenn sie zuvor bestanden hat:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    bool connected = false;

    try
    {
        host.Text = settings["host"] as string;
        port.Text = settings["port"] as string;
        uid.Text = settings["uid"] as string;
        connected = settings["connected"].Equals(true);
    }
    catch (KeyNotFoundException)
    {
        settings["host"] = host.Text;
        settings["port"] = port.Text;
        settings["uid"] = uid.Text;
        settings["connected"] = connected;
        settings.Save();
    }

    if (connected &&
        (ipcon == null ||
         ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_DISCONNECTED))
    {
        Connect();
    }
}

In OnNavigatedFrom() wird die Konfiguration dann wieder gespeichert:

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    settings["host"] = host.Text;
    settings["port"] = port.Text;
    settings["uid"] = uid.Text;

    if (ipcon != null && ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED)
    {
        settings["connected"] = true;
    }
    else
    {
        settings["connected"] = false;
    }

    settings.Save();
}

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):

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.IO.IsolatedStorage;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Tinkerforge;

namespace PowerOutletControl
{
    public partial class MainPage : PhoneApplicationPage
    {
        private IPConnection ipcon = null;
        private BrickletIndustrialQuadRelay relay = null;
        private BackgroundWorker connectWorker = null;
        private BackgroundWorker disconnectWorker = null;
        private BackgroundWorker triggerWorker = null;
        private IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;

        enum ConnectResult
        {
            SUCCESS,
            NO_CONNECTION,
            NO_DEVICE
        }

        public MainPage()
        {
            InitializeComponent();

            progress.Visibility = Visibility.Collapsed;
            progress.IsIndeterminate = true;

            a_on.IsEnabled = false;
            a_off.IsEnabled = false;
            b_on.IsEnabled = false;
            b_off.IsEnabled = false;

            connectWorker = new BackgroundWorker();
            connectWorker.DoWork += ConnectWorker_DoWork;
            connectWorker.RunWorkerCompleted += ConnectWorker_RunWorkerCompleted;

            disconnectWorker = new BackgroundWorker();
            disconnectWorker.DoWork += DisconnectWorker_DoWork;
            disconnectWorker.RunWorkerCompleted += DisconnectWorker_RunWorkerCompleted;

            triggerWorker = new BackgroundWorker();
            triggerWorker.DoWork += TriggerWorker_DoWork;
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            bool connected = false;

            try
            {
                host.Text = settings["host"] as string;
                port.Text = settings["port"] as string;
                uid.Text = settings["uid"] as string;
                connected = settings["connected"].Equals(true);
            }
            catch (KeyNotFoundException)
            {
                settings["host"] = host.Text;
                settings["port"] = port.Text;
                settings["uid"] = uid.Text;
                settings["connected"] = connected;
                settings.Save();
            }

            if (connected &&
                (ipcon == null ||
                 ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_DISCONNECTED))
            {
                Connect();
            }
        }

        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            settings["host"] = host.Text;
            settings["port"] = port.Text;
            settings["uid"] = uid.Text;

            if (ipcon != null && ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED)
            {
                settings["connected"] = true;
            }
            else
            {
                settings["connected"] = false;
            }

            settings.Save();
        }

        private void ConnectWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            string[] argument = e.Argument as string[];

            ipcon = new IPConnection();

            try
            {
                relay = new BrickletIndustrialQuadRelay(argument[2], ipcon);
            }
            catch (ArgumentOutOfRangeException)
            {
                e.Result = ConnectResult.NO_DEVICE;
                return;
            }

            try
            {
                ipcon.Connect(argument[0], Convert.ToInt32(argument[1]));
            }
            catch (System.IO.IOException)
            {
                e.Result = ConnectResult.NO_CONNECTION;
                return;
            }
            catch (ArgumentOutOfRangeException)
            {
                e.Result = ConnectResult.NO_CONNECTION;
                return;
            }

            try
            {
                string uid;
                string connectedUid;
                char position;
                byte[] hardwareVersion;
                byte[] firmwareVersion;
                int deviceIdentifier;

                relay.GetIdentity(out uid, out connectedUid, out position,
                                  out hardwareVersion, out firmwareVersion, out deviceIdentifier);

                if (deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER)
                {
                    ipcon.Disconnect();
                    e.Result = ConnectResult.NO_DEVICE;
                    return;
                }
            }
            catch (TinkerforgeException)
            {
                try
                {
                    ipcon.Disconnect();
                }
                catch (NotConnectedException)
                {
                }

                e.Result = ConnectResult.NO_DEVICE;
                return;
            }

            e.Result = ConnectResult.SUCCESS;
        }

        private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            ConnectResult result = (ConnectResult)e.Result;

            progress.Visibility = Visibility.Collapsed;

            if (result == ConnectResult.SUCCESS)
            {
                connect.Content = "Disconnect";
                connect.IsEnabled = true;
                a_on.IsEnabled = true;
                a_off.IsEnabled = true;
                b_on.IsEnabled = true;
                b_off.IsEnabled = true;
            }
            else
            {
                string message;
                MessageBoxResult retry;

                if (result == ConnectResult.NO_CONNECTION) {
                    message = "Could not connect to " + host.Text + ":" + port.Text + ". Retry?";
                } else { // ConnectResult.NO_DEVICE
                    message = "Could not find Industrial Quad Relay Bricklet [" + uid.Text + "]. Retry?";
                }

                retry = MessageBox.Show(message, "Error", MessageBoxButton.OKCancel);

                if (retry == MessageBoxResult.OK) {
                    Connect();
                } else {
                    host.IsEnabled = true;
                    port.IsEnabled = true;
                    uid.IsEnabled = true;
                    connect.Content = "Connect";
                    connect.IsEnabled = true;
                }
            }
        }

        private void DisconnectWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                ipcon.Disconnect();
                e.Result = true;
            }
            catch (NotConnectedException)
            {
                e.Result = false;
            }
        }

        private void DisconnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if ((bool)e.Result)
            {
                host.IsEnabled = true;
                port.IsEnabled = true;
                uid.IsEnabled = true;
                connect.Content = "Connect";
            }

            connect.IsEnabled = true;
        }

        private void TriggerWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            int selectionMask = (int)e.Argument;

            try
            {
                relay.SetMonoflop(selectionMask, 15, 500);
            }
            catch (TinkerforgeException)
            {
            }
        }

        private void Connect()
        {
            if (host.Text.Length == 0 || port.Text.Length == 0 || uid.Text.Length == 0)
            {
                MessageBox.Show("Host/Port/UID cannot be empty", "Error", MessageBoxButton.OK);
                return;
            }

            host.IsEnabled = false;
            port.IsEnabled = false;
            uid.IsEnabled = false;
            connect.IsEnabled = false;
            a_on.IsEnabled = false;
            a_off.IsEnabled = false;
            b_on.IsEnabled = false;
            b_off.IsEnabled = false;

            progress.Visibility = Visibility.Visible;

            string[] argument = new string[3];

            argument[0] = host.Text;
            argument[1] = port.Text;
            argument[2] = uid.Text;

            connectWorker.RunWorkerAsync(argument);
        }

        private void Connect_Click(object sender, RoutedEventArgs e)
        {
            if (ipcon == null || ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_DISCONNECTED)
            {
                Connect();
            }
            else
            {
                connect.IsEnabled = false;
                a_on.IsEnabled = false;
                a_off.IsEnabled = false;
                b_on.IsEnabled = false;
                b_off.IsEnabled = false;

                disconnectWorker.RunWorkerAsync();
            }
        }

        private void Aon_Click(object sender, RoutedEventArgs e)
        {
            triggerWorker.RunWorkerAsync((1 << 0) | (1 << 2));
        }

        private void Aoff_Click(object sender, RoutedEventArgs e)
        {
            triggerWorker.RunWorkerAsync((1 << 0) | (1 << 3));
        }

        private void Bon_Click(object sender, RoutedEventArgs e)
        {
            triggerWorker.RunWorkerAsync((1 << 1) | (1 << 2));
        }

        private void Boff_Click(object sender, RoutedEventArgs e)
        {
            triggerWorker.RunWorkerAsync((1 << 1) | (1 << 3));
        }
    }
}