Add GATT client

PiperOrigin-RevId: 557919250
This commit is contained in:
Nick Bourdakos
2023-08-17 13:27:39 -07:00
committed by Copybara-Service
parent 78dd1bab2c
commit 4aa52b45f9
19 changed files with 1647 additions and 29 deletions
@@ -23,4 +23,7 @@ typedef NS_ERROR_ENUM(GNCBLEErrorDomain, GNCBLEError){
GNCBLEErrorDuplicateCharacteristic,
GNCBLEErrorInvalidServiceData,
GNCBLEErrorAlreadyAdvertising,
GNCBLEErrorInvalidCharacteristic,
GNCBLEErrorAlreadyDiscoveringSpecifiedCharacteristics,
GNCBLEErrorAlreadyReadingCharacteristic,
};
@@ -15,20 +15,24 @@
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/** A container for GATT characteristic information. */
@interface GNCBLEGATTCharacteristic : NSObject
/** @remark init is not an available initializer. */
- (nonnull instancetype)init NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
/**
* Creates a container for GATT characteristic information.
* Creates a container for GATT characteristic information with properties.
*
* @param characteristicUUID The UUID of the characteristic.
* @param serviceUUID The UUID of the service.
* @param properties The properties of the characteristic.
*/
- (instancetype)initWithUUID:(nonnull CBUUID *)characteristicUUID
serviceUUID:(nonnull CBUUID *)serviceUUID;
- (instancetype)initWithUUID:(CBUUID *)characteristicUUID
serviceUUID:(CBUUID *)serviceUUID
properties:(CBCharacteristicProperties)properties;
/**
* Creates a container for GATT characteristic information with permissions and properties.
@@ -38,17 +42,16 @@
* @param permissions The permissions of the characteristic value.
* @param properties The properties of the characteristic.
*/
- (nonnull instancetype)initWithUUID:(nonnull CBUUID *)characteristicUUID
serviceUUID:(nonnull CBUUID *)serviceUUID
permissions:(CBAttributePermissions)permissions
properties:(CBCharacteristicProperties)properties
NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithUUID:(CBUUID *)characteristicUUID
serviceUUID:(CBUUID *)serviceUUID
permissions:(CBAttributePermissions)permissions
properties:(CBCharacteristicProperties)properties NS_DESIGNATED_INITIALIZER;
/** The 128-bit UUID that identifies the characteristic. */
@property(nonatomic, readonly, nonnull) CBUUID *characteristicUUID;
@property(nonatomic, readonly) CBUUID *characteristicUUID;
/** The 128-bit UUID that identifies the service that the characteristic belongs to. */
@property(nonatomic, readonly, nonnull) CBUUID *serviceUUID;
@property(nonatomic, readonly) CBUUID *serviceUUID;
/**
* The permissions of the characteristic value.
@@ -67,3 +70,5 @@
@property(nonatomic, readonly) CBCharacteristicProperties properties;
@end
NS_ASSUME_NONNULL_END
@@ -17,10 +17,17 @@
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@implementation GNCBLEGATTCharacteristic
- (instancetype)initWithUUID:(CBUUID *)characteristicUUID serviceUUID:(CBUUID *)serviceUUID {
return [self initWithUUID:characteristicUUID serviceUUID:serviceUUID permissions:0 properties:0];
- (instancetype)initWithUUID:(CBUUID *)characteristicUUID
serviceUUID:(CBUUID *)serviceUUID
properties:(CBCharacteristicProperties)properties {
return [self initWithUUID:characteristicUUID
serviceUUID:serviceUUID
permissions:0
properties:properties];
}
- (instancetype)initWithUUID:(CBUUID *)characteristicUUID
@@ -38,3 +45,5 @@
}
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,117 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
@class GNCBLEGATTCharacteristic;
@protocol GNCPeripheral;
NS_ASSUME_NONNULL_BEGIN
/**
* A block to be invoked when a call to
* @c discoverCharacteristicsWithUUIDs:serviceUUID:completionHandler: has completed.
*
* @param error On success, error will be @c nil. Otherwise, the error that occurred.
*/
typedef void (^GNCDiscoverCharacteristicsCompletionHandler)(NSError *_Nullable error);
/**
* A block to be invoked when a call to @c characteristicWithUUID:serviceUUID:completionHandler: has
* completed.
*
* @param characteristic The characteristic or @c nil if an error occurred.
* @param error On success, error will be @c nil. Otherwise, the error that occurred.
*/
typedef void (^GNCGetCharacteristicCompletionHandler)(
GNCBLEGATTCharacteristic *_Nullable characteristic, NSError *_Nullable error);
/**
* A block to be invoked when a call to @c readValueForCharacteristic:completionHandler: has
* completed.
*
* @param value The characteristic's value or @c nil if an error occurred.
* @param error On success, error will be @c nil. Otherwise, the error that occurred.
*/
typedef void (^GNCReadCharacteristicValueCompletionHandler)(NSData *_Nullable value,
NSError *_Nullable error);
/**
* An object that can be used to discover, explore, and interact with GATT services and
* characteristics available on a remote peripheral.
*
* @note The public APIs of this class are thread safe.
*/
@interface GNCBLEGATTClient : NSObject
/** @remark init is not an available initializer. */
- (instancetype)init NS_UNAVAILABLE;
/**
* Initializes the GATT client with a specified peripheral.
*
* @param peripheral The peripheral instance.
*/
- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral;
/**
* Discovers the specified characteristics of a service.
*
* A successful call to this method means the characteristics can be retreived with
* @c characteristicWithUUID:serviceUUID:completionHandler:.
*
* @param characteristics An array of 128-bit UUID objects of the characteristics that you are
* interested in.
* @param serviceUUID A 128-bit UUID that identifies the service that the characteristics belongs
* to.
* @param completionHandler Called on a private queue with @c nil if all characteristics have
* successfully been discovered or else an error.
*/
- (void)discoverCharacteristicsWithUUIDs:(NSArray<CBUUID *> *)characteristicUUIDs
serviceUUID:(CBUUID *)serviceUUID
completionHandler:
(nullable GNCDiscoverCharacteristicsCompletionHandler)completionHandler;
/**
* Retrieves a GATT characteristic.
*
* If you havent yet called the @c discoverCharacteristicsWithUUIDs:serviceUUID:completionHandler:
* method to discover the characteristic, or if there was an error in doing so, this method will
* complete with an error.
*
* @param characteristicUUID A 128-bit UUID that identifies the characteristic.
* @param serviceUUID A 128-bit UUID that identifies the service that the characteristic belongs to.
* @param completionHandler Called on a private queue with the characteristic if successful or an
* error if one has occured.
*/
- (void)characteristicWithUUID:(CBUUID *)characteristicUUID
serviceUUID:(CBUUID *)serviceUUID
completionHandler:(nullable GNCGetCharacteristicCompletionHandler)completionHandler;
/**
* Reads the requested characteristic from the associated remote device.
*
* @param characteristic The characteristic whose value you want to read.
* @param completionHandler Called on a private queue with the characteristics value if successful
* or an error if one has occured.
*/
- (void)readValueForCharacteristic:(GNCBLEGATTCharacteristic *)characteristic
completionHandler:
(nullable GNCReadCharacteristicValueCompletionHandler)completionHandler;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,299 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
NS_ASSUME_NONNULL_BEGIN
static char *const kGNCBLEGATTClientQueueLabel = "com.nearby.GNCBLEGATTClient";
static NSError *InvalidCharacteristicError() {
return [NSError errorWithDomain:GNCBLEErrorDomain
code:GNCBLEErrorInvalidCharacteristic
userInfo:nil];
}
static NSError *AlreadyDiscoveringSpecifiedCharacteristicsError() {
return [NSError errorWithDomain:GNCBLEErrorDomain
code:GNCBLEErrorAlreadyDiscoveringSpecifiedCharacteristics
userInfo:nil];
}
static NSError *AlreadyReadingCharacteristicError() {
return [NSError errorWithDomain:GNCBLEErrorDomain
code:GNCBLEErrorAlreadyReadingCharacteristic
userInfo:nil];
}
@interface GNCBLEGATTClient () <GNCPeripheralDelegate>
@end
@implementation GNCBLEGATTClient {
dispatch_queue_t _queue;
id<GNCPeripheral> _peripheral;
/**
* A map of service UUIDs with each service holding a map of a list of characterisitcs to a
* completion handler. This is used to track the groupings of characteristic discovery requests.
* When all characteristics of a request are discovered, the completion handler is called and
* removed from the map.
*/
NSMutableDictionary<CBUUID *, NSMutableDictionary<NSArray<CBUUID *> *,
GNCDiscoverCharacteristicsCompletionHandler> *>
*_discoverCharacteristicsCompletionHandlers;
/**
* A service to characteristic to completion handler map. Used to track pending read requests.
* When a characteristic's value has been updated, the completion handler is called and removed
* from the map.
*/
NSMutableDictionary<CBUUID *,
NSMutableDictionary<CBUUID *, GNCReadCharacteristicValueCompletionHandler> *>
*_readCharacteristicValueCompletionHandlers;
}
- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral {
return [self
initWithPeripheral:peripheral
queue:dispatch_queue_create(kGNCBLEGATTClientQueueLabel, DISPATCH_QUEUE_SERIAL)];
};
// Private.
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)peripheral
queue:(nullable dispatch_queue_t)queue {
self = [super init];
if (self) {
_queue = queue ?: dispatch_get_main_queue();
_peripheral = peripheral;
_peripheral.peripheralDelegate = self;
_discoverCharacteristicsCompletionHandlers = [[NSMutableDictionary alloc] init];
_readCharacteristicValueCompletionHandlers = [[NSMutableDictionary alloc] init];
}
return self;
};
- (void)discoverCharacteristicsWithUUIDs:(NSArray<CBUUID *> *)characteristicUUIDs
serviceUUID:(CBUUID *)serviceUUID
completionHandler:
(nullable GNCDiscoverCharacteristicsCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
if (!_discoverCharacteristicsCompletionHandlers[serviceUUID]) {
_discoverCharacteristicsCompletionHandlers[serviceUUID] = [[NSMutableDictionary alloc] init];
}
// Return an error if we are already discovering the provided characteristics.
if (_discoverCharacteristicsCompletionHandlers[serviceUUID][characteristicUUIDs]) {
if (completionHandler) {
completionHandler(AlreadyDiscoveringSpecifiedCharacteristicsError());
}
return;
}
// Use the list of characteristic UUIDs as the key. This makes it easy to associate the
// completion handler with the list of characteristics we are waiting for, as well as retrieving
// the complete list of characteristics we need to query for a single service.
_discoverCharacteristicsCompletionHandlers[serviceUUID][characteristicUUIDs] =
completionHandler;
// Note: A call to @c discoverServices: will always be paired with a delegate call even if the
// service has already been discovered.
[_peripheral discoverServices:@[ serviceUUID ]];
});
}
- (void)characteristicWithUUID:(CBUUID *)characteristicUUID
serviceUUID:(CBUUID *)serviceUUID
completionHandler:(nullable GNCGetCharacteristicCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
CBCharacteristic *characteristic = [self synchronousCharacteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID];
if (!characteristic) {
if (completionHandler) {
completionHandler(nil, InvalidCharacteristicError());
}
return;
}
if (completionHandler) {
completionHandler([[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristic.UUID
serviceUUID:characteristic.service.UUID
properties:characteristic.properties],
nil);
}
});
}
- (void)readValueForCharacteristic:(GNCBLEGATTCharacteristic *)characteristic
completionHandler:
(nullable GNCReadCharacteristicValueCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
CBCharacteristic *cbCharacteristic =
[self synchronousCharacteristicWithUUID:characteristic.characteristicUUID
serviceUUID:characteristic.serviceUUID];
if (!cbCharacteristic) {
if (completionHandler) {
completionHandler(nil, InvalidCharacteristicError());
}
return;
}
if (!_readCharacteristicValueCompletionHandlers[characteristic.serviceUUID]) {
_readCharacteristicValueCompletionHandlers[characteristic.serviceUUID] =
[[NSMutableDictionary alloc] init];
}
// Return an error if we are already discovering the provided characteristics.
if (_readCharacteristicValueCompletionHandlers[characteristic.serviceUUID]
[characteristic.characteristicUUID]) {
if (completionHandler) {
completionHandler(nil, AlreadyReadingCharacteristicError());
}
return;
}
_readCharacteristicValueCompletionHandlers[characteristic.serviceUUID]
[characteristic.characteristicUUID] =
completionHandler;
[_peripheral readValueForCharacteristic:cbCharacteristic];
});
}
#pragma mark - Internal
- (CBCharacteristic *)synchronousCharacteristicWithUUID:(CBUUID *)characteristicUUID
serviceUUID:(CBUUID *)serviceUUID {
dispatch_assert_queue(_queue);
for (CBService *service in _peripheral.services) {
if ([service.UUID isEqual:serviceUUID]) {
for (CBCharacteristic *characteristic in service.characteristics) {
if ([characteristic.UUID isEqual:characteristicUUID]) {
return characteristic;
}
}
}
}
return nil;
}
#pragma mark - GNCPeripheralDelegate
- (void)gnc_peripheral:(id<GNCPeripheral>)peripheral didDiscoverServices:(nullable NSError *)error {
dispatch_assert_queue(_queue);
// TODO(b/295911088): Queue incoming requests by service would allow us to not attempt
// characteristc discovery on all services and to short circuit and call the completion handler if
// there was an error.
for (CBService *service in peripheral.services) {
// Flatten lists of characteristics for a given service into a single list for discovery.
NSArray<NSArray<CBUUID *> *> *groupedCharacteristics =
_discoverCharacteristicsCompletionHandlers[service.UUID].allKeys;
if (!groupedCharacteristics) {
continue;
}
NSMutableSet<CBUUID *> *flattenedCharacteristics = [[NSMutableSet alloc] init];
for (NSArray<CBUUID *> *characteristics in groupedCharacteristics) {
[flattenedCharacteristics addObjectsFromArray:characteristics];
}
// Note: Since we don't clear @c _discoverCharacteristicsCompletionHandlers until the
// characterististics have been discovered, multiple calls to
// @c discoverService:characteristics:completionHandler: will cause duplicate characteristic
// discovery calls on the same service. We CANNOT clear the pending list until after we actually
// discover the characteristics, because there can be more than 1 service with the same UUID.
// This is a common occurence for Nearby services, so it should not be treated as an edge case.
[_peripheral discoverCharacteristics:[flattenedCharacteristics allObjects] forService:service];
}
}
- (void)gnc_peripheral:(id<GNCPeripheral>)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
error:(nullable NSError *)error {
dispatch_assert_queue(_queue);
// Check if each group of characteristics is a subset of the discovered characteristics. If all
// characteristics of the group have been discovered, call the completion handler with success,
// otherwise continue waiting. If the characteristic discovery returns an error, call all pending
// discovery request completion handlers with the error.
NSMutableSet<CBUUID *> *characteristics = [[NSMutableSet alloc] init];
for (CBCharacteristic *characteristic in service.characteristics) {
[characteristics addObject:characteristic.UUID];
}
[_discoverCharacteristicsCompletionHandlers[service.UUID].copy
enumerateKeysAndObjectsUsingBlock:^(NSArray<CBUUID *> *pendingCharacteristics,
GNCDiscoverCharacteristicsCompletionHandler handler,
BOOL *stop) {
if ([[NSSet setWithArray:pendingCharacteristics] isSubsetOfSet:characteristics]) {
_discoverCharacteristicsCompletionHandlers[service.UUID][pendingCharacteristics] = nil;
handler(nil);
return;
}
// TODO(b/295911088): Queue incoming requests by service to avoid this issue.
// This could be a race, where @c discoverService:characteristics:completionHandler: is
// called multiple times quickly for the same service. If a request fails, all pending
// requests for the same service are also failed.
if (error) {
_discoverCharacteristicsCompletionHandlers[service.UUID][pendingCharacteristics] = nil;
handler(error);
return;
}
}];
}
- (void)gnc_peripheral:(id<GNCPeripheral>)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(nullable NSError *)error {
dispatch_assert_queue(_queue);
NSMutableDictionary<CBUUID *, GNCReadCharacteristicValueCompletionHandler> *handlers =
_readCharacteristicValueCompletionHandlers[characteristic.service.UUID];
if (!handlers) {
return;
}
GNCReadCharacteristicValueCompletionHandler handler = handlers[characteristic.UUID];
handlers[characteristic.UUID] = nil;
if (handler) {
handler(characteristic.value, error);
}
}
#pragma mark - CBPeripheralDelegate
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error {
dispatch_async(_queue, ^{
[self gnc_peripheral:peripheral didDiscoverServices:error];
});
}
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
error:(nullable NSError *)error {
dispatch_async(_queue, ^{
[self gnc_peripheral:peripheral didDiscoverCharacteristicsForService:service error:error];
});
}
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(nullable NSError *)error {
dispatch_async(_queue, ^{
[self gnc_peripheral:peripheral didUpdateValueForCharacteristic:characteristic error:error];
});
}
@end
NS_ASSUME_NONNULL_END
@@ -41,8 +41,8 @@ typedef void (^GNCStartAdvertisingCompletionHandler)(NSError *_Nullable error);
* @param characteristicUUID A 128-bit UUID that identifies the characteristic.
* @param permissions The permissions of the characteristic value.
* @param properties The properties of the characteristic.
* @param completionHandler Called on the main queue with the characteristic if successfully created
* or an error if one has occured.
* @param completionHandler Called on a private queue with the characteristic if successfully
* created or an error if one has occured.
*/
- (void)createCharacteristicWithServiceID:(CBUUID *)serviceUUID
characteristicUUID:(CBUUID *)characteristicUUID
@@ -56,8 +56,8 @@ typedef void (^GNCStartAdvertisingCompletionHandler)(NSError *_Nullable error);
*
* @param characteristic The characteristic to update.
* @param value The new value for the characteristic.
* @param completionHandler Called on the main queue with @c nil if successfully updated or an error
* if one has occured.
* @param completionHandler Called on a private queue with @c nil if successfully updated or an
* error if one has occured.
*/
- (void)updateCharacteristic:(GNCBLEGATTCharacteristic *)characteristic
value:(nullable NSData *)value
@@ -76,8 +76,8 @@ typedef void (^GNCStartAdvertisingCompletionHandler)(NSError *_Nullable error);
* longer than 22 bytes. This also means we can only support advertising a single service.
*
* @param serviceData A dictionary that contains service-specific advertisement data.
* @param completionHandler Called on the main queue with @c nil if successfully started advertising
* or an error if one has occured.
* @param completionHandler Called on a private queue with @c nil if successfully started
* advertising or an error if one has occured.
*/
- (void)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData
completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler;
@@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
static char *const kGNCBLEGATTServerQueueLabel = "com.nearby.GNCBLEGATTServer";
@interface GNCBLEGATTServer () <GNCPeripheralManagerDelegate, CBPeripheralManagerDelegate>
@interface GNCBLEGATTServer () <GNCPeripheralManagerDelegate>
@end
@implementation GNCBLEGATTServer {
@@ -0,0 +1,167 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
@protocol GNCPeripheralDelegate;
NS_ASSUME_NONNULL_BEGIN
/** Protocol which helps create a fake of a @c CBPeripheral to inject for testing. */
@protocol GNCPeripheral
// This can't be @c delegate, because it would shadow @c CBPeripheral's delegate.
/**
* The peripheral's delegate.
*
* This delegate is only sent CBPeripheralDelegate messages, and the delegate is responsible for
* forwarding those messages to its GNCPeripheralDelegate method implementations.
*
* See: https://developer.apple.com/videos/play/wwdc2018/417/
*/
@property(weak, nonatomic, nullable) id<GNCPeripheralDelegate> peripheralDelegate;
/**
* A list of a peripherals discovered services.
*
* Returns an array of services (represented by CBService objects) that successful a call to the
* @c discoverServices: method discovered. If you havent yet called the @c discoverServices: method
* to discover the services of the peripheral, or if there was an error in doing so, the value of
* this property is @c nil.
*/
@property(retain, readonly, nullable) NSArray<CBService *> *services;
/**
* Discovers the specified services of the peripheral.
*
* You can provide an array of CBUUID objects, representing service UUIDs, in the @c serviceUUIDs
* parameter. When you do, the peripheral returns only the services of the peripheral that match the
* provided UUIDs.
*
* @note If the @c serviceUUIDs parameter is @c nil, this method returns all of the peripherals
* available services. This is much slower than providing an array of service UUIDs to search for.
*
* When the peripheral discovers one or more services, it calls the
* @c peripheral:didDiscoverServices: method of its delegate object. After a peripheral discovers
* services, you can access them through the peripherals @c services property.
*
* @param serviceUUIDs An array of CBUUID objects that you are interested in. Each CBUUID object
* represents a UUID that identifies the type of service you want to discover.
*/
- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs;
/**
* Discovers the specified characteristics of a service.
*
* You can provide an array of CBUUID objects, representing characteristic UUIDs, in the
* @c characteristicUUIDs parameter. When you do, the peripheral returns only the characteristics of
* the service that match the provided UUIDs. If the @c characteristicUUIDs parameter is @c nil,
* this method returns all characteristics of the service.
*
* @note If the @c characteristicUUIDs parameter is @c nil, this method returns all of the services
* characteristics. This is much slower than providing an array of characteristic UUIDs to search
* for.
*
* When the peripheral discovers one or more characteristics of the specified service, it calls the
* @c peripheral:didDiscoverCharacteristicsForService:error: method of its delegate object. After
* the peripheral discovers the services characteristics, you can access them through the services
* @c characteristics property.
*
* @param characteristicUUIDs An array of CBUUID objects that you are interested in. Each CBUUID
* object represents a UUID that identifies the type of a characteristic
* you want to discover.
* @param service The service whose characteristics you want to discover.
*/
- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)characteristicUUIDs
forService:(CBService *)service;
/**
* Retrieves the value of a specified characteristic.
*
* When you call this method to read the value of a characteristic, the peripheral calls the
* @c peripheral:didUpdateValueForCharacteristic:error: method of its delegate object. If the
* peripheral successfully reads the value of the characteristic, you can access it through the
* characteristics @c value property.
*
* Not all characteristics have a readable value. You can determine whether a characteristics value
* is readable by accessing the relevant properties of the CBCharacteristicProperties enumeration.
*
* @param characteristic The characteristic whose value you want to read.
*/
- (void)readValueForCharacteristic:(CBCharacteristic *)characteristic;
@end
/**
* Protocol which helps the @c GNCPeripheral wrap a @c CBPeripheralDelegate for
* testing.
*/
@protocol GNCPeripheralDelegate <CBPeripheralDelegate>
/**
* Tells the delegate that peripheral service discovery succeeded.
*
* Called when your app calls the @c discoverServices: method. If the peripheral successfully
* discovers services, you can access them through the peripherals @c services property. If
* successful, the @c error parameter is @c nil. If unsuccessful, the @c error parameter returns the
* cause of the failure.
*
* @param peripheral The peripheral to which the services belong.
* @param error The reason the call failed, or @c nil if no error occurred.
*/
- (void)gnc_peripheral:(id<GNCPeripheral>)peripheral didDiscoverServices:(nullable NSError *)error;
/**
* Tells the delegate that the peripheral found characteristics for a service.
*
* Called when your app calls the @c discoverCharacteristics:forService: method. If the peripheral
* successfully discovers the characteristics of the specified service, you can access them through
* the services @c characteristics property. If successful, the @c error parameter is @c nil. If
* unsuccessful, the @c error parameter returns the cause of the failure.
*
* @param peripheral The peripheral providing this information.
* @param service The service to which the characteristics belong.
* @param error The reason the call failed, or @c nil if no error occurred.
*/
- (void)gnc_peripheral:(id<GNCPeripheral>)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
error:(nullable NSError *)error;
/**
* Tells the delegate that retrieving the specified characteristics value succeeded, or that the
* characteristics value changed.
*
* Called when your app calls the @c readValueForCharacteristic: method. If successful, the @c error
* parameter is @c nil. If unsuccessful, the @c error parameter returns the cause of the failure.
*
* @param peripheral The peripheral providing this information.
* @param characteristic The characteristic containing the value.
* @param error The reason the call failed, or @c nil if no error occurred.
*/
- (void)gnc_peripheral:(id<GNCPeripheral>)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(nullable NSError *)error;
@end
/**
* Declares that @c CBPeripheral implements the @c GNCPeripheral protocol.
*
* This allows us to directly use a @c CBPeripheral as a @c GNCPeripheral.
*/
@interface CBPeripheral () <GNCPeripheral>
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,34 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@implementation CBPeripheral (GNCPeripheralAdditions)
- (void)setPeripheralDelegate:(nullable id<GNCPeripheralDelegate>)peripheralDelegate {
self.delegate = peripheralDelegate;
}
- (nullable id<GNCPeripheralDelegate>)peripheralDelegate {
return (id<GNCPeripheralDelegate>)self.delegate;
}
@end
NS_ASSUME_NONNULL_END
@@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
@protocol GNCPeripheralManager
/** Shadow property of a @c CBPeripheralManagerDelegate. */
@property(nonatomic, nullable) id<GNCPeripheralManagerDelegate> peripheralDelegate;
@property(weak, nonatomic, nullable) id<GNCPeripheralManagerDelegate> peripheralDelegate;
@property(nonatomic, assign, readonly) CBManagerState state;
@@ -124,7 +124,7 @@ NS_ASSUME_NONNULL_BEGIN
* Protocol which helps the @c GNCPeripheralManager wrap a @c CBPeripheralManagerDelegate for
* testing.
*/
@protocol GNCPeripheralManagerDelegate <NSObject>
@protocol GNCPeripheralManagerDelegate <CBPeripheralManagerDelegate>
/**
* Tells the delegate the peripheral managers state updated.
@@ -23,9 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
@implementation CBPeripheralManager (GNCPeripheralManagerAdditions)
- (void)setPeripheralDelegate:(nullable id<GNCPeripheralManagerDelegate>)peripheralDelegate {
NSAssert([peripheralDelegate conformsToProtocol:@protocol(CBPeripheralManagerDelegate)],
@"peripheralDelegate must conform to protocol CBPeripheralManagerDelegate");
self.delegate = (id<CBPeripheralManagerDelegate>)peripheralDelegate;
self.delegate = peripheralDelegate;
}
- (nullable id<GNCPeripheralManagerDelegate>)peripheralDelegate {
@@ -20,7 +20,9 @@ objc_library(
srcs = [
"BLEv2/GNCBLEError.m",
"BLEv2/GNCBLEGATTCharacteristic.m",
"BLEv2/GNCBLEGATTClient.m",
"BLEv2/GNCBLEGATTServer.m",
"BLEv2/GNCPeripheral.m",
"BLEv2/GNCPeripheralManager.m",
"BLEv2/NSData+GNCWebSafeBase64.m",
"Ble/GNCMBleCentral.m",
@@ -40,7 +42,9 @@ objc_library(
hdrs = [
"BLEv2/GNCBLEError.h",
"BLEv2/GNCBLEGATTCharacteristic.h",
"BLEv2/GNCBLEGATTClient.h",
"BLEv2/GNCBLEGATTServer.h",
"BLEv2/GNCPeripheral.h",
"BLEv2/GNCPeripheralManager.h",
"BLEv2/NSData+GNCWebSafeBase64.h",
"Ble/GNCMBleCentral.h",
@@ -24,12 +24,15 @@ objc_library(
testonly = True,
srcs = [
"GNCBLEGATTCharacteristicTest.mm",
"GNCBLEGATTClientTest.m",
"GNCBLEGATTServer+Testing.h",
"GNCBLEGATTServerTest.m",
"GNCBLEUtilsTest.mm",
"GNCBleTest.mm",
"GNCBluetoothAdapterTest.mm",
"GNCCryptoTest.mm",
"GNCFakePeripheral.h",
"GNCFakePeripheral.m",
"GNCFakePeripheralManager.h",
"GNCFakePeripheralManager.m",
"GNCIPAddressTest.mm",
@@ -40,6 +43,7 @@ objc_library(
"NSData+GNCWebSafeBase64Test.m",
],
deps = [
":GNCBLEGATTClient_Testing",
"//internal/platform:base",
"//internal/platform/implementation:comm",
"//internal/platform/implementation:platform",
@@ -54,6 +58,15 @@ objc_library(
],
)
objc_library(
name = "GNCBLEGATTClient_Testing",
hdrs = ["GNCBLEGATTClient+Testing.h"],
deps = [
"//internal/platform/implementation/apple/Mediums",
"//third_party/apple_frameworks:Foundation",
],
)
ios_unit_test(
name = "PlatformTests",
minimum_os_version = IOS_MINIMUM_OS,
@@ -26,12 +26,15 @@
- (void)testConvenienceInit {
CBUUID *characteristicUUID = [CBUUID UUIDWithString:@"00000000-0000-3000-8000-000000000000"];
CBUUID *serviceUUID = [CBUUID UUIDWithString:@"0000FEF3-0000-1000-8000-00805F9B34FB"];
CBCharacteristicProperties properties = CBCharacteristicPropertyRead;
GNCBLEGATTCharacteristic *characteristic =
[[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID serviceUUID:serviceUUID];
[[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID
serviceUUID:serviceUUID
properties:properties];
XCTAssertEqualObjects(characteristic.characteristicUUID, characteristicUUID);
XCTAssertEqualObjects(characteristic.serviceUUID, serviceUUID);
XCTAssertEqual(characteristic.permissions, 0);
XCTAssertEqual(characteristic.properties, 0);
XCTAssertEqual(characteristic.properties, properties);
}
- (void)testInit {
@@ -0,0 +1,40 @@
// THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_APPLE_TESTS_GNCBLEGATTCLIENT_TESTING_H_
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h"
#import <Foundation/Foundation.h>
@protocol GNCPeripheral;
NS_ASSUME_NONNULL_BEGIN
@interface GNCBLEGATTClient (Testing)
/**
* Creates a GATT client with a provided peripheral.
*
* This is only exposed for testing and can be used to inject a fake peripheral.
*
* @param peripheral The peripheral instance.
* @param queue The queue to run on, this must match the queue that the peripheral's delegate is
* running on. Defaults to the main queue when @c nil.
*/
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)peripheral
queue:(nullable dispatch_queue_t)queue;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,748 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h"
#import "internal/platform/implementation/apple/Tests/GNCBLEGATTClient+Testing.h"
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.h"
static NSString *const kServiceUUID1 = @"0000FEF3-0000-1000-8000-00805F9B34FB";
static NSString *const kServiceUUID2 = @"0000FEF4-0000-1000-8000-00805F9B34FB";
static NSString *const kCharacteristicUUID1 = @"00000000-0000-3000-8000-000000000000";
static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-000000000001";
@interface GNCBLEGATTClientTest : XCTestCase
@end
@implementation GNCBLEGATTClientTest
#pragma mark - Discover Characteristics
- (void)testDiscoverCharacteristics {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *discoverCharacteristicsExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Discover characteristics."];
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[discoverCharacteristicsExpectation fulfill];
}];
[self waitForExpectations:@[ discoverCharacteristicsExpectation ] timeout:3];
XCTAssertEqualObjects(fakePeripheral.services[0].UUID, serviceUUID);
XCTAssertEqualObjects(fakePeripheral.services[0].characteristics[0].UUID, characteristicUUID);
}
- (void)testDiscoverCharacteristicsWithServiceDiscoveryError {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
fakePeripheral.discoverServicesError = [NSError errorWithDomain:@"fake" code:0 userInfo:nil];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *discoverCharacteristicsExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Discover characteristics."];
// TODO(b/295911088): Failed discovery of services won't trigger the completionHandler until we
// implement a service queue.
discoverCharacteristicsExpectation.inverted = YES;
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
// TODO(b/295911088): Make assertions when service queue is
// implemented.
[discoverCharacteristicsExpectation fulfill];
}];
[self waitForExpectations:@[ discoverCharacteristicsExpectation ] timeout:3];
XCTAssertEqual(fakePeripheral.services.count, 0);
}
- (void)testDiscoverCharacteristicsWithCharacteristicDiscoveryError {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
fakePeripheral.discoverCharacteristicsForServiceError = [NSError errorWithDomain:@"fake"
code:0
userInfo:nil];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *discoverCharacteristicsExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Discover characteristics."];
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
XCTAssertNotNil(error);
[discoverCharacteristicsExpectation fulfill];
}];
[self waitForExpectations:@[ discoverCharacteristicsExpectation ] timeout:3];
XCTAssertEqualObjects(fakePeripheral.services[0].UUID, serviceUUID);
XCTAssertEqual(fakePeripheral.services[0].characteristics.count, 0);
}
// TODO(b/295911088): When service queue is implemented, this is expected to not be an error.
- (void)testDuplicateDiscoverCharacteristics {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *discoverCharacteristics1Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 1."];
XCTestExpectation *discoverCharacteristics2Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 2."];
fakePeripheral.delegateDelay = 1;
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[discoverCharacteristics1Expectation fulfill];
}];
// Queue the delay change so it doesn't immediately overwrite the delay set for the previous
// operation.
dispatch_async(dispatch_get_main_queue(), ^{
fakePeripheral.delegateDelay = 0;
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
XCTAssertNotNil(error);
[discoverCharacteristics2Expectation fulfill];
}];
});
[self waitForExpectations:@[
discoverCharacteristics1Expectation, discoverCharacteristics2Expectation
]
timeout:3];
}
- (void)testDiscoverCharacteristicsMultipleCallsWithDifferentServices {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID1 = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *serviceUUID2 = [CBUUID UUIDWithString:kServiceUUID2];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *discoverCharacteristics1Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 1."];
XCTestExpectation *discoverCharacteristics2Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 2."];
fakePeripheral.delegateDelay = 1;
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID1
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[discoverCharacteristics1Expectation fulfill];
}];
// Queue the delay change so it doesn't immediately overwrite the delay set for the previous
// operation.
dispatch_async(dispatch_get_main_queue(), ^{
fakePeripheral.delegateDelay = 0;
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID2
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[discoverCharacteristics2Expectation fulfill];
}];
});
[self waitForExpectations:@[
discoverCharacteristics1Expectation, discoverCharacteristics2Expectation
]
timeout:3];
XCTAssertEqualObjects(fakePeripheral.services[0].UUID, serviceUUID2);
XCTAssertEqualObjects(fakePeripheral.services[0].characteristics[0].UUID, characteristicUUID);
XCTAssertEqualObjects(fakePeripheral.services[1].UUID, serviceUUID1);
XCTAssertEqualObjects(fakePeripheral.services[1].characteristics[0].UUID, characteristicUUID);
}
- (void)testDiscoverCharacteristicsMultipleCallsWithDifferentCharacteristics {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:kCharacteristicUUID1];
CBUUID *characteristicUUID2 = [CBUUID UUIDWithString:kCharacteristicUUID2];
XCTestExpectation *discoverCharacteristics1Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 1."];
XCTestExpectation *discoverCharacteristics2Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 2."];
fakePeripheral.delegateDelay = 1;
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID1 ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[discoverCharacteristics1Expectation fulfill];
}];
// Queue the delay change so it doesn't immediately overwrite the delay set for the previous
// operation.
dispatch_async(dispatch_get_main_queue(), ^{
fakePeripheral.delegateDelay = 0;
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID2 ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[discoverCharacteristics2Expectation fulfill];
}];
});
[self waitForExpectations:@[
discoverCharacteristics1Expectation, discoverCharacteristics2Expectation
]
timeout:3];
XCTAssertEqualObjects(fakePeripheral.services[0].UUID, serviceUUID);
XCTAssertEqualObjects(fakePeripheral.services[0].characteristics[0].UUID, characteristicUUID2);
XCTAssertEqualObjects(fakePeripheral.services[0].characteristics[1].UUID, characteristicUUID1);
}
#pragma mark - Get Characteristic
- (void)testGetCharacteristic {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *characteristicExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Get characteristic."];
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[gattClient characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(
GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(characteristic);
[characteristicExpectation fulfill];
}];
}];
[self waitForExpectations:@[ characteristicExpectation ] timeout:3];
}
- (void)testGetCharacteristicWithServiceDiscoveryError {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
fakePeripheral.discoverServicesError = [NSError errorWithDomain:@"fake" code:0 userInfo:nil];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *characteristicExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Get characteristic."];
// TODO(b/295911088): Failed discovery of services won't trigger the completionHandler until we
// implement a service queue.
characteristicExpectation.inverted = YES;
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[gattClient characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(
GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
// TODO(b/295911088): Make assertions when service
// queue is implemented.
[characteristicExpectation fulfill];
}];
}];
[self waitForExpectations:@[ characteristicExpectation ] timeout:3];
}
- (void)testGetCharacteristicWithCharacteristicDiscoveryError {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
fakePeripheral.discoverCharacteristicsForServiceError = [NSError errorWithDomain:@"fake"
code:0
userInfo:nil];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *characteristicExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Get characteristic."];
[gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[gattClient characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(
GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
XCTAssertNotNil(error);
XCTAssertNil(characteristic);
[characteristicExpectation fulfill];
}];
}];
[self waitForExpectations:@[ characteristicExpectation ] timeout:3];
}
- (void)testDuplicateGetCharacteristic {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *characteristic1Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Get characteristic 1."];
XCTestExpectation *characteristic2Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Get characteristic 2."];
[gattClient
discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
fakePeripheral.delegateDelay = 1;
[gattClient
characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(characteristic);
[characteristic1Expectation fulfill];
}];
// Queue the delay change so it doesn't immediately overwrite the delay set
// for the previous operation.
dispatch_async(dispatch_get_main_queue(), ^{
fakePeripheral.delegateDelay = 0;
[gattClient
characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(characteristic);
[characteristic2Expectation fulfill];
}];
});
}];
[self waitForExpectations:@[ characteristic1Expectation, characteristic2Expectation ] timeout:3];
}
- (void)testGetNonExistentCharacteristic {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *characteristicExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Get characteristic."];
[gattClient characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic, NSError *error) {
XCTAssertNotNil(error);
XCTAssertNil(characteristic);
[characteristicExpectation fulfill];
}];
[self waitForExpectations:@[ characteristicExpectation ] timeout:3];
}
#pragma mark - Read Value for Characteristic
- (void)testReadValueForCharacteristic {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *readValueForCharacteristicExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."];
[gattClient
discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[gattClient
characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
[gattClient
readValueForCharacteristic:characteristic
completionHandler:^(NSData *value, NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(value);
[readValueForCharacteristicExpectation fulfill];
}];
}];
}];
[self waitForExpectations:@[ readValueForCharacteristicExpectation ] timeout:3];
}
- (void)testReadValueForCharacteristicWithServiceDiscoveryError {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
fakePeripheral.discoverServicesError = [NSError errorWithDomain:@"fake" code:0 userInfo:nil];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *readValueForCharacteristicExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."];
// TODO(b/295911088): Failed discovery of services won't trigger the completionHandler until we
// implement a service queue.
readValueForCharacteristicExpectation.inverted = YES;
[gattClient
discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[gattClient
characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
[gattClient
readValueForCharacteristic:characteristic
completionHandler:^(NSData *value, NSError *error) {
// TODO(b/295911088): Make assertions when service
// queue is implemented.
[readValueForCharacteristicExpectation fulfill];
}];
}];
}];
[self waitForExpectations:@[ readValueForCharacteristicExpectation ] timeout:3];
}
- (void)testReadValueForCharacteristicWithCharacteristicDiscoveryError {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
fakePeripheral.discoverCharacteristicsForServiceError = [NSError errorWithDomain:@"fake"
code:0
userInfo:nil];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *readValueForCharacteristicExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."];
[gattClient
discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[gattClient
characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
[gattClient
readValueForCharacteristic:characteristic
completionHandler:^(NSData *value, NSError *error) {
XCTAssertNotNil(error);
XCTAssertNil(value);
[readValueForCharacteristicExpectation fulfill];
}];
}];
}];
[self waitForExpectations:@[ readValueForCharacteristicExpectation ] timeout:3];
}
- (void)testReadValueForCharacteristicWithReadValueForCharacteristicError {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
fakePeripheral.readValueForCharacteristicError = [NSError errorWithDomain:@"fake"
code:0
userInfo:nil];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *readValueForCharacteristicExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."];
[gattClient
discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[gattClient
characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
[gattClient
readValueForCharacteristic:characteristic
completionHandler:^(NSData *value, NSError *error) {
XCTAssertNotNil(error);
XCTAssertNil(value);
[readValueForCharacteristicExpectation fulfill];
}];
}];
}];
[self waitForExpectations:@[ readValueForCharacteristicExpectation ] timeout:3];
}
// TODO(b/295911088): When service queue is implemented, this is expected to not be an error.
- (void)testDuplicateReadValueForCharacteristic {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
XCTestExpectation *readValueForCharacteristic1Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic 1."];
XCTestExpectation *readValueForCharacteristic2Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic 2."];
[gattClient
discoverCharacteristicsWithUUIDs:@[ characteristicUUID ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[gattClient
characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
fakePeripheral.delegateDelay = 1;
[gattClient
readValueForCharacteristic:characteristic
completionHandler:^(NSData *value, NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(value);
[readValueForCharacteristic1Expectation fulfill];
}];
// Queue the delay change so it doesn't immediately overwrite the
// delay set for the previous operation.
dispatch_async(dispatch_get_main_queue(), ^{
fakePeripheral.delegateDelay = 0;
[gattClient
readValueForCharacteristic:characteristic
completionHandler:^(NSData *value,
NSError *error) {
XCTAssertNotNil(error);
XCTAssertNil(value);
[readValueForCharacteristic2Expectation fulfill];
}];
});
}];
}];
[self waitForExpectations:@[
readValueForCharacteristic1Expectation, readValueForCharacteristic2Expectation
]
timeout:3];
}
- (void)testReadValueForMultipleCharacteristics {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:kCharacteristicUUID1];
CBUUID *characteristicUUID2 = [CBUUID UUIDWithString:kCharacteristicUUID2];
XCTestExpectation *readValueForCharacteristic1Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic 1."];
XCTestExpectation *readValueForCharacteristic2Expectation =
[[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic 2."];
[gattClient
discoverCharacteristicsWithUUIDs:@[ characteristicUUID1, characteristicUUID2 ]
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[gattClient
characteristicWithUUID:characteristicUUID1
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
fakePeripheral.delegateDelay = 1;
[gattClient
readValueForCharacteristic:characteristic
completionHandler:^(NSData *value, NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(value);
[readValueForCharacteristic1Expectation fulfill];
}];
}];
[gattClient
characteristicWithUUID:characteristicUUID2
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
// Queue the delay change so it doesn't immediately overwrite the
// delay set for the previous operation.
dispatch_async(dispatch_get_main_queue(), ^{
fakePeripheral.delegateDelay = 0;
[gattClient
readValueForCharacteristic:characteristic
completionHandler:^(NSData *value,
NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(value);
[readValueForCharacteristic2Expectation fulfill];
}];
});
}];
}];
[self waitForExpectations:@[
readValueForCharacteristic1Expectation, readValueForCharacteristic2Expectation
]
timeout:3];
}
- (void)testReadValueForUndiscoveredCharacteristic {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID
serviceUUID:serviceUUID
properties:CBCharacteristicPropertyRead];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."];
[gattClient readValueForCharacteristic:characteristic
completionHandler:^(NSData *value, NSError *error) {
XCTAssertNotNil(error);
XCTAssertNil(value);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
#pragma mark - Delegate Calls
- (void)testUnexpectedDelegateCalls {
GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init];
GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral
queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
[fakePeripheral.peripheralDelegate gnc_peripheral:fakePeripheral didDiscoverServices:nil];
[fakePeripheral.peripheralDelegate
gnc_peripheral:fakePeripheral
didDiscoverCharacteristicsForService:[[CBMutableService alloc] initWithType:serviceUUID
primary:YES]
error:nil];
[fakePeripheral.peripheralDelegate gnc_peripheral:fakePeripheral
didUpdateValueForCharacteristic:[[CBMutableCharacteristic alloc]
initWithType:characteristicUUID
properties:0
value:nil
permissions:0]
error:nil];
// Test to make sure unexpected delegate calls don't cause any issues from missing handlers.
XCTAssertNotNil(gattClient);
}
@end
@@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h"
#import "internal/platform/implementation/apple/Tests/GNCBLEGATTServer+Testing.h"
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h"
@@ -0,0 +1,60 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
NS_ASSUME_NONNULL_BEGIN
/** A fake implementation of @c GNCPeripheral to inject for testing. */
@interface GNCFakePeripheral : NSObject <GNCPeripheral>
/**
* Similates a @c discoverServices: error.
*
* Setting this error to a value other than @c nil will simulate a failure when calling
* @c discoverServices: and will call the
* @c gnc_peripheral:didDiscoverServices: delegate method with the provided
* error.
*/
@property(nonatomic, nullable, readwrite) NSError *discoverServicesError;
/**
* Similates a @c discoverCharacteristics:forService: error.
*
* Setting this error to a value other than @c nil will simulate a failure when calling
* @c discoverCharacteristics:forService: and will call the
* @c gnc_peripheral:didDiscoverCharacteristicsForService:error: delegate method with the provided
* error.
*/
@property(nonatomic, nullable, readwrite) NSError *discoverCharacteristicsForServiceError;
/**
* Similates a @c readValueForCharacteristic: error.
*
* Setting this error to a value other than @c nil will simulate a failure when calling
* @c discoverCharacteristics:forService: and will call the
* @c gnc_peripheral:didUpdateValueForCharacteristic:error: delegate method with the provided
* error.
*/
@property(nonatomic, nullable, readwrite) NSError *readValueForCharacteristicError;
/** Similates a delay in all delegate calls by the specified amount. */
@property(nonatomic, readwrite) NSTimeInterval delegateDelay;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,118 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
NS_ASSUME_NONNULL_BEGIN
@interface CBService ()
// Change property to readwrite for tests.
@property(retain, readwrite, nullable) NSArray<CBCharacteristic *> *characteristics;
@end
@interface CBCharacteristic ()
// Change property to readwrite for tests.
@property(retain, readwrite, nullable) NSData *value;
@end
@implementation GNCFakePeripheral {
NSMutableArray<CBService *> *_services;
}
@synthesize peripheralDelegate;
- (instancetype)init {
self = [super init];
if (self) {
_services = [[NSMutableArray alloc] init];
}
return self;
}
- (nullable NSArray<CBService *> *)services {
return _services;
}
- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs {
[self delayDelegateUsingBlock:^() {
if (!_discoverServicesError) {
for (CBUUID *serviceUUID in serviceUUIDs) {
[_services addObject:[[CBMutableService alloc] initWithType:serviceUUID primary:YES]];
}
}
[peripheralDelegate gnc_peripheral:self didDiscoverServices:_discoverServicesError];
}];
}
- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)characteristicUUIDs
forService:(CBService *)service {
[self delayDelegateUsingBlock:^() {
if (!_discoverCharacteristicsForServiceError) {
NSMutableArray<CBCharacteristic *> *characteristics = service.characteristics.mutableCopy;
if (!characteristics) {
characteristics = [NSMutableArray array];
}
for (CBUUID *characteristicUUID in characteristicUUIDs) {
[characteristics addObject:[[CBMutableCharacteristic alloc]
initWithType:characteristicUUID
properties:CBCharacteristicPropertyRead
value:nil
permissions:CBAttributePermissionsReadable]];
}
service.characteristics = characteristics;
}
[peripheralDelegate gnc_peripheral:self
didDiscoverCharacteristicsForService:service
error:_discoverCharacteristicsForServiceError];
}];
}
- (void)readValueForCharacteristic:(CBCharacteristic *)characteristic {
[self delayDelegateUsingBlock:^() {
if (!_readValueForCharacteristicError) {
characteristic.value = [NSData data];
}
[peripheralDelegate gnc_peripheral:self
didUpdateValueForCharacteristic:characteristic
error:_readValueForCharacteristicError];
}];
}
- (void)delayDelegateUsingBlock:(void (^)())block {
if (_delegateDelay <= 0) {
block();
} else {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, _delegateDelay * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
block();
});
}
}
@end
NS_ASSUME_NONNULL_END