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.
In diesem Projekt werden wir eine einfach iOS App entwickeln, die die Funktionalität der eigentlichen Fernbedienung nachbildet.
Nach dem Erstellen einer neuen "iOS Single View Application" namens "Garage Control" in Xcode beginnen wir mit der Erstellung der GUI im Interface Builder:
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 folgt ein "Button" Element für das Auslösen eines Tastendrucks auf der gehackten Fernbedienung. Unterhalb der 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 *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
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)triggerPressed:(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.
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.
Die Verbindung ist hergestellt und das Industrial Quad Relay Bricklet wurde gefunden, aber es fehlt noch die Logik um einen Taster auf der Fernbedienung auszulösen wenn der Knopf geklickt wurde.
Die IBAction
Methode des Trigger-Knopf ruft die
industrial_quad_relay_set_monoflop()
Funktion des Industrial Quad Relay
Bricklets 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)triggerPressed:(id)sender
{
dispatch_async(queue, ^{
industrial_quad_relay_set_monoflop(&relay, (1 << 0), (1 << 0), 1500);
});
}
Der Aufruf von industrial_quad_relay_set_monoflop(&relay, 1 << 0, 15, 500)
schließt das erste Relais für 1,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 den Garagentoröffner mittels dessen gehackter Fernbedienung zu steuern.
Es fehlt noch ein Disconnect-Knopf und der Trigger-Knopf kann auch geklickt werden obwohl keine Verbindung besteht. Es fehlt also noch etwas mehr 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 der Trigger-Knopf sollte 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
{
// [...]
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;
});
});
}
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!
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 mittels eines Industrial Quad Relay Bricklets betätigen.
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.
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 *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