HeartRateMonitor/HeartRateMonitorAppDelegate.m
/*  | 
Copyright (C) 2018 Apple Inc. All Rights Reserved.  | 
See LICENSE.txt for this sample’s licensing information  | 
Abstract:  | 
Implementatin of Heart Rate Monitor app using Bluetooth Low Energy (LE) Heart Rate Service. This app demonstrats the use of CoreBluetooth APIs for LE devices.  | 
*/  | 
#import "HeartRateMonitorAppDelegate.h"  | 
#import <QuartzCore/QuartzCore.h>  | 
@implementation HeartRateMonitorAppDelegate  | 
@synthesize window;  | 
@synthesize heartRate;  | 
@synthesize heartView;  | 
@synthesize pulseTimer;  | 
@synthesize scanSheet;  | 
@synthesize heartRateMonitors;  | 
@synthesize arrayController;  | 
@synthesize manufacturer;  | 
@synthesize connected;  | 
#define PULSESCALE 1.2  | 
#define PULSEDURATION 0.2  | 
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification  | 
{ | 
self.heartRate = 0;  | 
/* autoConnect = TRUE; */ /* uncomment this line if you want to automatically connect to previosly known peripheral */  | 
self.heartRateMonitors = [NSMutableArray array];  | 
[NSAnimationContext beginGrouping];  | 
[[NSAnimationContext currentContext] setDuration:0.];  | 
[self.heartView layer].position = CGPointMake( [[self.heartView layer] frame].size.width / 2, [[self.heartView layer] frame].size.height / 2 );  | 
[self.heartView layer].anchorPoint = CGPointMake(0.5, 0.5);  | 
[NSAnimationContext endGrouping];  | 
manager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];  | 
if( autoConnect )  | 
    { | 
[self startScan];  | 
}  | 
}  | 
- (void) dealloc  | 
{ | 
[self stopScan];  | 
[peripheral setDelegate:nil];  | 
[peripheral release];  | 
[heartRateMonitors release];  | 
[manager release];  | 
[super dealloc];  | 
}  | 
/*  | 
Disconnect peripheral when application terminate  | 
*/  | 
- (void) applicationWillTerminate:(NSNotification *)notification  | 
{ | 
if(peripheral)  | 
    { | 
[manager cancelPeripheralConnection:peripheral];  | 
}  | 
}  | 
#pragma mark - Scan sheet methods  | 
/*  | 
Open scan sheet to discover heart rate peripherals if it is LE capable hardware  | 
*/  | 
- (IBAction)openScanSheet:(id)sender  | 
{ | 
if( [self isLECapableHardware] )  | 
    { | 
autoConnect = FALSE;  | 
[arrayController removeObjects:heartRateMonitors];  | 
        [window beginSheet:self.scanSheet completionHandler:^(NSModalResponse returnCode) { | 
[self sheetDidEnd:self.scanSheet returnCode:returnCode contextInfo:nil];  | 
} ];  | 
[self startScan];  | 
}  | 
}  | 
/*  | 
Close scan sheet once device is selected  | 
*/  | 
- (IBAction)closeScanSheet:(id)sender  | 
{ | 
[window endSheet:self.scanSheet returnCode:NSAlertFirstButtonReturn];  | 
}  | 
/*  | 
Close scan sheet without choosing any device  | 
*/  | 
- (IBAction)cancelScanSheet:(id)sender  | 
{ | 
[window endSheet:self.scanSheet returnCode:NSAlertSecondButtonReturn];  | 
}  | 
/*  | 
This method is called when Scan sheet is closed. Initiate connection to selected heart rate peripheral  | 
*/  | 
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo  | 
{ | 
[self stopScan];  | 
if( returnCode == NSAlertFirstButtonReturn )  | 
    { | 
NSIndexSet *indexes = [self.arrayController selectionIndexes];  | 
if ([indexes count] != 0)  | 
        { | 
NSUInteger anIndex = [indexes firstIndex];  | 
peripheral = [self.heartRateMonitors objectAtIndex:anIndex];  | 
[peripheral retain];  | 
[indicatorButton setHidden:FALSE];  | 
[progressIndicator setHidden:FALSE];  | 
[progressIndicator startAnimation:self];  | 
[connectButton setTitle:@"Cancel"];  | 
[manager connectPeripheral:peripheral options:nil];  | 
}  | 
}  | 
}  | 
#pragma mark - Connect Button  | 
/*  | 
This method is called when connect button pressed and it takes appropriate actions depending on device connection state  | 
*/  | 
- (IBAction)connectButtonPressed:(id)sender  | 
{ | 
if(peripheral && (peripheral.state == CBPeripheralStateConnected))  | 
    {  | 
/* Disconnect if it's already connected */  | 
[manager cancelPeripheralConnection:peripheral];  | 
}  | 
else if (peripheral)  | 
    { | 
/* Device is not connected, cancel pendig connection */  | 
[indicatorButton setHidden:TRUE];  | 
[progressIndicator setHidden:TRUE];  | 
[progressIndicator stopAnimation:self];  | 
[connectButton setTitle:@"Connect"];  | 
[manager cancelPeripheralConnection:peripheral];  | 
[self openScanSheet:nil];  | 
}  | 
else  | 
    {   /* No outstanding connection, open scan sheet */ | 
[self openScanSheet:nil];  | 
}  | 
}  | 
#pragma mark - Heart Rate Data  | 
/*  | 
Update UI with heart rate data received from device  | 
*/  | 
- (void) updateWithHRMData:(NSData *)data  | 
{ | 
const uint8_t *reportData = [data bytes];  | 
uint16_t bpm = 0;  | 
if ((reportData[0] & 0x01) == 0)  | 
    { | 
/* uint8 bpm */  | 
bpm = reportData[1];  | 
}  | 
else  | 
    { | 
/* uint16 bpm */  | 
bpm = CFSwapInt16LittleToHost(*(uint16_t *)(&reportData[1]));  | 
}  | 
uint16_t oldBpm = self.heartRate;  | 
self.heartRate = bpm;  | 
if (oldBpm == 0 && bpm != 0)  | 
    { | 
[self pulse];  | 
self.pulseTimer = [NSTimer scheduledTimerWithTimeInterval:(60. / heartRate) target:self selector:@selector(pulse) userInfo:nil repeats:NO];  | 
}  | 
}  | 
/*  | 
Update pulse UI  | 
*/  | 
- (void) pulse  | 
{ | 
CABasicAnimation *pulseAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];  | 
pulseAnimation.toValue = [NSNumber numberWithFloat:PULSESCALE];  | 
pulseAnimation.fromValue = [NSNumber numberWithFloat:1.0];  | 
pulseAnimation.duration = PULSEDURATION;  | 
pulseAnimation.repeatCount = 1;  | 
pulseAnimation.autoreverses = YES;  | 
pulseAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];  | 
[[heartView layer] addAnimation:pulseAnimation forKey:@"scale"];  | 
if (heartRate == 0)  | 
    { | 
[self.pulseTimer invalidate];  | 
self.pulseTimer = nil;  | 
}  | 
else  | 
    { | 
self.pulseTimer = [NSTimer scheduledTimerWithTimeInterval:(60. / heartRate) target:self selector:@selector(pulse) userInfo:nil repeats:NO];  | 
}  | 
}  | 
#pragma mark - Start/Stop Scan methods  | 
/*  | 
Uses CBCentralManager to check whether the current platform/hardware supports Bluetooth LE. An alert is raised if Bluetooth LE is not enabled or is not supported.  | 
*/  | 
- (BOOL) isLECapableHardware  | 
{ | 
NSString * state = nil;  | 
switch ([manager state])  | 
    { | 
case CBManagerStateUnsupported:  | 
state = @"The platform/hardware doesn't support Bluetooth Low Energy.";  | 
break;  | 
case CBManagerStateUnauthorized:  | 
state = @"The app is not authorized to use Bluetooth Low Energy.";  | 
break;  | 
case CBManagerStatePoweredOff:  | 
state = @"Bluetooth is currently powered off.";  | 
break;  | 
case CBManagerStatePoweredOn:  | 
return TRUE;  | 
case CBManagerStateUnknown:  | 
default:  | 
return FALSE;  | 
}  | 
NSLog(@"Central manager state: %@", state);  | 
[self cancelScanSheet:nil];  | 
NSAlert *alert = [[[NSAlert alloc] init] autorelease];  | 
[alert setMessageText:state];  | 
[alert addButtonWithTitle:@"OK"];  | 
[alert setIcon:[[[NSImage alloc] initWithContentsOfFile:@"AppIcon"] autorelease]];  | 
    [alert beginSheetModalForWindow:[self window] completionHandler:^(NSModalResponse returnCode) { | 
return;  | 
}];  | 
return FALSE;  | 
}  | 
/*  | 
Request CBCentralManager to scan for heart rate peripherals using service UUID 0x180D  | 
*/  | 
- (void) startScan  | 
{ | 
[manager scanForPeripheralsWithServices:[NSArray arrayWithObject:[CBUUID UUIDWithString:@"180D"]] options:nil];  | 
}  | 
/*  | 
Request CBCentralManager to stop scanning for heart rate peripherals  | 
*/  | 
- (void) stopScan  | 
{ | 
[manager stopScan];  | 
}  | 
#pragma mark - CBCentralManager delegate methods  | 
/*  | 
Invoked whenever the central manager's state is updated.  | 
*/  | 
- (void) centralManagerDidUpdateState:(CBCentralManager *)central  | 
{ | 
[self isLECapableHardware];  | 
}  | 
/*  | 
Invoked when the central discovers heart rate peripheral while scanning.  | 
*/  | 
- (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)aPeripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI  | 
{     | 
NSMutableArray *peripherals = [self mutableArrayValueForKey:@"heartRateMonitors"];  | 
if( ![self.heartRateMonitors containsObject:aPeripheral] )  | 
[peripherals addObject:aPeripheral];  | 
/* Retreive already known devices */  | 
if(autoConnect)  | 
    { | 
[manager retrievePeripheralsWithIdentifiers:[NSArray arrayWithObject:(id)aPeripheral.identifier]];  | 
}  | 
}  | 
/*  | 
Invoked when the central manager retrieves the list of known peripherals.  | 
Automatically connect to first known peripheral  | 
*/  | 
- (void)centralManager:(CBCentralManager *)central didRetrievePeripherals:(NSArray *)peripherals  | 
{ | 
NSLog(@"Retrieved peripheral: %lu - %@", [peripherals count], peripherals);  | 
[self stopScan];  | 
/* If there are any known devices, automatically connect to it.*/  | 
if([peripherals count] >=1)  | 
    { | 
[indicatorButton setHidden:FALSE];  | 
[progressIndicator setHidden:FALSE];  | 
[progressIndicator startAnimation:self];  | 
peripheral = [peripherals objectAtIndex:0];  | 
[peripheral retain];  | 
[connectButton setTitle:@"Cancel"];  | 
[manager connectPeripheral:peripheral options:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:CBConnectPeripheralOptionNotifyOnDisconnectionKey]];  | 
}  | 
}  | 
/*  | 
Invoked whenever a connection is succesfully created with the peripheral.  | 
Discover available services on the peripheral  | 
*/  | 
- (void) centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral  | 
{     | 
[aPeripheral setDelegate:self];  | 
[aPeripheral discoverServices:nil];  | 
self.connected = @"Connected";  | 
[connectButton setTitle:@"Disconnect"];  | 
[indicatorButton setHidden:TRUE];  | 
[progressIndicator setHidden:TRUE];  | 
[progressIndicator stopAnimation:self];  | 
}  | 
/*  | 
Invoked whenever an existing connection with the peripheral is torn down.  | 
Reset local variables  | 
*/  | 
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)aPeripheral error:(NSError *)error  | 
{ | 
self.connected = @"Not connected";  | 
[connectButton setTitle:@"Connect"];  | 
self.manufacturer = @"";  | 
self.heartRate = 0;  | 
if( peripheral )  | 
    { | 
[peripheral setDelegate:nil];  | 
[peripheral release];  | 
peripheral = nil;  | 
}  | 
}  | 
/*  | 
Invoked whenever the central manager fails to create a connection with the peripheral.  | 
*/  | 
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)aPeripheral error:(NSError *)error  | 
{ | 
NSLog(@"Fail to connect to peripheral: %@ with error = %@", aPeripheral, [error localizedDescription]);  | 
[connectButton setTitle:@"Connect"];  | 
if( peripheral )  | 
    { | 
[peripheral setDelegate:nil];  | 
[peripheral release];  | 
peripheral = nil;  | 
}  | 
}  | 
#pragma mark - CBPeripheral delegate methods  | 
/*  | 
Invoked upon completion of a -[discoverServices:] request.  | 
Discover available characteristics on interested services  | 
*/  | 
- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error  | 
{ | 
for (CBService *aService in aPeripheral.services)  | 
    { | 
NSLog(@"Service found with UUID: %@", aService.UUID);  | 
/* Heart Rate Service */  | 
if ([aService.UUID isEqual:[CBUUID UUIDWithString:@"180D"]])  | 
        { | 
[aPeripheral discoverCharacteristics:nil forService:aService];  | 
}  | 
/* Device Information Service */  | 
if ([aService.UUID isEqual:[CBUUID UUIDWithString:@"180A"]])  | 
        { | 
[aPeripheral discoverCharacteristics:nil forService:aService];  | 
}  | 
/* GAP (Generic Access Profile) for Device Name */  | 
if ( [aService.UUID isEqual:[CBUUID UUIDWithString:@"1800"]] )  | 
        { | 
[aPeripheral discoverCharacteristics:nil forService:aService];  | 
}  | 
}  | 
}  | 
/*  | 
Invoked upon completion of a -[discoverCharacteristics:forService:] request.  | 
Perform appropriate operations on interested characteristics  | 
*/  | 
- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error  | 
{     | 
if ([service.UUID isEqual:[CBUUID UUIDWithString:@"180D"]])  | 
    { | 
for (CBCharacteristic *aChar in service.characteristics)  | 
        { | 
/* Set notification on heart rate measurement */  | 
if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@"2A37"]])  | 
            { | 
[peripheral setNotifyValue:YES forCharacteristic:aChar];  | 
NSLog(@"Found a Heart Rate Measurement Characteristic");  | 
}  | 
/* Read body sensor location */  | 
if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@"2A38"]])  | 
            { | 
[aPeripheral readValueForCharacteristic:aChar];  | 
NSLog(@"Found a Body Sensor Location Characteristic");  | 
}  | 
/* Write heart rate control point */  | 
if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@"2A39"]])  | 
            { | 
uint8_t val = 1;  | 
NSData* valData = [NSData dataWithBytes:(void*)&val length:sizeof(val)];  | 
[aPeripheral writeValue:valData forCharacteristic:aChar type:CBCharacteristicWriteWithResponse];  | 
}  | 
}  | 
}  | 
if ( [service.UUID isEqual:[CBUUID UUIDWithString:@"1800"]] )  | 
    { | 
for (CBCharacteristic *aChar in service.characteristics)  | 
        { | 
/* Read device name */  | 
if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@"2A00"]])  | 
            { | 
[aPeripheral readValueForCharacteristic:aChar];  | 
NSLog(@"Found a Device Name Characteristic");  | 
}  | 
}  | 
}  | 
if ([service.UUID isEqual:[CBUUID UUIDWithString:@"180A"]])  | 
    { | 
for (CBCharacteristic *aChar in service.characteristics)  | 
        { | 
/* Read manufacturer name */  | 
if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@"2A29"]])  | 
            { | 
[aPeripheral readValueForCharacteristic:aChar];  | 
NSLog(@"Found a Device Manufacturer Name Characteristic");  | 
}  | 
}  | 
}  | 
}  | 
/*  | 
Invoked upon completion of a -[readValueForCharacteristic:] request or on the reception of a notification/indication.  | 
*/  | 
- (void) peripheral:(CBPeripheral *)aPeripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error  | 
{ | 
/* Updated value for heart rate measurement received */  | 
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"2A37"]])  | 
    { | 
if( (characteristic.value) || !error )  | 
        { | 
/* Update UI with heart rate data */  | 
[self updateWithHRMData:characteristic.value];  | 
}  | 
}  | 
/* Value for body sensor location received */  | 
else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"2A38"]])  | 
    { | 
NSData * updatedValue = characteristic.value;  | 
uint8_t* dataPointer = (uint8_t*)[updatedValue bytes];  | 
if(dataPointer)  | 
        { | 
uint8_t location = dataPointer[0];  | 
NSString* locationString;  | 
switch (location)  | 
            { | 
case 0:  | 
locationString = @"Other";  | 
break;  | 
case 1:  | 
locationString = @"Chest";  | 
break;  | 
case 2:  | 
locationString = @"Wrist";  | 
break;  | 
case 3:  | 
locationString = @"Finger";  | 
break;  | 
case 4:  | 
locationString = @"Hand";  | 
break;  | 
case 5:  | 
locationString = @"Ear Lobe";  | 
break;  | 
case 6:  | 
locationString = @"Foot";  | 
break;  | 
default:  | 
locationString = @"Reserved";  | 
break;  | 
}  | 
NSLog(@"Body Sensor Location = %@ (%d)", locationString, location);  | 
}  | 
}  | 
/* Value for device Name received */  | 
else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"2A00"]])  | 
    { | 
NSString * deviceName = [[[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding] autorelease];  | 
NSLog(@"Device Name = %@", deviceName);  | 
}  | 
/* Value for manufacturer name received */  | 
else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"2A29"]])  | 
    { | 
self.manufacturer = [[[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding] autorelease];  | 
NSLog(@"Manufacturer Name = %@", self.manufacturer);  | 
}  | 
}  | 
@end  | 
Copyright © 2018 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2018-03-08