Funksteckdosen mit iOS fernsteuern

Für diese Projekt setzen wir voraus, dass Xcode eingerichtet ist und ein grundsätzliches Verständnis der Objective-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 Xcode Projekt kann hier heruntergeladen werden.

Ziele

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

Schritt 1: Die GUI erstellen

Nach dem Erstellen einer neuen "iOS Single View Application" namens "Power Outlet Control" in Xcode beginnen wir mit der Erstellung der GUI im Interface Builder:

App GUI

Drei "Text Field" Elemente ermöglichen die Eingabe von Host, Port und UID des Industrial Quad Relay Bricklets. Das nächste Element ist ein "Button" für den Aufbau und das Trennen der Verbindung. Dann folgen vier "Button" Elemente für das Drücken der verschiedenen Taster auf der gehackten Fernbedienung. Unterhalb der vier Knöpfe folgt ein "Indicator" Element (nicht sichtbar im Screenshot), das einen laufenden Verbindungsversuch anzeigen wird.

Damit ist das Layout des GUIs fertig. Um auf die GUI Elemente von Objective-C aus zugreifen zu können fügen wir ein IBOutlet für jedes GUI Element dem ViewController Interface hinzu:

@interface ViewController : UIViewController
{
    IBOutlet UITextField *hostTextField;
    IBOutlet UITextField *portTextField;
    IBOutlet UITextField *uidTextField;
    IBOutlet UIButton *connectButton;
    IBOutlet UIButton *aOnButton;
    IBOutlet UIButton *aOffButton;
    IBOutlet UIButton *bOnButton;
    IBOutlet UIButton *bOffButton;
    IBOutlet UIActivityIndicatorView *indicator;
}

@property (nonatomic, retain) UITextField *hostTextField;
@property (nonatomic, retain) UITextField *portTextField;
@property (nonatomic, retain) UITextField *uidTextField;
@property (nonatomic, retain) UIButton *connectButton;
@property (nonatomic, retain) UIButton *aOnButton;
@property (nonatomic, retain) UIButton *aOffButton;
@property (nonatomic, retain) UIButton *bOnButton;
@property (nonatomic, retain) UIButton *bOffButton;
@property (nonatomic, retain) UIActivityIndicatorView *indicator;

@end

Diese IBOutlet müssen jetzt im Interface Builder mit den GUI Elementen verbunden werden. Dazu über das jeweilige Kontextmenu der GUI Elemente einen neuen "Referencing Outlet" zwischen jedem GUI Element und dem entsprechenden IBOutlet des "File's Owner" erzeugen.

Zuletzt fügen wir noch eine IBAction Methode für jedes "Button" Element zum ViewController Interface hinzu, um auf das Drücken der Knöpfe reagieren zu können:

@interface ViewController : UIViewController
{
    // [...]
}

// [...]

- (IBAction)connectPressed:(id)sender;
- (IBAction)aOnPressed:(id)sender;
- (IBAction)aOffPressed:(id)sender;
- (IBAction)bOnPressed:(id)sender;
- (IBAction)bOffPressed:(id)sender;

@end

Damit diese Methoden auch aufgerufen werden müssen sie im Interface Builder mit den "Touch Up Inside" Events der entsprechenden Knöpfe verbunden werden. Dies ist ähnlich zur der Art und Weise wie die IBOutlet mit ihren GUI Elementen verbunden sind.

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 IPCON_CALLBACK_ENUMERATE 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 ipcon_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 ipcon_connect() aus einem Grand Central Dispatch (GCD) Block aufgerufen werden, so dass es im Hintergrund ausgeführt und die GUI nicht blockiert wird:

@interface ViewController : UIViewController
{
    // [...]

    dispatch_queue_t queue;
    IPConnection ipcon;
    IndustrialQuadRelay relay;
}
- (void)viewDidLoad
{
    // [...]

    queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
}

- (void)connect
{
    NSString *host = hostTextField.text;
    NSString *port = portTextField.text;
    NSString *uid = uidTextField.text;

    dispatch_async(queue, ^{
        ipcon_create(&ipcon);
        industrial_quad_relay_create(&relay, [uid UTF8String], &ipcon);
        ipcon_connect(&ipcon, [host UTF8String], [port intValue]);
    });
}

Bevor der GCD Block (^{ ... }) zur Ausführung gebracht werden kann müssen Host, Port und UID Konfiguration von den GUI Elementen in lokale Variablen zwischengespeichert werden. Dies ist notwendig, da diese Information innerhalb von ^{ ... } benötigt werden, der Zugriff auf GUI Element innerhalb von ^{ ... } aber nicht erlaubt ist, da der Block auf einem anderen Thread ausgeführt werden wird. Jetzt kann ^{ ... } die IPConnection und IndustrialQuadRelay Objekte erzeugen und ipcon_connect() aufrufen.

Schlussendlich soll connect aufgerufen werden, wenn der Connect-Knopf geklickt wird:

- (IBAction)connectPressed:(id)sender
{
    [self 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.

Die IBAction Methoden der vier Trigger-Knöpfe rufen die industrial_quad_relay_set_monoflop() Funktion des Industrial Quad Relay Bricklets mit der entsprechenden Bitmaske auf, um einen Taster auf der Fernbedienung zu drücken. Dieser Aufruf ist in einen GCD Block verpackt, um zu vermeiden etwas potentiell blockierendes im Haupt-Thread der App zu tun:

- (IBAction)aOnPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 0) | (1 << 2), 15, 500);
    });
}

- (IBAction)aOffPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 0) | (1 << 3), 15, 500);
    });
}

- (IBAction)bOnPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 1) | (1 << 2), 15, 500);
    });
}

- (IBAction)bOffPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 1) | (1 << 3), 15, 500);
    });
}

Der Aufruf von industrial_quad_relay_set_monoflop(&relay, selection_mask, 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:

@interface ViewController : UIViewController
{
    // [...]

    BOOL connected;
}
- (void)viewDidLoad
{
    // [...]

    connected = NO;
}

- (void)connect
{
    // [...]

    dispatch_async(queue, ^{
        // [...]

        dispatch_async(dispatch_get_main_queue(), ^{
            [connectButton setTitle:@"Disconnect" forState: UIControlStateNormal];

            connected = YES;
        });
    });
}

Nachdem die Verbindung hergestellt wurde wird ein GCD Block zur Haupt-Queue hinzugefügt in dem der Text des Connect-Knopfes geändert wird. Blöcke in der Haupt-Queue werden durch den Haupt-Thread ausgeführt, dieser darf mit dem GUI interagieren.

Es wird auch einen neue connected Variable zum ViewController hinzugefügt, um den Zustand des GUI zu speichern. Dadurch kann die IBAction des Connect-Knopfes dann entscheiden ob connect oder disconnect aufgerufen werden sollen:

- (IBAction)connectPressed:(id)sender
{
    if (!connected) {
        [self connect];
    } else {
        [self disconnect];
    }
}

Die ipcon_disconnect() Funktion soll nicht direkt aufgerufen werden, da diese einen Moment dauern kann und in dieser Zeit die GUI blockiert ist. Stattdessen wird ipcon_disconnect() in einem GCD Block aufgerufen, wodurch es im Hintergrund ausgeführt und die GUI nicht blockiert wird:

- (void)disconnect
{
    dispatch_async(queue, ^{
        ipcon_disconnect(&ipcon);
        industrial_quad_relay_destroy(&relay);
        ipcon_destroy(&ipcon);

        dispatch_async(dispatch_get_main_queue(), ^{
            [connectButton setTitle:@"Connect" forState: UIControlStateNormal];

            connected = NO;
        });
    });
}

Sobald die Verbindung getrennt wurde wird der Title des Connect-Knopf geändert und die connected Variable wird auf NO gesetzt, so dass die Verbindung neu aufbaut wird wenn der Connect-Knopf das nächste mal geklickt wird.

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.

Die connect und die disconnect Methode werden so erweitert, dass sie die GUI Elemente abhängig von dem aktuellen Zustand der Verbindung aktivieren oder deaktivieren:

- (void)viewDidLoad
{
    // [...]

    aOnButton.enabled = NO;
    aOffButton.enabled = NO;
    bOnButton.enabled = NO;
    bOffButton.enabled = NO;
}
- (void)connect
{
    // [...]

    hostTextField.enabled = NO;
    portTextField.enabled = NO;
    uidTextField.enabled = NO;
    connectButton.enabled = NO;
    aOnButton.enabled = NO;
    aOffButton.enabled = NO;
    bOnButton.enabled = NO;
    bOffButton.enabled = NO;

    dispatch_async(queue, ^{
        // [...]

        dispatch_async(dispatch_get_main_queue(), ^{
            // [...]

            connectButton.enabled = YES;
            aOnButton.enabled = YES;
            aOffButton.enabled = YES;
            bOnButton.enabled = YES;
            bOffButton.enabled = YES;
        });
    });
}
- (void)disconnect
{
    connectButton.enabled = NO;
    aOnButton.enabled = NO;
    aOffButton.enabled = NO;
    bOnButton.enabled = NO;
    bOffButton.enabled = NO;

    dispatch_async(queue, ^{
        // [...]

        dispatch_async(dispatch_get_main_queue(), ^{
            // [...]

            connectButton.enabled = YES;
            hostTextField.enabled = YES;
            portTextField.enabled = YES;
            uidTextField.enabled = YES;
        });
    });
}

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 printf() für Fehlermeldungen verwenden, da dies ein GUI Programm ist und kein Konsolenfenster hat. Stattdessen werden Dialogboxen verwendet.

Die connect Methode muss die Benutzereingaben validieren bevor sie verwendet werden. Mittels UIAlertView werden mögliche Probleme gemeldet:

- (void)connect
{
    NSString *host = hostTextField.text;
    NSString *port = portTextField.text;
    NSString *uid = uidTextField.text;

    if ([host length] == 0 || [port length] == 0 || [uid length] == 0) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Host/Port/UID cannot be empty"
                                                  delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Okay", nil];
        [alert show];
        return;
    }

    int portNumber = [port intValue];
    NSString *reformatedPort = [NSString stringWithFormat:@"%d", portNumber];

    if (portNumber < 1 || portNumber > 65535 || ![port isEqualToString:reformatedPort]) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Port number is invalid"
                                                  delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Okay", nil];
        [alert show];
        return;
    }

    // [...]
}

Das "Indicator" Element wird eingeblendet um auf den laufenden Verbindungsversuch hinzuweisen:

- (void)viewDidLoad
{
    // [...]

    [indicator setHidden:YES];
}

- (void)connect
{
    // [...]

    [indicator setHidden:NO];
    [indicator startAnimating];

    // [...]

    dispatch_async(queue, ^{
        // [...]

        dispatch_async(dispatch_get_main_queue(), ^{
            [indicator setHidden:YES];

            // [...]
        });
    });
}

Der Aufruf von ipcon_connect() kann fehlschlagen, z.B. weil Host oder Port nicht stimmen. In diesem Fall muss ein Fehler gemeldet werden:

- (void)connect
{
    // [...]

    dispatch_async(queue, ^{
        ipcon_create(&ipcon);
        industrial_quad_relay_create(&relay, [uid UTF8String], &ipcon);

        if (ipcon_connect(&ipcon, [host UTF8String], portNumber) < 0) {
            industrial_quad_relay_destroy(&relay);
            ipcon_destroy(&ipcon);

            dispatch_async(dispatch_get_main_queue(), ^{
                [indicator setHidden:YES];

                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error"
                                                          message:[NSString stringWithFormat:@"Could not connect to %@:%d", host, portNumber]
                                                          delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Retry", nil];
                [alert show];
            });

            return;
        }

        // [...]
    });
}

Mit der industrial_quad_relay_get_identity() Funktion 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:

- (void)connect
{
    // [...]

    dispatch_async(queue, ^{
        // [...]

        char uid_[8];
        char connected_uid[8];
        char position;
        uint8_t hardware_version[3];
        uint8_t firmware_version[3];
        uint16_t device_identifier;

        if (industrial_quad_relay_get_identity(&relay, uid_, connected_uid, &position,
                                               hardware_version, firmware_version, &device_identifier) < 0 ||
            device_identifier != INDUSTRIAL_QUAD_RELAY_DEVICE_IDENTIFIER) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [indicator setHidden:YES];

                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error"
                                                          message:[NSString stringWithFormat:@"Could not find Industrial Quad Relay Bricklet [%@]", uid]
                                                          delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Retry", nil];
                [alert show];
            });

            return;
        }

        // [...]
    });
}

In beiden Fällen wird eine UIAlertView mit delegate auf self gesetzt verwendet, um den Fehler zu melden. Der Delegate muss dem UIAlertViewDelegate Protokoll entsprechen und eine clickedButtonAtIndex Methode implementieren, welche aufgerufen wird wenn ein Knopf der UIAlertView geklickt wird. Dies können wir verwenden um einen Retry-Knopf zu realisieren. Wird der Retry-Knopf (buttonIndex == 1) geklickt dann wird connect erneut aufgerufen, andernfalls wird der Verbindungsversuch aufgegeben sichergestellt, dass sich das GUI im richtigen Zustand befindet:

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1) {
        [self connect];
    } else {
        [connectButton setTitle:@"Connect" forState: UIControlStateNormal];

        connectButton.enabled = YES;
        hostTextField.enabled = YES;
        portTextField.enabled = YES;
        uidTextField.enabled = YES;
    }
}

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. iOS bietet dafür die NSUserDefaults Klasse an. Zwei neue Methoden werden dem ViewController hinzugefügt um die Konfiguration zu speichern und wieder zu laden:

- (void)saveState
{
    [[NSUserDefaults standardUserDefaults] setObject:hostTextField.text forKey:@"host"];
    [[NSUserDefaults standardUserDefaults] setObject:portTextField.text forKey:@"port"];
    [[NSUserDefaults standardUserDefaults] setObject:uidTextField.text forKey:@"uid"];
    [[NSUserDefaults standardUserDefaults] setBool:connected forKey:@"connected"];
}
- (void)restoreState
{
    NSString *host = [[NSUserDefaults standardUserDefaults] stringForKey:@"host"];
    NSString *port = [[NSUserDefaults standardUserDefaults] stringForKey:@"port"];
    NSString *uid = [[NSUserDefaults standardUserDefaults] stringForKey:@"uid"];

    if (host != nil) {
        hostTextField.text = host;
    }

    if (port != nil) {
        portTextField.text = port;
    }

    if (uid != nil) {
        uidTextField.text = uid;
    }

    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"connected"] && !connected) {
        [self connect];
    }
}

Dann wird noch der AppDelegate so geändert, dass die neuen Methoden zum richtigen Zeitpunkt aufgerufen werden:

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    [self.viewController saveState];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [self.viewController restoreState];
}

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 (Downloads: ViewController.h, ViewController.m):

#import <UIKit/UIKit.h>

#include "ip_connection.h"
#include "bricklet_industrial_quad_relay.h"

@interface ViewController : UIViewController
{
    IBOutlet UITextField *hostTextField;
    IBOutlet UITextField *portTextField;
    IBOutlet UITextField *uidTextField;
    IBOutlet UIButton *connectButton;
    IBOutlet UIButton *aOnButton;
    IBOutlet UIButton *aOffButton;
    IBOutlet UIButton *bOnButton;
    IBOutlet UIButton *bOffButton;
    IBOutlet UIActivityIndicatorView *indicator;

    dispatch_queue_t queue;
    IPConnection ipcon;
    IndustrialQuadRelay relay;
    BOOL connected;
}

@property (nonatomic, retain) UITextField *hostTextField;
@property (nonatomic, retain) UITextField *portTextField;
@property (nonatomic, retain) UITextField *uidTextField;
@property (nonatomic, retain) UIButton *connectButton;
@property (nonatomic, retain) UIButton *aOnButton;
@property (nonatomic, retain) UIButton *aOffButton;
@property (nonatomic, retain) UIButton *bOnButton;
@property (nonatomic, retain) UIButton *bOffButton;
@property (nonatomic, retain) UIActivityIndicatorView *indicator;

- (IBAction)connectPressed:(id)sender;
- (IBAction)aOnPressed:(id)sender;
- (IBAction)aOffPressed:(id)sender;
- (IBAction)bOnPressed:(id)sender;
- (IBAction)bOffPressed:(id)sender;

- (void)saveState;
- (void)restoreState;

@end
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

@synthesize hostTextField;
@synthesize portTextField;
@synthesize uidTextField;
@synthesize connectButton;
@synthesize aOnButton;
@synthesize aOffButton;
@synthesize bOnButton;
@synthesize bOffButton;
@synthesize indicator;

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    connected = NO;

    aOnButton.enabled = NO;
    aOffButton.enabled = NO;
    bOnButton.enabled = NO;
    bOffButton.enabled = NO;
    [indicator setHidden:YES];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)connect
{
    NSString *host = hostTextField.text;
    NSString *port = portTextField.text;
    NSString *uid = uidTextField.text;

    if ([host length] == 0 || [port length] == 0 || [uid length] == 0) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Host/Port/UID cannot be empty"
                                                  delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Okay", nil];
        [alert show];
        return;
    }

    int portNumber = [port intValue];
    NSString *reformatedPort = [NSString stringWithFormat:@"%d", portNumber];

    if (portNumber < 1 || portNumber > 65535 || ![port isEqualToString:reformatedPort]) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Port number is invalid"
                                                  delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Okay", nil];
        [alert show];
        return;
    }

    hostTextField.enabled = NO;
    portTextField.enabled = NO;
    uidTextField.enabled = NO;
    connectButton.enabled = NO;
    aOnButton.enabled = NO;
    aOffButton.enabled = NO;
    bOnButton.enabled = NO;
    bOffButton.enabled = NO;
    [indicator setHidden:NO];
    [indicator startAnimating];

    dispatch_async(queue, ^{
        ipcon_create(&ipcon);
        industrial_quad_relay_create(&relay, [uid UTF8String], &ipcon);

        if (ipcon_connect(&ipcon, [host UTF8String], portNumber) < 0) {
            industrial_quad_relay_destroy(&relay);
            ipcon_destroy(&ipcon);

            dispatch_async(dispatch_get_main_queue(), ^{
                [indicator setHidden:YES];

                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error"
                                                          message:[NSString stringWithFormat:@"Could not connect to %@:%d", host, portNumber]
                                                          delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Retry", nil];
                [alert show];
            });

            return;
        }

        char uid_[8];
        char connected_uid[8];
        char position;
        uint8_t hardware_version[3];
        uint8_t firmware_version[3];
        uint16_t device_identifier;

        if (industrial_quad_relay_get_identity(&relay, uid_, connected_uid, &position,
                                               hardware_version, firmware_version, &device_identifier) < 0 ||
            device_identifier != INDUSTRIAL_QUAD_RELAY_DEVICE_IDENTIFIER) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [indicator setHidden:YES];

                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error"
                                                          message:[NSString stringWithFormat:@"Could not find Industrial Quad Relay Bricklet [%@]", uid]
                                                          delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Retry", nil];
                [alert show];
            });

            return;
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            [indicator setHidden:YES];

            [connectButton setTitle:@"Disconnect" forState: UIControlStateNormal];

            connectButton.enabled = YES;
            aOnButton.enabled = YES;
            aOffButton.enabled = YES;
            bOnButton.enabled = YES;
            bOffButton.enabled = YES;

            connected = YES;
        });
    });
}

- (void)disconnect
{
    connectButton.enabled = NO;
    aOnButton.enabled = NO;
    aOffButton.enabled = NO;
    bOnButton.enabled = NO;
    bOffButton.enabled = NO;

    dispatch_async(queue, ^{
        if (ipcon_disconnect(&ipcon) < 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                connectButton.enabled = YES;
            });

            return;
        }

        industrial_quad_relay_destroy(&relay);
        ipcon_destroy(&ipcon);

        dispatch_async(dispatch_get_main_queue(), ^{
            [connectButton setTitle:@"Connect" forState: UIControlStateNormal];

            connectButton.enabled = YES;
            hostTextField.enabled = YES;
            portTextField.enabled = YES;
            uidTextField.enabled = YES;

            connected = NO;
        });
    });
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1) {
        [self connect];
    } else {
        [connectButton setTitle:@"Connect" forState: UIControlStateNormal];

        connectButton.enabled = YES;
        hostTextField.enabled = YES;
        portTextField.enabled = YES;
        uidTextField.enabled = YES;
    }
}

- (IBAction)connectPressed:(id)sender
{
    if (!connected) {
        [self connect];
    } else {
        [self disconnect];
    }
}

- (IBAction)aOnPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 0) | (1 << 2), 15, 500);
    });
}

- (IBAction)aOffPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 0) | (1 << 3), 15, 500);
    });
}

- (IBAction)bOnPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 1) | (1 << 2), 15, 500);
    });
}

- (IBAction)bOffPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 1) | (1 << 3), 15, 500);
    });
}

- (void)saveState
{
    [[NSUserDefaults standardUserDefaults] setObject:hostTextField.text forKey:@"host"];
    [[NSUserDefaults standardUserDefaults] setObject:portTextField.text forKey:@"port"];
    [[NSUserDefaults standardUserDefaults] setObject:uidTextField.text forKey:@"uid"];
    [[NSUserDefaults standardUserDefaults] setBool:connected forKey:@"connected"];
}

- (void)restoreState
{
    NSString *host = [[NSUserDefaults standardUserDefaults] stringForKey:@"host"];
    NSString *port = [[NSUserDefaults standardUserDefaults] stringForKey:@"port"];
    NSString *uid = [[NSUserDefaults standardUserDefaults] stringForKey:@"uid"];

    if (host != nil) {
        hostTextField.text = host;
    }

    if (port != nil) {
        portTextField.text = port;
    }

    if (uid != nil) {
        uidTextField.text = uid;
    }

    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"connected"] && !connected) {
        [self connect];
    }
}

@end