Control Garage Door Openers using iOS

For this project we are assuming, that you have Xcode set up and that you have a rudimentary understanding of the Objective-C language.

If you are totally new to Objective-C itself you should start here. If you are new to the Tinkerforge API, you should start here.

We are also assuming that you have a remote control connected to an Industrial Quad Relay Bricklet as described here.

The complete Xcode project can be downloaded here.

Goals

In this project we will create a simple iOS app that resembles the functionality of the actual remote control.

Step 1: Creating the GUI

After creating a new "iOS Single View Application" named "Garage Control" in Xcode we start with creating the GUI in the Interface Builder:

App GUI

Three "Text Field" elements allow to enter the host, port and UID of the Industrial Quad Relay Bricklet. The next element is a "Button" to connect and disconnect. Below that goes another "Button" element to trigger the remote control. The final element is an "Indicator" (not visible on screenshot) that will be used to indicate that a connection attempt is in progress.

Now the GUI layout is finished. To access the GUI components in the Objective-C code we add an IBOutlet for each GUI element to the ViewController interface:

@interface ViewController : UIViewController
{
    IBOutlet UITextField *hostTextField;
    IBOutlet UITextField *portTextField;
    IBOutlet UITextField *uidTextField;
    IBOutlet UIButton *connectButton;
    IBOutlet UIButton *triggerButton;
    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 *triggerButton;
@property (nonatomic, retain) UIActivityIndicatorView *indicator;

@end

Now these IBOutlet have to be connected to the GUI elements in the Interface Builder. To do this create a new "Referencing Outlet" between each GUI element and its corresponding IBOutlet of the "File's Owner" using the context menu of each GUI element.

Finally we add a IBAction method for each "Button" element to the ViewController interface, to be able to react on button presses:

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

// [...]

- (IBAction)connectPressed:(id)sender;
- (IBAction)triggerPressed:(id)sender;

@end

To have this methods called correctly they have to be connected to the "Touch Up Inside" events of the corresponding buttons in the Interface Builder. This is similar to the way IBOutlet are connected to their GUI elements.

Step 2: Discover Bricks and Bricklets

This step is similar to step 1 in the Read out Smoke Detectors using C project. We apply some changes to make it work in a GUI program and instead of using the IPCON_CALLBACK_ENUMERATE to discover the Industrial Quad Relay Bricklet its UID has to be specified. This approach allows to pick the correct Industrial Quad Relay Bricklet even if multiple are connected to the same host at once.

We don't want to call the ipcon_connect() method directly, because it might take a moment and block the GUI during that period of time. Instead ipcon_connect() will be called from a Grand Central Dispatch (GCD) block, so it will run in the background and the GUI stays responsive:

@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]);
    });
}

Before the GCD block (^{ ... }) is queued for execution the host, port and UID configuration from the GUI elements is stores in local variables. This is necessary, because this information is needed inside ^{ ... }, but accessing the GUI elements is not allowed inside ^{ ... } as it will be executed on a different thread. Now the ^{ ... } can create an IPConnection and IndustrialQuadRelay object and call the ipcon_connect() function.

Finally, connect should called when the connect button is clicked:

- (IBAction)connectPressed:(id)sender
{
    [self connect];
}

Host, port and UID can now be configured and a click on the connect button establishes the connection.

Step 3: Triggering Switches

The connection is established and the Industrial Quad Relay Bricklet is found but there is no logic yet to trigger the switch on the remote control if the trigger button is clicked.

The IBAction method for the trigger button calls the industrial_quad_relay_set_monoflop() function of the Industrial Quad Relay Bricklet to trigger a switch on the remote control. This call is wrapped into a GCD block to avoid doing a potentially blocking operation on the main thread of the app:

- (IBAction)triggerPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 0), (1 << 0), 1500);
    });
}

The call to industrial_quad_relay_set_monoflop(&relay, 1 << 0, 15, 500) closes the first relay for 1.5s then opens it again.

That's it. If we would copy these three steps together in one project, we would have a working app that allows a smart phone to control a garage door opener using its hacked remote control!

We don't have a disconnect button yet and the trigger button can be clicked before the connection is established. We need some more GUI logic!

Step 4: More GUI logic

There is no button to close the connection again after it got established. The connect button could do this. When the connection is established it should allow to disconnect it again:

@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;
        });
    });
}

After the connection got established a GCD block is added to the main queue to change the text on the connect button. Blocks in the main queue are executed by the main thread that is allowed to interact with the GUI.

We also add a new connected variable to the ViewController to keep track of the GUI state. Then the IBAction method for the connect button knows when to call connect and when to call the new disconnect method:

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

We don't want to call the ipcon_disconnect() function directly, because it might take a moment and block the GUI during that period of time. Instead ipcon_disconnect() will be called from another GCD block, so it will run in the background and the GUI stays responsive:

- (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;
        });
    });
}

Once the connection is closed the title on the connect button is changed and the connected variable is set to NO so a new connection will be establish if the connect button is clicked again.

Finally, the user should not be able to change the content of the text fields during the time the connection gets established and the trigger button should not be clickable if there is no connection.

The connect and the disconnect methods are extended to disable and enable the GUI elements according to the current connection state:

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

    triggerButton.enabled = NO;
}
- (void)connect
{
    // [...]

    hostTextField.enabled = NO;
    portTextField.enabled = NO;
    uidTextField.enabled = NO;
    connectButton.enabled = NO;
    triggerButton.enabled = NO;

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

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

            connectButton.enabled = YES;
            triggerButton.enabled = YES;
        });
    });
}
- (void)disconnect
{
    connectButton.enabled = NO;
    triggerButton.enabled = NO;

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

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

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

But the program is not yet robust enough. What happens if can't connect? What happens if there is no Industrial Quad Relay Bricklet with the given UID?

What we need is error handling!

Step 5: Error Handling and Reporting

We will use similar principals as in step 4 of the Read out Smoke Detectors using C project, but with some changes to make it work in a GUI program.

We can't just use printf() for error reporting because there is no console window in an app. Instead dialog boxes are used.

The connect method has to validate the user input before using it. An UIAlertView is used to report possible problems:

- (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;
    }

    // [...]
}

Also the progress indicator is made visible to indicate that a connection attempt is in progress:

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

    [indicator setHidden:YES];
}

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

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

    // [...]

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

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

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

The call of ipcon_connect() might fail, because host or port might be wrong. We need to check for this and report an error in that case:

- (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;
        }

        // [...]
    });
}

The industrial_quad_relay_get_identity() function is used to check that the device for the given UID really is an Industrial Quad Relay Bricklet. If this is not the case then the connection gets closed:

- (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 both cases an UIAlertView with delegate set to self is used to report the error. The delegate has to conform to the UIAlertViewDelegate protocol by implementing a clickedButtonAtIndex method, which is called if the user clicks a button the the UIAlertView. We can use this to realize a retry button. If the retry button (buttonIndex == 1) is clicked then connect is called again, otherwise the connection attempt is aborted and the GUI is changed to the correct state:

- (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;
    }
}

Now the app can connect to an configurable host and port and trigger a button on the remote control of your garage door opener using an Industrial Quad Relay Bricklet.

Step 6: Persistent Configuration and State

The app doesn't store its configuration yet. iOS provides the NSUserDefaults class to take care of this. Two new methods are added to the ViewController to save and restore the current state:

- (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];
    }
}

Then the AppDelegate is changed to call them in the right places:

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

Now the configuration and state is stored persistent across a restart of the app.

Step 7: Everything put together

That's it! We are done with the app for our hacked garage door opener remote control.

Now all of the above put together (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 *triggerButton;
    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 *triggerButton;
@property (nonatomic, retain) UIActivityIndicatorView *indicator;

- (IBAction)connectPressed:(id)sender;
- (IBAction)triggerPressed:(id)sender;

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

@end
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

@synthesize hostTextField;
@synthesize portTextField;
@synthesize uidTextField;
@synthesize connectButton;
@synthesize triggerButton;
@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;

    triggerButton.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;
    triggerButton.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;
            triggerButton.enabled = YES;

            connected = YES;
        });
    });
}

- (void)disconnect
{
    connectButton.enabled = NO;
    triggerButton.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)triggerPressed:(id)sender
{
    dispatch_async(queue, ^{
        industrial_quad_relay_set_monoflop(&relay, (1 << 0), (1 << 0), 1500);
    });
}

- (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