Skip to content
Guido Marucci Blas edited this page Dec 22, 2014 · 2 revisions

As you probably already know Bluetooth 4.0 devices expose services. Those services are basically a group of characteristics and they are grouped together because they share some common logic. Most probably you will endup organizing your Bluetooth application code that handles this logic in services and those services would probably have a one-to-one relation with the Bluetooth services. This is what we got in mind when we were designing the services API.

Once a connection has been established with a device you would need to discover all its services. This is when you need the WLXServicesManager. The services manager can be obtained from a WLXConnectionManager through the servicesManager property. This property will return nil if the connection was not established. After you get a reference to the services manager, it does not matter if the connections gets lost; the services manager is bound to the CBPeripheral object. Once the connection is reestablished you can still use the same services manager (same for all its child objects).

With the WLXServicesManager instance you can discover all the device's services by calling discoverServicesUsingBlock:. The block you pass to this method will get called with an NSError in case there is a problem discovering the services or nil if the discovery process succeed.

For each discovered service an instance of WLXServiceManager is created. This class is the one that is actually responsable of the interaction with the Bluetooth service and its characteristics. In order to get an instance of WLXServiceManager you need the service's UUID and call WLXServicesManager's managerForService: method.

The following is an example of how to get a WLXServiceManager instance for a particular service:

CBUUID * serviceUUID = [CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00066"];
WLXServicesManager * servicesManager = connectionManager.servicesManager;
[servicesManager discoverServicesUsingBlock:^(NSError * error) {
  if (error) {
    NSLog(@"There was an error discovering the services");
  } else {
    WLXServiceManager * serviceManager = [servicesManager managerForService:serviceUUID];
  }
}];

Characteristics

A characteristic is the "atomic" value in the Bluetooth 4.0 world. When you want to read o write a value from a Bluetooth device, you are reading or writing a characteristic.

The WLXServiceManager exposes an API to read or write a service's characteristic. You can also subscribe for notification for those characteristic which provide notifications.

Using WLXServiceManager to interact with the characteristic allows you to not having to worry about discovery characteristics before you can write o read them. All you need is the characteristic UUID and library will discovery the characteristic if necessary.

Lets say you want to write to characteristic 68753A44-4D6F-1226-9C60-0050E4C00067 and you don't care to be acknowledge if the write was successful. All you need to do is call the writeValue:forCharacteristicUUID: method from WLXServiceManager and the library will automatically discover the characteristic and then perform a write. Once the characteristic is discovered, it gets cached so other operations don't have to discover it again.

Reading a characteristic's value

If you want to read a value from a characteristic you should invoke the readValueForCharacteristicUUID:usingBlock:. Suppose you have a characteristic with UUID 68753A44-4D6F-1226-9C60-0050E4C00067 that holds an NSUInteger value. The following is an example of how would you read that value:

CBUUID * characteristicUUID = [CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00067"];
[serviceManager readValueForCharacteristicUUID:characteristicUUID usingBlock:^(NSError * error, NSData * data) {
  if (error) {
    NSLog(@"There was an error reading characteristic %@: %@", characteristicUUID.UUIDString, error);
  } else {
    NSUInteger value = *((NSUInteger *)data.bytes);
    NSLog(@"Read value is %ul", (unsigned long)value);
  }
}];

Writing a characteristic's value

There are two ways of writing a characteristic's value. You can write passing a callback block or without a callback block, writeValue:forCharacteristicUUID:usingBlock: or writeValue:forCharacteristicUUID:. If you use the version with the callback block, it will translate to a write of type CBCharacteristicWriteWithResponse. If you don't provide a callback the write will be of type CBCharacteristicWriteWithoutResponse, in this case there is no way of knowing if the write succeed or not.

CBUUID * characteristicUUID = [CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00067"];
NSUInteger value = 10;
NSData * data = [NSData dataWithBytes:&value length:sizeof(value)];
[serviceManager writeValue:data forCharacteristicUUID:characteristicUUID usingBlock:^(NSError * error) {
  if (error) {
    NSLog(@"There was an error writing characteristic %@: %@", characteristicUUID.UUIDString, error);
  }
}];

Subscribing to a characteristic's value

There are some characteristics that allow notifications, meaning every time the characteristic value gets updated you will get a notification. To enable and disable notifications for a characteristic you can use the following methods enableNotificationsForCharacteristic:usingBlock: and disableNotificationsForCharacteristic:usingBlock:.

After you enable notifications for a characteristic you have to subscribe an observer for that characteristic using one of the following methods: addObserverForCharacteristic:usingBlock: or addObserverForCharacteristic:selector:target:. You can then remove observers using the removeObserver: method.

CBUUID * characteristicUUID = [CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00067"];
[serviceManager enableNotificationsForCharacteristic:characteristicUUID usingBlock:^(NSError * error) {
  if (error) {
    NSLog(@"There was an error enabling notifications for characteristic %@", characteristicUUID.UUIDString);
  } else {
    serviceManager addObserverForCharacteristic:characteristicUUID usingBlock:^(NSError * error, NSData * data) {
      if (error) {
        NSLog(@"There was an error reading characteristic %@: %@", characteristicUUID.UUIDString, error);
      } else {
        NSUInteger value = *((NSUInteger *)data.bytes);
        NSLog(@"Value updated to %lu", (unsigned long)value);
      }
    }];
  }
}];

Implementing your own services

The following is an example of how we suggest Bluetooth based services should be implemented using this library. Lets say that we a thermostat device that provides two services: a temperature service and a battery service.

The temperature service exposes a read/write characteristic that allows you to get/set the thermostat temperature and a another characteristic that allows you to set the temperature units: Fahrenheit or Celsius

The battery services exposes a read/notify characteristic that allows you to get the battery level.

The following is an example of how you could implement such services using WLXServiceManager:

TemperatureService.h

typedef enum TemperatureUnit {
  TemperatureUnitFahrenheit,
  TemperatureUnitCelsius
} TemperatureUnit;

@interface TemperatureService : NSObject

@property (nonatomic, readonly) WLXServiceManager * serviceManager;

+ (CBUUID *)serviceUUID;

- (instancetype)initWithServiceManager:(WLXServiceManager *)serviceManager;

- (void)readTemperatureUsingBlock:(void(^)(NSError *, CGFloat));

- (void)setTemperature:(CGFloat)temperature usingBlock:(void(^)(NSError *))block;

- (void)setTemperatureUnit:(TemperatureUnit)unit usingBlock:(void(^)(NSError *))block;

@end

TemperatureService.m

static NSString * serviceUUIDString = @"68753A44-4D6F-1226-9C60-0050E4C00066";
static NSString * temperatureCharacteristicUUIDString = @"68753A44-4D6F-1226-9C60-0050E4C00067";
static NSString * temperatureUnitCharacteristicUUID = @"68753A44-4D6F-1226-9C60-0050E4C00068";
static CBUUID * serviceUUID;
static CBUUID * temperatureCharacteristicUUID;
static CBUUID * temperatureUnitCharacteristicUUID;

@implementation TemperatureService

+ (void)initialize {
  serviceUUID = [CBUUID UUIDWithString:serviceUUIDString];
  temperatureCharacteristicUUID = [CBUUID UUIDWithString:temperatureCharacteristicUUIDString];
  temperatureUnitCharacteristicUUID = [CBUUID UUIDWithString:temperatureUnitCharacteristicUUIDString];
}

+ (CBUUID *)serviceUUID {
  return serviceUUID;
}

- (instancetype)initWithServiceManager:(WLXServiceManager *)serviceManager {
  self = [super init];
  if (self) {
    _serviceManager = serviceManager;
  }
  return self;
}

- (void)readTemperatureUsingBlock:(void(^)(NSError *, CGFloat))block {
  [self.serviceManager readValueForCharacteristicUUID:temperatureCharacteriscticUUID usingBlock:^(NSError * error, NSData * data) {
    if (error) {
      block(error, 0.0);
    } else {
      CGFloat value = *((CGFloat *)data.bytes);
      block(nil, value);
    }
  }];
}

- (void)setTemperature:(CGFloat)temperature usingBlock:(void(^)(NSError *))block {
  NSData * data = [NSData dataWithBytes:&temperature size:sizeof(temperature)];
  [self.serviceManager writeValue:data forCharacteristicUUID:temperatureCharacteristicUUID usingBlock:block];
}

- (void)setTemperatureUnit:(TemperatureUnit)unit usingBlock:(void(^)(NSError *))block {
  NSData * data = [NSData dataWithBytes:&unit size:sizeof(unit)];
  [self.serviceManager writeValue:data forCharacteristicUUID:temperatureUnitCharacteristicUUID usingBlock:block];
}

@end

BatteryService.h

@interface BatteryService : NSObject

@property (nonatomic, readonly) WLXServiceManager * serviceManager;
@property (nonatomic) BOOL notificationsEnabled;
@property (nonoatomic, copy) void (^notificationsErrorHandler)(NSError *);

+ (CBUUID *)serviceUUID;

- (instancetype)initWithServiceManager:(WLXServiceManager *)serviceManager;

- (void)readBatteryLevelUsingBlock:(void(^)(NSError *, NSUInteger))block;

- (id)addBatteryLevelObserver:(void(^)(NSError *, NSUInteger))observer;

- (void)removeBatteryLevelObserver:(id)observer;

@end

BatteryService.m

static NSString * batteryCharacteristicUUIDString = @"68753A44-4D6F-1226-9C60-0050E4C00069";
static NSString * serviceUUIDString = @"68753A44-4D6F-1226-9C60-0050E4C00070";
static CBUUID * serviceUUID;
static CBUUID * batteryCharacteristicUUID;

@implementation BatteryService

+ (void)initialize {
  serviceUUID = [CBUUID UUIDWithString:serviceUUIDString];
  batteryCharacteristicUUID = [CBUUID UUIDWithString:batteryCharacteristicUUIDString];
}

+ (CBUUID *)serviceUUID {
  return serviceUUID;
}

- (instancetype)initWithServiceManager:(WLXServiceManager *)serviceManager {
  self = [super init];
  if (self) {
    _serviceManager = serviceManager;
    _notificationsEnabled = NO;
  }
  return self;
}

- (void)setNotificationsEnabled:(BOOL)enabled {
  if (enabled == _notificationsEnabled) {
    return;
  }

  _notificationsEnabled = enabled;
  if (enabled) {
    [self.serviceManager enableNotificationsForCharacteristic:batteryCharacteristicUUID usingBlock:^(NSError * error) {
      if (error) {
        _notificationsEnabled = NO;
        if (self.notificationsErrorHandler) {
          notificationsErrorHandler(error);
        }
      }
    }];
  } else {
    [self.serviceManager disableNotificationsForCharacteristic:batteryCharacteristicUUID usingBlock:^(NSError * error) {
      if (error) {
        _notificationsEnabled = YES;
        if (self.notificationsErrorHandler) {
          notificationsErrorHandler(error);
        }
      }
    }];
  }
}

- (void)readBatteryLevelUsingBlock:(void(^)(NSError *, NSUInteger))block {
  [self.serviceManager readValueForCharacteristicUUID:batteryCharacteristicUUID usingBlock:^(NSError * error, NSData * data) {
    if (error) {
      block(error, 0);
    } else {
      NSUInteger value = *((NSUInteger *)data.bytes);
      block(nil, value);
    }
  }];
}

- (id)addBatteryLevelObserver:(void(^)(NSError *, NSUInteger))observer {
  return [self.serviceManager addObserverForCharacteristic:batteryCharacteristicUUID usingBlock:^(NSError * error, NSData * data) {
    if (error) {
      block(error, 0);
    } else {
      NSUInteger value = *((NSUInteger *)data.bytes);
      block(nil, value);
    }
  }];
}

- (void)removeBatteryLevelObserver:(id)observer {
  [self.serviceManager removeObserver:observer];
}

@end

Dispatch queues

Apart from the dispatch queue that the WLXBluetoothDeviceManager uses for all the Bluetooth operations, each WLXServiceManager creates its own serial dispatch queue. Once a bluetooth event gets dispatched to the service manager all the service related operations, like executing observers or callback blocks, are executed in a dedicated serial dispatch queue.

This is done to prevent "slow clients" to congest other services. For example if one service is notifying a lot of notifications and the registered observers are taking too long to process those notifications, the only service that will be affected will be the once with "slow clients".