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.
In this project we will create a simple iOS app that resembles the functionality of the actual remote control.
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.
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.
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!
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!
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.
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.
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