Control Garage Door Openers using iOS – Tinkerforge

- Getting Started
  - Primer
  - Tutorials
  - FAQ
- Hardware
  - Bricks
  - Bricklets
  - Master Extensions
  - Power Supplies
  - Accessories
  - Timeline
- Software
  - Brick Daemon (brickd)
  - Brick Viewer (brickv)
  - API Bindings
  - Firmwares and Plugins
  - Device Identifier
  - Source Code and Bug Tracking
  - Programming Interface
- Starter Kits
  - Weather Station
  - Hardware Hacking
    - Features
    - Description
    - Technical Specifications
    - Resources
    - Required Tools
    - Firmware updating and first tests
    - How it works
    - Hardware Hacking for Beginners
    - Examples
      - Control Mains Switches Remotely
      - Read out Smoke Detectors
      - Control Garage Doors Remotely
        - Hardware Setup
        - Using Android
        - Using Windows Phone
        - Using iOS
          - Goals
          - Step 1: Creating the GUI
          - Step 2: Discover Bricks and Bricklets
          - Step 3: Triggering Switches
          - Step 4: More GUI logic
          - Step 5: Error Handling and Reporting
          - Step 6: Persistent Configuration and State
          - Step 7: Everything put together
      - Is someone at the Door? - Doorbell Notifier
  - Server Room Monitoring
  - Blinkenlights
  - Internet of Things
  - Camera Slider
- Embedded Boards
  - Raspberry Pi
- Specifications
  - Technical Data
  - TCP/IP Protocol
  - Modbus Protocol
  - Wireshark Dissector

# 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. A demo app based on this project is available in the App Store.

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

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

The content of this page is licensed under Creative Commons Attribution 3.0 Unported License.