In part 1 I described the hardware for this project and how to connect it. In part 2 I described the Arduino code that runs in the LightBlue Bean. In the final articles we will take a look at the iOS code for a simple application that triggers the lock.
Due to the size of the iOS code, I have decided to split the final part into two. This first half looks at the main iOS code and the second will look at the settings screen.
The source code for the Arduino and iOS application is available on GitHub
Update
In order to run this project on an iOS device you need a paid-membership of the Apple iOS developer program, as without this you can only run apps under the simulator, and the simulator doesn’t have BLE support.
In order to help with this, I have added a simple OSX application to the repository. It is functionally equivalent but can be run on any Mac without any payment to Apple.
The iOS project uses the Punch Through Bean SDK for OSX/iOS, but this is not contained the BeanLock GitHub repository. To simply the installation of the Bean SDK the BeanLock repository contains a Podfile that will manage the installation for you.
If you haven’t already installed CocoaPods, then you can do so by following the CocoPods installation guide. Once you have CocoaPods installed, you can add the Bean SDK.
Open a terminal window and change to the directory where you have copied the BeanLock repository. Then enter the following commands:
cd iOS pod install |
You should see messages as the Bean SDK is downloaded. You can then open Beanlock.xcworkspace (not Beanlock.xcodeproj) in Xcode.
At this point you should be able to compile and run the project and discover Beans through the settings screen. Unfortunately BLE is not supported on the iOS simulator, so you will need to run the code on a real device – either an iPhone (4S, 5, 5C 5S) or an iPad (3,4, air or mini).
The user interface of the app isn’t going to win any design awards but, hey, this is about the code right?
The main screen shows the connection status and, when connected to a Bean, the current temperature and battery voltage. The main screen also has a button to ‘open’ the lock and another to access the settings screen.
The settings screen displays the currently visible beans. Selecting a bean from the table marks it with a checkmark to indicate that this is the current ‘target’ Bean for the application. On the settings screen you can also enter the password that is supplied to the lock (This needs to match the password in the Arduino code) and a switch to select ‘auto unlock’ mode.
When ‘auto unlock’ is selected responding to an app notification will send the unlock command without requiring you to press “Open”.
BLBeanStuff
Punch Through Design provide an API that wraps around the Apple Core-Bluetooth framework to simplify the discovery of Beans, connecting to Beans and communicating with Beans. The core of this API is the PTDBeanManager. This class does a lot for you, but I wanted a re-usable class to handle the discovery and tracking of visible beans – enter the BLBeanStuff class.
Looking at BLBeanStuff.h we can see that it defines the following properties –
@interface BLBeanStuff: NSObject @property (weak,nonatomic) id delegate; @property (nonatomic,readonly) NSArray *connectedBeans; @property (nonatomic,readonly) NSArray *discoveredBeans; |
These are a delegate (We will see more about this later), a read-only array of connected Beans and a read-only array of discovered Beans.
BLBeanstuff.h also defines some methods –
-(BOOL) isConnectedToBean:(PTDBean *)bean; -(BOOL) isConnectedToBeanWithIdentifier:(NSUUID *)identifier; -(void) connectToBean:(PTDBean *)bean; -(BOOL) connectToBeanWithIdentifier:(NSUUID *)identifier; -(void) disconnectFromBean:(PTDBean *)bean; -(void) startScanningForBeans; -(void) stopScanningForBeans; +(BLBeanStuff *)sharedBeanStuff; |
- isConnectedToBean returns a boolean that indicates whether the specified bean is currently connected
- isConnectedToBeanWithIdentifier does the same thing except that it accepts a UUID rather than a PTDBean
- connectToBean attempts to connect to the specified bean
- connectToBeanWithIdentifier is similar, except that you will see it returns a boolean – If a Bean with the specified identifier hasn’t been discovered then this method will return NO. If the Bean is known then a connection is attempted
- disconnectFromBean does exactly what you would think
- startScanningForBeans and stopScanningForBeans start and stop scanning for Beans respectively
The final method is sharedBeanStuff. BLBeanStuff is implemented as a singleton – this means that there is only ever one instance of it. sharedBeanStuff is used to obtain the reference to this instance.
Finally, BLBeanStuff.h also defines the protocol for the BLBeanStuffDelegate –
protocol BLBeanStuffDelegate @optional -(void) didUpdateDiscoveredBeans:(NSArray *)discoveredBeans withBean:(PTDBean *)newBean; @optional -(void) didConnectToBean:(PTDBean *)bean; @optional -(void) didDisconnectFromBean:(PTDBean *)bean; @end |
This protocol is implemented by the class that is set as the BLBeanStuff delegate and specifies three methods.
- didUpdateDiscoveredBeans:withBean is called when a new Bean is discovered
- didConnectToBean: is called when a connection to a Bean is established
- didDisconnectFromBean: is called when a connection to a Bean is lost
There is nothing particularly remarkable in the BLBeanStuff.m code, so for brevity I won’t go through it here.
A Bean by any other name…
At this point, I want to discuss Bean Identification. A Bean has three identifiers –
- A serial number printed on the BLE module
- A Bluetooth UUID
- A Name
None of these are related. The serial number printed on the module is not exposed by any BLE characteristics of the Bean. The UUID is not printed on the label, and you can change the Bean’s name by the API or using the Bean application.
BeanLock stores two identifiers for your selected bean in NSUserDefaults – the UUID and the name. The name is displayed on screen as it is more ‘user friendly’. The UUID is used internally for identifying and connecting to Beans as it is guaranteed to be unique and cannot be changed.
View Controllers
As you might guess from the two screen shots above, the application has two UIViewController subclasses – one for the main view and one for the settings. The same classes handle both iPhone and iPad interfaces, with a little bit of code to handle the presentation of the settings page as a pop over on the iPad rather than as a modal view.
BLMainViewController
Much of the view controller code is straight-forward, so I won’t go through it here (If you are new to iOS programming and looking for some good tutorials I suggest taking a look at raywenderlich.com). I will go through the code that manages the connection to the Bean and communicating with it.
processSettings
The processSettings method is called when the view controller appears on screen and when the settings view (Either page or pop over) is dismissed. It reads the settings from NSUserDefaults and sets up the connection to the selected Bean. It looks like this –
- (void) processSettings { self.myBeanStuff.delegate=self; // Ensure that we are re-set as the BeanStuff delegate NSUserDefaults *userDefaults=[NSUserDefaults standardUserDefaults]; NSString *newTargetBean=[userDefaults objectForKey:kBLTargetBeanPref]; if (newTargetBean == nil) { self.messageLabel.text=@"Please select a lock in settings"; self.statusLabel.text=@""; } if (![newTargetBean isEqualToString:self.targetBean]) { self.targetBean=newTargetBean; self.targetBeanName=[userDefaults objectForKey:kBLTargetBeanNamePref]; if (self.connectedBean != nil) { [self.myBeanStuff disconnectFromBean:self.connectedBean]; } else { [self connect]; } } } |
The first thing this method does is ensure that this class is set as the delegate for the BLBeanStuff object – This needs to be reset as the settings view will have set itself as the delegate while it was displayed.
The next thing it does is to retrieve the target Bean UUID from the settings. If it is nil then a message is displayed indicating that a Bean needs to be selected from settings.
Then, it checks to see if the new target bean is different to the current target bean. If it isn’t then we are done.\
If it is different then the method checks to see if there is a current connection – if so, this is disconnected, otherwise we attempt a connection.
You may wonder why [self connect] isn’t called when the current Bean is disconnected – We will see later that the disconnection delegate method automatically re-initiates the connection to the target Bean.
Connect
The connect method is called from processSettings –
-(void) connect { NSUUID *beanID=[[NSUUID alloc] initWithUUIDString:self.targetBean]; self.statusLabel.text=[NSString stringWithFormat:@"Connecting to %@",self.targetBeanName]; self.messageLabel.text=@""; self.temperatureLabel.text=@"-"; self.batteryLabel.text=@"-"; self.batteryProgressView.progress=0; if (![self.myBeanStuff connectToBeanWithIdentifier:beanID] ) { // Connect directly if we can [self.myBeanStuff startScanningForBeans]; // Otherwise scan for the bean } } |
Much of this method is concerned with updating the information on the view – note the use of targetBeanName in the status label but target bean UUID in the actual connection.
The call to myBeanStuffconnectToBeanWithIdentifier: will return NO if the specified Bean hasn’t been seen. In this case we instruct the BLBeanStuff object to start scanning for Beans.
didConnectToBean
Remember that BLBeanStuffDelegate protocol that was defined back in BLBeanStuff.h? didConnectToBean is one of the methods in that protocol. It is called when a Bean is successfully connected.
-(void) didConnectToBean:(PTDBean *)bean { if (![self.targetBeanNameisEqualToString:bean.name]) { [[NSUserDefaults standardUserDefaults] setObject:bean.nameforKey:kBLTargetBeanNamePref]; self.targetBeanName=bean.name; } self.statusLabel.text=[NSString stringWithFormat:@"Connected to %@",self.targetBeanName]; bean.delegate=self; self.connectedBean=bean; self.openButton.enabled=YES; [self.myBeanStuff stopScanningForBeans]; [bean readTemperature]; if ([[UIApplicationsharedApplication] applicationState] == UIApplicationStateBackground) { UILocalNotification* localNotification = [[UILocalNotificationalloc] init]; localNotification.fireDate = [NSDate new]; localNotification.alertBody = @"I see a lock"; localNotification.timeZone = [NSTimeZone defaultTimeZone]; [[UIApplicationsharedApplication] scheduleLocalNotification:localNotification]; } } |
The Bean may have had it’s ‘nice’ name changed, so once we connect, the current name of the bean is compared with the value we have stored. If it is different then the stored value is updated.
Next the status message is updated and this object is set as the delegate for the PTDBean object that we connected to.
We store the currently connected Bean, enabled the open button and instruct the PTDBeanManager (via BLBeanStuff) to stop scanning for beans.
[bean.readTemperature] instructs the Punch Through api to request an updated temperature value from the Bean.
Finally, the current execution mode is checked. If the app is executing in the background then a notification is posted to alert the user that we have connected to a lock.
didDisconnectFromBean
As you may guess, the didDisconnectFromBean: delegate method is called when a Bean disconnects.
-(void) didDisconnectFromBean:(PTDBean *)bean { self.messageLabel.text=@"Disconnected"; self.connectedBean=nil; self.openButton.enabled=NO; if (self.targetBean != nil) { [self connect]; } } |
This updates the status message, clears the currently connected Bean, disables the ‘open’ button and, if there is a target Bean set, calls the connect method to try and establish the connection to the target Bean. This call to connect is why there was no connection attempt if a current connection was detected in processSettings.
didUpdateDiscoveredBeans
This is the final BLBeanStuffDelegate method. It is called when the PTDBeanManager discovers a new Bean.
-(void) didUpdateDiscoveredBeans:(NSArray *)discoveredBeans withBean:(PTDBean *)newBean { if ([self.targetBean isEqualToString:newBean.identifier.UUIDString]) { [self connect]; } } |
If the Bean that was discovered is the Bean that we are looking for, then we attempt a connection.
Open Sesame
The unlock method sends the password from the settings to the Bean in response to the ‘Open’ button being pressed.
-(void) unlock { NSString *password=[[NSUserDefaults standardUserDefaults] objectForKey:kBLPasswordPref]; NSString *openCommand=[NSString stringWithFormat:@"\002%@\003",password]; [self.connectedBean sendSerialData:[openCommand dataUsingEncoding:NSASCIIStringEncoding]]; [self.connectedBean readTemperature]; } |
The password is retrieved from NSUserDefaults and a string is built with the <stx>password<etx> format expected by the Arduino. This is the converted to a series of ASCII bytes and sent via the virtual serial port that connects the BLE link to the Arduino.
We also request an update of the Bean’s temperature reading.
PTDBeanDelegate
The final set of methods that I want to examine are the PTDBeanDelegate methods. These methods are part of the Punch Through API. See back there in didConnectToBean where we said “bean.delegate=self;” – that was setting up the PTDBean to call these delegate methods when things happen. The things we are interested in are:
- serialDataReceived – This method is called when data arrives from the Arduino code
- didUpdateTemperature – This method is called when new temperature data is available from the Bean
- beanDidUpdateBatteryVoltage – Can you guess? That’s right, this method is called when the Bean sends new battery voltage data
serialDataReceived
In this delegate method we examine the serial data and update the view accordingly. Note how we first convert the NSData to an NSString by decoding it as ASCII bytes.
- (void)bean:(PTDBean *)bean serialDataReceived:(NSData *)data { NSString *receivedMessage=[[NSString alloc]initWithData:data encoding:NSASCIIStringEncoding]; if ([receivedMessage isEqualToString:@"OK"]) { self.messageLabel.text=@"Lock opened"; } else if ([receivedMessage isEqualToString:@"No"]) { self.messageLabel.text=@"Incorrect password!"; } else if ([receivedMessage isEqualToString:@"Error"]) { self.messageLabel.text=@"An error occurred"; } else if ([receivedMessage isEqualToString:@"Closed"]) { self.messageLabel.text=@""; } } |
didUpdateTemperature
You may have noticed at several points we called “[self.connectedBean readTemperature];”. This sends a request to the Bean for an updated temperature reading. When the Bean replies, this delegate method is called. It simply updates the label on the view –
- (void)bean:(PTDBean *)bean didUpdateTemperature:(NSNumber *)degrees_celsius { self.temperatureLabel.text=[NSString stringWithFormat:@"%0.1fºC",[degrees_celsius floatValue]]; } |
beanDidUpdateBatteryVoltage
Similar to the temperature update, this method provides updated battery voltage details. Unlike the temperature, updates are not solicited. They simply arrive when the Bean see a change in the battery voltage. The implementation of this method updates the label on the screen and a progress view displays the battery voltage graphically as a percentage of 4.0V. It also sets some pretty colours based on the voltage –
- (void)beanDidUpdateBatteryVoltage:(PTDBean *)bean error:(NSError *)error { float batteryVoltage = [bean.batteryVoltage floatValue]; self.batteryLabel.text=[NSString stringWithFormat:@"%0.4fV",batteryVoltage]; UIColor *batteryColor=[UIColor redColor]; if (batteryVoltage > 2.5) { batteryColor=[UIColor greenColor]; } else if (batteryVoltage >2) { batteryColor=[UIColor orangeColor]; } self.batteryProgressView.tintColor=batteryColor; self.batteryProgressView.progress=[bean.batteryVoltage floatValue]/4.0; } |
A Trilogy…In Four Parts
When I started writing this series of posts I intended to publish three parts, however there is more involved in the iOS code than I envisaged, so in the tradition of Douglas Adams, I will publish a fourth article that will deal with the settings view controller.
Hi Paul,
Great turtorial indeed ! I really need some startup support….
Some issues:
1) Ref your comment; “In order to help with this, I have added a simple OSX application to the repository. It is functionally equivalent but can be run on any Mac without any payment to Apple”: I cant find what OSX file you advice of !?
This would be greatly needed or the overall efforts is for nothing.
2) At “pod install” under the BeanLock-master folder of my choice, I enter the OSX subfolder and (in terminal) enter the pod install. It SEEM to work but give some error indication which I am not familiar with (among others, being a noob on Xcode for a start). I am used of C+ but heck this is really different it seems….
Strongly hope possible to get some support if ever possible….
PS: Also looking forward to the “chapter 4” 🙂
Hi Johan,
The OS X application is in the GITHUB repository under the “OSX” folder – it is an Xcode project that you need to compile, not an executable.
Add info: For the bullet 2), I get this message:
Macmini:OSX JDG$ pod install
Analyzing dependencies
/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/pathname.rb:422:in `open’: No such file or directory – /Users/JDG/.cocoapods/repos (Errno::ENOENT)
from /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/pathname.rb:422:in `foreach’
from /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/pathname.rb:422:in `children’
from /Library/Ruby/Gems/2.0.0/gems/cocoapods-0.35.0/lib/cocoapods/sources_manager.rb:63:in `all’
from /Library/Ruby/Gems/2.0.0/gems/cocoapods-0.35.0/lib/cocoapods/user_interface/error_report.rb:130:in `repo_information’
from /Library/Ruby/Gems/2.0.0/gems/cocoapods-0.35.0/lib/cocoapods/user_interface/error_report.rb:34:in `report’
from /Library/Ruby/Gems/2.0.0/gems/cocoapods-0.35.0/lib/cocoapods/command.rb:58:in `report_error’
from /Library/Ruby/Gems/2.0.0/gems/claide-0.7.0/lib/claide/command.rb:300:in `handle_exception’
from /Library/Ruby/Gems/2.0.0/gems/claide-0.7.0/lib/claide/command.rb:274:in `rescue in run’
from /Library/Ruby/Gems/2.0.0/gems/claide-0.7.0/lib/claide/command.rb:264:in `run’
from /Library/Ruby/Gems/2.0.0/gems/cocoapods-0.35.0/lib/cocoapods/command.rb:45:in `run’
from /Library/Ruby/Gems/2.0.0/gems/cocoapods-0.35.0/bin/pod:43:in `’
from /usr/bin/pod:23:in `load’
from /usr/bin/pod:23:in `’
Macmini:OSX JDG$
Does this mean I miss some Ruby part ? This should be standard bundle in OSX 10.10.1, right?
BR/Johan
Hi – This seems to describe your problem – http://stackoverflow.com/questions/26990057/cocoapods-commands-fail-due-to-no-such-file-or-directory-dir-initialize-us Try running `pod setup` before the `pod install`
Hi,
That did it !
Thanks for the quick help !!
Now I “only” need get my head into Xcode…
(And do hope for your chapter 4!)
Hi,
Good information… 🙂
i’m vary interested for your tutorial.
waiting for fourth Tutorial…
Hi Paul! I really appreciate the work that you have been done. Thanks.
I wonder when are you going to publish the fourth part of your three parts articles.