We’ve just finished the first iOSCon hackathon. Some great work was done with the Affectiva emotion SDK along with some great stuff with Estimotes and Scentees.
I played with my Raspberry Pi and turned it into a fully functional bluetooth device communicating with an app running on my iPhone. I also plugged in the Scentee SDK and got the phone making a smell when the Pi picked up key presses on the remote control. I really wanted the Pi to make the smells, but the volume from the Pi’s audio jack wasn’t loud enough to activate the device.
To get my raspberry pi communicating I installed node and then used Bleno.
Bleno is a really nice node.js module for implementing Bluetooth low energy peripherals. For example to get an iBeacon running you just need the following code:
var bleno = require('bleno');
var uuid = 'e2c56db5dffb48d2b060d0f5a71096e0';
var major = 0; // 0x0000 - 0xffff
var minor = 0; // 0x0000 - 0xffff
var measuredPower = -59; // -128 - 127
bleno.startAdvertisingIBeacon(uuid, major, minor, measuredPower);
Assuming you’ve saved this file to ibeacon.js you can run it using:
$ sudo node ibeacon.js
To create a more fully functional device than a simple iBeacon we need to do a bit more work. I wanted to fully exercise my Raspberry Pi, using all the sensors that I’d wired up so far, connected up with my iPhone. This meant that I’d need to be able to write strings to my LCD display, get a stream of temperature readings and also get notified when an IR remote control button was pushed.
The basic outline of a bluetooth device looks like this using bleno:
var bleno = require('bleno');
bleno.on('stateChange', function(state) {
console.log('on -> stateChange: ' + state);
if (state === 'poweredOn') {
bleno.startAdvertising('MyDevice',['b1a6752152eb4d36e13e357d7c225465']);
} else {
bleno.stopAdvertising();
}
});
bleno.on('advertisingStart', function(error) {
console.log('on -> advertisingStart: ' + (error ? 'error ' + error : 'success'));
if (!error) {
bleno.setServices([
new bleno.PrimaryService({
uuid : 'b1a6752152eb4d36e13e357d7c225465',
characteristics : [
// add characteristics here
]
})
]);
}
});
We can now add some characteristics to our service.
For my lcd characteristic I created this:
var data = new Buffer('Send me some data to display');
// new characteristic added to the service
new bleno.Characteristic({
uuid : '9e739ec2b3a24af0c4dc14f059a8a62d',
properties : ['read','writeWithoutResponse'],
onReadRequest : function(offset, callback) {
if(offset > data.length) {
callback(bleno.Characteristic.RESULT_INVALID_OFFSET);
} else {
callback(bleno.Characteristic.RESULT_SUCCESS, data.slice(offset));
}
},
onWriteRequest : function(newData, offset, withoutResponse, callback) {
if(offset > 0) {
callback(bleno.Characteristic.RESULT_INVALID_OFFSET);
} else {
exec('sudo ./lcd "'+newData.toString('utf8')+'"');
data = newData;
callback(bleno.Characteristic.RESULT_SUCCESS);
}
}
})
This characteristic provides a read function and a write function. The write function shells out to a simple command line program that sets the contents of the LCD. The read function lets you read back whatever it is you stored.
#include <stdio.h>
#include <wiringPi.h>
#include <mcp23s17.h>
#include <lcd.h>
// BASE port for the SPI GPIO extension
#define BASE 100
int main (int argc, char *argv[])
{
printf("Will set text to %s", argv[1]);
wiringPiSetup () ;
// initialise the SPI extension
mcp23s17Setup (BASE, 0, 0) ;
// setup the LCD
int fd = lcdInit(2, 16, 4, BASE + 10, BASE + 11, BASE + 12,
BASE + 13, BASE + 14, BASE + 15, 0, 0, 0, 0);
// print whatever is in argv[1]
lcdClear (fd);
lcdHome (fd);
lcdPrintf(fd, argv[1]);
}
For my InfraRed receiver characteristic I wanted to be able to notify a connected client that there was a new value. To do this we use the onSubscribe event coupled with the node module infrared. The infrared module gives us a nice wrapper around lirc.
var IRW = require('infrared').irw;
new bleno.Characteristic({
uuid : 'b747cdd0ddcc11e38b680800200c9a66',
properties : ['notify'],
onSubscribe : function(maxValueSize, updateValueCallback) {
irw.on('stdout', function(data) {
updateValueCallback(new Buffer(data.split(' ')[2]));
});
irw.start();
}
}),
When someone subscribes to this property we start up irw and whenever it gets an event we send the button push back to the client.
We should probably also handle the onUnsubscribe event as well and stop irw.
For the temperature sensor we can use the ds18b20 module - there are several other modules that we could use, but this is the first one I found.
new bleno.Characteristic({
uuid : '4b842c60ddd611e38b680800200c9a66',
properties : ['notify'],
onSubscribe : function(maxValueSize, updateValueCallback) {
setInterval(function() {
sense.temperature('28-0000057cc14e', function(err, value) {
updateValueCallback(new Buffer(value + 'C'));
});
}, 1000);
}
}),
When someone subscribes I kick off a repeating timer to read the temperature and send it back every second. Once again, we should really handle the unsubscribe event and kill our timer.
To run the server we do the following:
# load up the SPI driver
$ gpio load spi
#load up the temperature sensors
$ sudo modprobe w1_gpio
$ sudo modprobe w1_therm
# start up the server
$ sudo node server.js
That’s it for the Raspberry Pi side of things - if you have any other sensors or outputs you want to wire up then you can follow a similar pattern.
You can now test your device using one of the many bluetooth test applications that are on the app store or download the (source code)[https://github.com/StudioSophisti/BTLE-Tools] and run one yourself.
To build our own app we need to use the Core Bluetooth framework to access our peripheral.
// create the CBCentral manager
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
// when it is powered on start looking for our peripheral
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
if(central.state==CBCentralManagerStatePoweredOn)
{
CBUUID *serviceUUID = [CBUUID UUIDWithString:@"b1a67521-52eb-4d36-e13e-357d7c225465"];
[central scanForPeripheralsWithServices:@[serviceUUID] options:nil];
}
}
// central manager discovered a peripheral
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
self.peripheral = peripheral;
[self.centralManager stopScan];
self.peripheral.delegate = self;
// make a connection to the peripheral
[self.centralManager connectPeripheral:self.peripheral options:nil];
}
// connected, start discovering services
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
if(peripheral.state == CBPeripheralStateConnected) {
[peripheral discoverServices:nil];
}
}
// services discovered - find their characteristics
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
for (CBService *service in peripheral.services) {
[peripheral discoverCharacteristics:nil forService:service];
}
}
// discovered some characteristics
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
for (CBCharacteristic *characteristic in service.characteristics) {
if([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"9e739ec2-b3a2-4af0-c4dc-14f059a8a62d"]]) {
self.lcdCharacteristic = characteristic;
}
if([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"b747cdd0-ddcc-11e3-8b68-0800200c9a66"]]) {
self.irCharacteristic = characteristic;
}
if([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"4b842c60-ddd6-11e3-8b68-0800200c9a66"]]) {
self.tempCharacteristic = characteristic;
}
[peripheral readValueForCharacteristic:characteristic];
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
}
}
// this is called in response to the readValueForCharacteristic and also when the peripheral notifies us of changes
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
// get the value
NSString *value = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
// see which characteristic was updated
if(characteristic == self.lcdCharacteristic) {
// LCD display
}
if(characteristic == self.irCharacteristic) {
// InfraRed remote
}
if(characteristic == self.tempCharacteristic) {
// Temperature
}
}
The first step is to create the core bluetooth central manager. We have to wait for it to power on and then we can start scanning for peripherals.
Once we’ve discovered the peripheral we can connect to it and discover the services it offers and once we find the services we can ask the service what characteristics it supports.
When we get the set of characteristics for the service we can read the current value and we can also subscribe to changes in the value.
When we read values or are notified of values we get a call to didUpdateValueForCharacteristic
.
To write values to our new LCD characteristic we call:
[self.peripheral writeValue:[text dataUsingEncoding:NSUTF8StringEncoding]
forCharacteristic:self.lcdCharacteristic
type:CBCharacteristicWriteWithoutResponse];
That’s it! Bleno provides a really nice environment for wiring up bluetooth services and the Core Bluetooth frameworks makes it very easy to connect up to your device.