your very own raspberry pi and iphone butler

Sun, May 4, 2014

This post is about creating your very own digital butler. You’re iButler if you like. Who will welcome you into your room with a butlerish pleasantry and wave you goodbye as you leave. Let me introduce you to Bluetooth the Butler and his sidekick, piBeacon.

What I’ll be doing is getting a Raspberry Pi to act as an iBeacon (piBeacon), broadcasting some data and then getting an iPhone to pick it up. If the iPhone detects you entering the location it will play a welcome message. When you leave it will play a farewell, see you soon message.

So what is an iBeacon? Well, it’s a fancy term for a wee shouty thing. Expensive shouty things have other capabilities onboard such as temperature sensors and the like but for Bluetooth the Butler, I’ll be using the simplest possible wee shouty thing.

So what does wee shouty thing actually shout? It shouts three numbers over and over and over…

The first number it shouts is a Universally Unique Identifier (UUID). The second number is a Major Version and the third number is a Minor Version. So what a BLE detector will see is:

D0C2AA60-D10F-11E3-9C1A-0800200C9A66 1 5

The first number gubbins from D0 to 66 is the UUID. 1 is the Major Version and 5 is the Minor Version. These are all configurable when you install the dongle and you can change them at any time. You can create your own UUID online here.

It shouts these over Bluetooth Low Energy (BLE). So for the first part, a BLE dongle is required. I used an iogear GBU521W6, which I got off ebay for about 15 quid. Compared to a commercial iBeacon, that’s cheap.

The theory goes along the lines of the UUID being unique to a manufacturer, or an organisation or institution for example. For this exercise, D0C2AA60-D10F-11E3-9C1A-0800200C9A66 is my codeBrane iButler Company identifier (CIB). I’ll build some more piBeacons and scatter them round the house and my iButler will greet me as I roam around with my steaming mug of coffee, wondering where the cat has gone.

That’s where the Major/Minor stuff comes in. I can set the Major Version to, say, 1 for all ground floor piBeacons and the Minor Version to any number I care, to denote the room the piBeacon is in. For this setup, I’ve set the Major to 1 and the Minor to 5.

So, let’s set the BLE dongle up to shout these numbers.

It’s just a case of turning off the pi, plugging the dongle into the USB port and turning the pi back on. Then login and see if you can see the dongle. Ah. Yes. OK, let’s install the software first!

Have a look at the bluez download page and pick the latest version. I chose bluez-5.18. Download it:

wget www.kernel.org/pub/linux/bluetooth/bluez-5.18.tar.gz

unpack:

tar xvf bluez-5.18.tar.gz

and build and install it:


sudo apt-get install libglib2.0-dev
sudo apt-get install libdbus-1-dev
sudo apt-get install libical-dev
sudo apt-get install libreadline-dev
./configure --prefix=/home/pi/apps/bluez-5.18 \
            --disable-systemd
make
sudo make install

I like to keep my applications in:

/home/pi/apps

Then add /home/pi/apps/bluez-5.18/bin to your PATH in /home/pi/.bashrc

Now, on the pi, type:

hciconfig

and you’ll see something along the lines of:


hci0:   Type: BR/EDR  Bus: USB
    BD Address: 00:02:72:CD:64:39  ACL MTU: 1021:8  SCO MTU: 64:1
    DOWN
    RX bytes:541 acl:0 sco:0 events:26 errors:0
    TX bytes:384 acl:0 sco:0 commands:27 errors:0

the dongle is down so bring it up:

sudo /home/pi/apps/bluez/bin/hciconfig hci0 up

Now, intuitively (maybe), you’d think you’d need to tell the dongle what to advertise and then enable advertising but this doesn’t always work. It seems to be more reliable if you tell it to advertise first, then tell it what to advertise. So, do that, enable advertising:

sudo /home/pi/apps/bluez/bin/hciconfig hci0 leadv 3

then tell it what to advertise (the shouty stuff):

sudo /home/pi/apps/bluez/bin/hcitool -i hci0 cmd 0x08 0x0008 1e 02 01 1a 1a ff 4c 00 02 15 d0 c2 aa 60 d1 0f 11 e3 9c 1a 08 00 20 0c 9a 66 00 01 00 05 c5 00 00 00 00 00 00 00 00 00 00 00 00 00

The above command is quite full of what seems to be gibberish but it’s actually butlerish. It’s how you talk to your iButler.

From 0x0008 up to 02 15 is an ‘excuse me Peeves, I’d like to have a word in your shell like’. At this point Peeves the iButler is listening.

‘You rang sir?’.

‘Yes, Peeves, I rang. Here is my UUID’:

d0 c2 aa 60 d1 0f 11 e3 9c 1a 08 00 20 0c 9a 66

‘my Major Version’:

00 01

‘and my Minor Version’:

00 05

‘run along now like a good chap and and start shouting.’:

C5 … 00

Peeves the iButler is now configured with his shouty stuff.

To make sure it’s working, see if it’s up now:

hciconfig


hci0:   Type: BR/EDR  Bus: USB
    BD Address: 00:02:72:CD:64:39  ACL MTU: 1021:8  SCO MTU: 64:1
    UP RUNNING
    RX bytes:1094 acl:0 sco:0 events:54 errors:0
    TX bytes:815 acl:0 sco:0 commands:55 errors:0

Voila! iButler is now up and running on your pi, which is incidentally a piBeacon now!

Your piBeacon is shouting out your UUID, Major Version and Minor Version, constantly, over and over. Shouting, shouting, shouting. We need something to listen now.

I won’t go into creating the app from scratch. Just create a single view application and copy/paste in the good bits from here. For the sound files to play, I just recorded them on the phone using the voice memo app and emailed them to myself. So I ended up with Welcome.m4a and Bye.m4a. Just drag/drop these into your XCode file window and make sure it copies them in. When you build the app they’ll be in the bundle.

Add the AVFoundation, AudioToolbox, CoreBluetooth and CoreLocation Frameworks to your project and the following code is all you need to create your iButler:


#import <CoreLocation/CoreLocation.h>
#import <CoreBluetooth/CoreBluetooth.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
#import "ViewController.h"

@interface ViewController () {
    NSUUID *beaconUUID;
    NSString *beaconIdent;
    AVAudioPlayer *welcomeAudioPlayer;
    AVAudioPlayer *byeAudioPlayer;
    NSString *welcomePath;
    NSString *byePath;
    BOOL welcomed;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    welcomed = NO;
    
    if (![CLLocationManager isMonitoringAvailableForClass:[CLBeaconRegion class]]) {
        // oh dear, get me out of here!
    }
    else {
        welcomePath = [[NSBundle mainBundle] pathForResource:@"Welcome" ofType:@"m4a"];
        byePath = [[NSBundle mainBundle] pathForResource:@"Bye" ofType:@"m4a"];
        welcomeAudioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:welcomePath] error:NULL];
        byeAudioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:byePath] error:NULL];
        
        beaconUUID = [[NSUUID alloc] initWithUUIDString:@"D0C2AA60-D10F-11E3-9C1A-0800200C9A66"];
        beaconIdent = @"com.codebrane.rpi";
        self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:beaconUUID identifier:beaconIdent];
        
        self.locationManager = [[CLLocationManager alloc]init];
        self.locationManager.delegate = self;
        
        [self.locationManager startMonitoringForRegion:self.beaconRegion];
        [self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
    }
}

- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
    CLBeacon *beacon = [[CLBeacon alloc] init];
    beacon = [beacons lastObject];
    
    self.statusLabel.text = @"Found Beacon!";
    self.uuidLabel.text = beacon.proximityUUID.UUIDString;
    
    self.majorLabel.text = [beacon.major stringValue];
    self.minorLabel.text = [beacon.minor stringValue];
    
    self.accuracyLabel.text = [NSString stringWithFormat:@"%f", beacon.accuracy];
    
    if (beacon.proximity == CLProximityUnknown) {
        self.proximityLabel.text = @"Unknown Proximity";
    }
    else if (beacon.proximity == CLProximityImmediate) {
        self.proximityLabel.text = @"Immediate";
    }
    else if (beacon.proximity == CLProximityNear) {
        self.proximityLabel.text = @"Near";
        if (!welcomed) {
            [welcomeAudioPlayer play];
            welcomed = YES;
        }
    }
    else if (beacon.proximity == CLProximityFar) {
        self.proximityLabel.text = @"Far";
        if (welcomed) {
            [byeAudioPlayer play];
            welcomed = NO;
        }
    }
    
    self.rssiLabel.text = [NSString stringWithFormat:@"%li", (long)beacon.rssi];
}

- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
    [self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
}

- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
    [self.locationManager stopRangingBeaconsInRegion:self.beaconRegion];
    self.statusLabel.text = @"OUT!";
}


We first import all the gubbins then define some private members of this particular gentleman’s club. In the viewDidLoad method things get interesting. We first set up a welcomed check, otherwise Peeves will just repeat himself over and over when we enter the room. We don’t want Peeves the parrot!

Next up is a check for isMonitoringAvailableForClass. If not, time for a sharp exit as the device has no BLE capabilities. iPhone 4S and above will work.

We then load up the audio resources, ready for playing.

Peeve’s UUID is represented here by beaconUUID. beaconIdent is internal and has nothing to do with the piBeacon, or BLE in general. It’s just for iOS app use.

We then define a bluetooth region to monitor, based on our UUID. When the phone detects this region it will report on all beacons it finds in there. If you want to only look for beacons with a Major Version of ‘1’ then add that, or add a Minor Version too if you want to narrow the search down even more.

We then tell the location manger to fire up and send messages to this class. startMonitoringForRegion will look for regions in the vicinity.

Now, this is a bit of a gotcha. If you’re not in a region when you start the app, all fine and good. You’ll walk into a region and the didEnterRegion method will fire. It’s here we then start ranging for beacons with a call to startRangingBeaconsInRegion. But if you’re already in the region when you start the app, didEnterRegion may not fire so you’ll never find any beacons. So to cover that, we call startRangingBeaconsInRegion in viewDidLoad. When the app detects beacons, the didRangeBeacons method will fire, with a list of beacons it’s found.

A beacon is in three main states as far as your app is concerned. Far, Near and Immediate. Far seems to be around 3-4m. Near about 0.5-3m and Immediate seems to be a few cm from the beacon.

In didRangeBeacons, Far lined up nicely with the room door, with the piBeacon next to the window, about 3 and a bit metres from the door, so [welcomeAudioPlayer play] plays the welcome greeting. This coincided from crossing from Far to Near. Inside the room is classed as Near, according to the piBeacon distance. When I walk back out, the range increases to Far and [byeAudioPlayer play] does its job of waving me goodbye.

One Bluetooth Butler and piBeacon, done!

References: