From 9a1ada9a56883cfc44964f565eac76e2e14d841e Mon Sep 17 00:00:00 2001 From: edwinwu Date: Wed, 15 Jun 2022 19:22:29 -0700 Subject: [PATCH] [BLE Refactor] Implement iOS Scanning modules. PiperOrigin-RevId: 455274324 --- internal/platform/ble_v2.cc | 5 +- .../ios/Mediums/Ble/GNCMBleCentral.h | 59 +++- .../ios/Mediums/Ble/GNCMBleCentral.m | 317 +++++++++++++++++- .../implementation/ios/Tests/GNCBleTest.mm | 19 +- internal/platform/implementation/ios/ble.h | 30 +- internal/platform/implementation/ios/ble.mm | 114 ++++++- .../implementation/ios/bluetooth_adapter.h | 6 + 7 files changed, 512 insertions(+), 38 deletions(-) diff --git a/internal/platform/ble_v2.cc b/internal/platform/ble_v2.cc index 6154e849..907e653e 100644 --- a/internal/platform/ble_v2.cc +++ b/internal/platform/ble_v2.cc @@ -53,13 +53,12 @@ bool BleV2Medium::StartScanning(const Uuid& service_uuid, [this](api::ble_v2::BlePeripheral& peripheral, BleAdvertisementData advertisement_data) { MutexLock lock(&mutex_); - if (peripherals_.contains(&peripheral)) { + if (!peripherals_.contains(&peripheral)) { NEARBY_LOGS(INFO) << "There is no need to callback due to peripheral impl=" << &peripheral << ", which already exists."; - return; + peripherals_.insert(&peripheral); } - peripherals_.insert(&peripheral); BleV2Peripheral proxy(&peripheral); NEARBY_LOGS(INFO) << "New peripheral imp=" << &peripheral diff --git a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h index 8d87ec92..44b472ce 100644 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h +++ b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h @@ -12,16 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" - #import +@class CBUUID; + NS_ASSUME_NONNULL_BEGIN /** * This handler is called on a discover when a nearby advertising endpoint is discovered. */ -typedef void (^GNCMScanResultHandler)(NSString *serviceUUID, NSData *serviceData); +typedef void (^GNCMScanResultHandler)(NSString *peripheralID, NSData *serviceData); + +/** + * This handler is called on a discovery when a nearby advertising endpoint is connected. + */ +typedef void (^GNCMGATTConnectionResultHandler)(NSError *_Nullable error); + +/** + * This handler is called on a discovery when a nearby advertising endpoint is connected. The input + * is a discovered map of characteristic values. + */ +typedef void (^GNCMGATTDiscoverResultHandler)( + NSDictionary *_Nullable characteristicValues); /** * GNCMBleCentral discovers devices advertising the specified service UUID via BLE (using the @@ -33,18 +45,49 @@ typedef void (^GNCMScanResultHandler)(NSString *serviceUUID, NSData *serviceData */ @interface GNCMBleCentral : NSObject -- (instancetype)init NS_UNAVAILABLE; +- (instancetype)init; /** - * Initializes an `GNCMBleCentral` object. + * Starts scanning with service UUID. * * @param serviceUUID A string that uniquely identifies the scanning services to search for. * @param scanResultHandler The handler that is called when an endpoint advertising the service * UUID is discovered. */ -- (instancetype)initWithServiceUUID:(NSString *)serviceUUID - scanResultHandler:(GNCMScanResultHandler)scanResultHandler - NS_DESIGNATED_INITIALIZER; +- (BOOL)startScanningWithServiceUUID:(NSString *)serviceUUID + scanResultHandler:(GNCMScanResultHandler)scanResultHandler; + +/** + * Sets up a GATT connection. + * + * @param peripheralID A string that uniquely identifies the peripheral. + * @param gattConnectionResultHandler The handler that is called when an endpoint is connected. + */ +- (void)connectGattServerWithPeripheralID:(NSString *)peripheralID + gattConnectionResultHandler: + (GNCMGATTConnectionResultHandler)gattConnectionResultHandler; + +/** + * Discovers GATT service and its associated characteristics with values. + * + * @param serviceUUID A CBUUID for service to discover. + * @param gattCharacteristics Array of CBUUID for characteristic to discover. + * @param peripheralID A string that uniquely identifies the peripheral. + * @param gattDiscoverResultHandler This handler is called on a discovery for a discovered map of + * characteristic values when a nearby advertising endpoint is + * connected. + */ +- (void)discoverGattService:(CBUUID *)serviceUUID + gattCharacteristics:(NSArray *)characteristicUUIDs + peripheralID:(NSString *)peripheralID + gattDiscoverResultHandler:(GNCMGATTDiscoverResultHandler)gattDiscoverResultHandler; + +/** + * Disconnects GATT connection. + * + * @param peripheralID A string that uniquely identifies the peripheral. + */ +- (void)disconnectGattServiceWithPeripheralID:(NSString *)peripheralID; @end diff --git a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.m b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.m index 5feb3598..ad6652b7 100644 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.m +++ b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.m @@ -14,12 +14,63 @@ #import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h" -#include +#import #import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSUInteger, GNCMCentralState) { + GNCMCentralStateStopped, + GNCMCentralStateScanning, +}; + +typedef void (^GNCMBleCharacteristicsHandler)(NSArray *characteristics, + NSError *error); +typedef void (^GNCMBleCharacteristicValueHandler)(CBCharacteristic *characteristic, NSError *error); +typedef void (^GNCIntHandler)(int); + +/** This lets a GNCIntHandler call itself. */ +GNCIntHandler GNCRecursiveIntHandler(void (^block)(GNCIntHandler blockSelf, int i)) { + return ^(int i) { + return block(GNCRecursiveIntHandler(block), i); + }; +} + +/** This represents a discovered peripheral. */ +@interface GNCMPeripheralInfo : NSObject +@property(nonatomic) CBPeripheral *peripheral; + +/** Called when characteristics are discovered. */ +@property(nonatomic, nullable) GNCMBleCharacteristicsHandler charsHandler; + +/** Called when a characteristic value is read. */ +@property(nonatomic, nullable) GNCMBleCharacteristicValueHandler charValueHandler; + +@end + +@implementation GNCMPeripheralInfo + +- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral { + self = [super init]; + if (self) { + _peripheral = peripheral; + } + return self; +} + +- (BOOL)isEqual:(id)object { + // There is always exactly one info object per peripheral, so compare by identity. This is needed + // for maintenance of the peripherals stored in multiple maps. + return self == object; +} + +- (NSUInteger)hash { + return (NSUInteger)self; +} + +@end + @interface GNCMBleCentral () @end @@ -32,22 +83,36 @@ NS_ASSUME_NONNULL_BEGIN CBCentralManager *_centralManager; /** Serial background queue for |centralManager|. */ dispatch_queue_t _selfQueue; + /** Central state for stop or scanning. */ + GNCMCentralState _state; + /** The dictionary keyed by CBPeripheral identifier to value GNCMPeripheralInfo. */ + NSMutableDictionary *_nearbyPeripheralsByID; + /** Array of characteristic UUID used for discovering. */ + NSArray *_characteristicUUIDs; + /** GATT connection result handler. */ + GNCMGATTConnectionResultHandler _gattConnectionResultHandler; + /** GATT service and characteristic discovery result hanadler. */ + GNCMGATTDiscoverResultHandler _gattDiscoverResultHandler; + /** The discovered characteristic values map used to callback for `_gattDiscoverResultHandler`. */ + NSMutableDictionary *_gattCharacteristicValues; } -- (instancetype)initWithServiceUUID:(NSString *)serviceUUID - scanResultHandler:(GNCMScanResultHandler)scanResultHandler { - self = [super init]; - if (self) { - _serviceUUID = [CBUUID UUIDWithString:serviceUUID]; - _scanResultHandler = scanResultHandler; - +- (instancetype)init { + if (self = [super init]) { // To make this class thread-safe, use a serial queue for all state changes, and have Core // Bluetooth also use this queue. _selfQueue = dispatch_queue_create("GNCCentralManagerQueue", DISPATCH_QUEUE_SERIAL); + + _nearbyPeripheralsByID = [NSMutableDictionary dictionary]; + _gattCharacteristicValues = [NSMutableDictionary dictionary]; + + // Set up the central manager for scanning. _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:_selfQueue options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}]; + + _state = GNCMCentralStateStopped; } return self; } @@ -57,20 +122,248 @@ NS_ASSUME_NONNULL_BEGIN // dispatch_sync. This means dealloc must be called from an external queue, which means |self| // must never be captured by any escaping block used in this class. dispatch_sync(_selfQueue, ^{ - [_centralManager stopScan]; + [self stopScanningInternal]; + }); +} + +- (BOOL)startScanningWithServiceUUID:(NSString *)serviceUUID + scanResultHandler:(GNCMScanResultHandler)scanResultHandler { + _serviceUUID = [CBUUID UUIDWithString:serviceUUID]; + _scanResultHandler = scanResultHandler; + _state = GNCMCentralStateScanning; + return YES; +} + +- (void)connectGattServerWithPeripheralID:(NSString *)peripheralID + gattConnectionResultHandler: + (GNCMGATTConnectionResultHandler)gattConnectionResultHandler { + _gattConnectionResultHandler = gattConnectionResultHandler; + dispatch_sync(_selfQueue, ^{ + GNCMPeripheralInfo *peripheralInfo = + _nearbyPeripheralsByID[[[NSUUID alloc] initWithUUIDString:peripheralID]]; + if (!peripheralInfo) return; + [_centralManager connectPeripheral:peripheralInfo.peripheral options:nil]; + }); +} + +- (void)discoverGattService:(CBUUID *)serviceUUID + gattCharacteristics:(NSArray *)characteristicUUIDs + peripheralID:(NSString *)peripheralID + gattDiscoverResultHandler:(GNCMGATTDiscoverResultHandler)gattDiscoverResultHandler { + _gattDiscoverResultHandler = gattDiscoverResultHandler; + [_gattCharacteristicValues removeAllObjects]; + dispatch_sync(_selfQueue, ^{ + GNCMPeripheralInfo *peripheralInfo = + _nearbyPeripheralsByID[[[NSUUID alloc] initWithUUIDString:peripheralID]]; + if (!peripheralInfo) return; + _characteristicUUIDs = [characteristicUUIDs copy]; + + // Start to discover service and the delegate will get its characteristics and read their values + // recursively + [peripheralInfo.peripheral discoverServices:@[ serviceUUID ]]; + }); +} + +- (void)disconnectGattServiceWithPeripheralID:(NSString *)peripheralID { + dispatch_sync(_selfQueue, ^{ + GNCMPeripheralInfo *peripheralInfo = + _nearbyPeripheralsByID[[[NSUUID alloc] initWithUUIDString:peripheralID]]; + if (!peripheralInfo) return; + _gattConnectionResultHandler = nil; + _gattDiscoverResultHandler = nil; + [_centralManager cancelPeripheralConnection:peripheralInfo.peripheral]; }); } #pragma mark CBCentralManagerDelegate - (void)centralManagerDidUpdateState:(CBCentralManager *)central { - if (central.state == CBManagerStatePoweredOn) { + if (central.state == CBManagerStatePoweredOn && !central.isScanning && + _state == GNCMCentralStateScanning) { NSLog(@"[NEARBY] CBCentralManager powered on; starting scan"); + [self startScanningInternal]; + } else { + NSLog(@"[NEARBY] CBCentralManager not powered on; stopping scan"); + [self stopScanningInternal]; + } +} + +- (void)centralManager:(CBCentralManager *)central + didDiscoverPeripheral:(CBPeripheral *)peripheral + advertisementData:(NSDictionary *)advertisementData + RSSI:(NSNumber *)RSSI { + NSNumber *connectable = advertisementData[CBAdvertisementDataIsConnectable]; + if (![connectable boolValue]) return; + + // Look for the NC advertisement header in either the service data (from non-iOS) or the + // advertised name (from iOS). + NSData *serviceData = advertisementData[CBAdvertisementDataServiceDataKey][_serviceUUID] + ?: advertisementData[CBAdvertisementDataLocalNameKey]; + + // Try to look up the peripheral by ID. + GNCMPeripheralInfo *info = _nearbyPeripheralsByID[peripheral.identifier]; + if (!info) { + NSLog(@"[NEARBY] New peripheral: %@", peripheral); + // This is a new peripheral, so create a new peripheral info object. + info = [[GNCMPeripheralInfo alloc] initWithPeripheral:peripheral]; + } else { + info.peripheral = peripheral; + } + + _nearbyPeripheralsByID[peripheral.identifier] = info; + _scanResultHandler(peripheral.identifier.UUIDString, serviceData); +} + +- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { + NSLog(@"[NEARBY] Connected to peripheral: %@", peripheral); + peripheral.delegate = self; + // Tell the caller the connection is done. + _gattConnectionResultHandler(nil); +} + +- (void)centralManager:(CBCentralManager *)central + didFailToConnectPeripheral:(CBPeripheral *)peripheral + error:(nullable NSError *)error { + NSLog(@"[NEARBY] Failed to connect to peripheral: %@, error: %@", peripheral, error); + // Tell the caller the connection failed. + _gattConnectionResultHandler(error); +} + +- (void)centralManager:(CBCentralManager *)central + didDisconnectPeripheral:(CBPeripheral *)peripheral + error:(nullable NSError *)error { + NSLog(@"[NEARBY] Disconnected to peripheral: %@, error: %@", peripheral, error); +} + +#pragma mark CBPeripheralDelegate + +- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error { + GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peripheral.identifier]; + if (!peripheralInfo) return; + if (error || (peripheral.services.count == 0)) { + NSLog(@"[NEARBY] Error reading advertisement: unable to discover services."); + _gattDiscoverResultHandler(nil); + } else { + NSLog(@"[NEARBY] Discovered services for %@: %@", peripheral.name, peripheral.services); + + // Helper functions for discovering characteristics and reading their values. + void (^discoverChars)(CBService *, GNCMBleCharacteristicsHandler) = + ^(CBService *service, GNCMBleCharacteristicsHandler handler) { + NSAssert(!peripheralInfo.charsHandler, @"Unexpected characteristic handler"); + peripheralInfo.charsHandler = handler; + + // Discover all characteristics that may contain the advertisement. + [peripheral discoverCharacteristics:_characteristicUUIDs forService:service]; + }; + void (^readCharValue)(CBCharacteristic *, GNCMBleCharacteristicValueHandler) = + ^(CBCharacteristic *characteristic, GNCMBleCharacteristicValueHandler handler) { + NSAssert(!peripheralInfo.charValueHandler, @"Unexpected characteristic value handler"); + peripheralInfo.charValueHandler = handler; + [peripheral readValueForCharacteristic:characteristic]; + }; + + // Multiple services may have the same UUID, so find the right service by searching for the + // characteristic containing the advertisement with a matching service ID hash. + __weak __typeof__(self) weakSelf = self; + void (^tryService)(int) = GNCRecursiveIntHandler(^(GNCIntHandler tryService, int serviceIndex) { + __strong __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) return; + + // We've tried all the services, report the discovered characteristics and their values. + if (serviceIndex == peripheral.services.count) { + NSLog(@"[NEARBY] Done traversing all services or no services to traverse."); + _gattDiscoverResultHandler(_gattCharacteristicValues); + return; + } + + NSLog(@"[NEARBY] Trying service: %@", peripheral.services[serviceIndex]); + discoverChars( + peripheral.services[serviceIndex], ^(NSArray *chars, NSError *error) { + void (^tryNextService)() = ^{ + tryService(serviceIndex + 1); + }; + + // If there was an error or there are no characteristics on this service, try next one. + if (error || (chars.count == 0)) { + tryNextService(); + return; + } + + // Read each characteristic. + void (^tryChar)(int) = GNCRecursiveIntHandler(^(GNCIntHandler tryChar, int charIndex) { + // We've tried all characteristics on this service without error, try next service. + if (charIndex == chars.count) { + NSLog(@"[NEARBY] No matching advertisement found"); + tryNextService(); + return; + } + + NSLog(@"[NEARBY] Trying characteristic: %@", chars[charIndex]); + readCharValue(chars[charIndex], ^(CBCharacteristic *characteristic, NSError *error) { + if (error) { + tryNextService(); + } else { + // We've found the characteristic and its non-nil value. Store it. + if (characteristic.value.length != 0) { + [_gattCharacteristicValues setObject:characteristic.value + forKey:characteristic.UUID]; + } + tryChar(charIndex + 1); + } + }); + }); + + // Start searching the characteristics for the current service. + tryChar(0); + }); + }); + + // Start searching the services. + tryService(0); + } +} + +- (void)peripheral:(CBPeripheral *)peripheral + didDiscoverCharacteristicsForService:(CBService *)service + error:(nullable NSError *)error { + NSLog(@"[NEARBY] Discovered characteristics: %@ error: %@", service.characteristics, error); + GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peripheral.identifier]; + if (!peripheralInfo) return; + GNCMBleCharacteristicsHandler charsHandler = peripheralInfo.charsHandler; + peripheralInfo.charsHandler = nil; + charsHandler(service.characteristics, error); +} + +- (void)peripheral:(CBPeripheral *)peripheral + didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic + error:(nullable NSError *)error { + NSLog(@"[NEARBY] Read characteristic value: %@ error: %@", characteristic, error); + GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peripheral.identifier]; + if (!peripheralInfo) return; + GNCMBleCharacteristicValueHandler valueHandler = peripheralInfo.charValueHandler; + peripheralInfo.charValueHandler = nil; + valueHandler(characteristic, error); +} + +#pragma mark Private + +/** Signals the central manager to start scanning. Must be called on _selfQueue */ +- (void)startScanningInternal { + if (![_centralManager isScanning]) { + NSLog(@"[NEARBY] startScanningInternal"); + [_centralManager scanForPeripheralsWithServices:@[ _serviceUUID ] options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}]; - } else { - NSLog(@"[NEARBY] CBCentralManager not powered on; stopping scan"); + } +} + +/** Signals the central manager to stop scanning. Must be called on _selfQueue */ +- (void)stopScanningInternal { + if ([_centralManager isScanning]) { + NSLog(@"[NEARBY] stopScanningInternal"); + _state = GNCMCentralStateStopped; + [_centralManager stopScan]; } } diff --git a/internal/platform/implementation/ios/Tests/GNCBleTest.mm b/internal/platform/implementation/ios/Tests/GNCBleTest.mm index 70cc8b5e..cb4b2b3e 100644 --- a/internal/platform/implementation/ios/Tests/GNCBleTest.mm +++ b/internal/platform/implementation/ios/Tests/GNCBleTest.mm @@ -13,6 +13,7 @@ // limitations under the License. #import +#include "internal/platform/implementation/ios/bluetooth_adapter.h" #include #include @@ -30,6 +31,7 @@ using ::location::nearby::api::ble_v2::BleAdvertisementData; using ::location::nearby::api::ble_v2::BleMedium; using ::location::nearby::api::ble_v2::GattCharacteristic; using ::location::nearby::api::ble_v2::TxPowerLevel; +using IOSBluetoothAdapter = ::location::nearby::ios::BluetoothAdapter; static const char *const kAdvertisementString = "\x0a\x0b\x0c\x0d"; static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh; @@ -37,6 +39,7 @@ static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh; @interface GNCBleTest : XCTestCase @end +// TODO(b/222392304): More tests on GNCBleTest. @implementation GNCBleTest { std::unique_ptr _adapter; std::unique_ptr _ble; @@ -83,10 +86,8 @@ static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh; // Test creating characteristic. Uuid service_uuid(1234, 5678); Uuid characteristic_uuid(5678, 1234); - std::vector permissions = { - GattCharacteristic::Permission::kRead}; - std::vector properties = { - GattCharacteristic::Property::kRead}; + std::vector permissions = {GattCharacteristic::Permission::kRead}; + std::vector properties = {GattCharacteristic::Property::kRead}; // NOLINTNEXTLINE absl::optional gatt_characteristic = @@ -95,10 +96,16 @@ static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh; // Test updating characteristic. ByteArray any_byte("any"); - XCTAssertTrue( - gatt_server->UpdateCharacteristic(gatt_characteristic.value(), any_byte)); + XCTAssertTrue(gatt_server->UpdateCharacteristic(gatt_characteristic.value(), any_byte)); gatt_server->Stop(); } +- (void)testCreateGattClient { + IOSBluetoothAdapter *adapter = static_cast(_adapter.get()); + auto gatt_client = _ble->ConnectToGattServer(adapter->GetPeripheral(), kTxPowerLevel, {}); + + XCTAssert(gatt_client != nullptr); +} + @end diff --git a/internal/platform/implementation/ios/ble.h b/internal/platform/implementation/ios/ble.h index 1ce33a2f..58a76f18 100644 --- a/internal/platform/implementation/ios/ble.h +++ b/internal/platform/implementation/ios/ble.h @@ -77,10 +77,38 @@ class BleMedium : public api::ble_v2::BleMedium { GNCMBlePeripheral* peripheral_; }; + // A concrete implemenation for GattClient. + class GattClient : public api::ble_v2::GattClient { + public: + GattClient() = default; + explicit GattClient(GNCMBleCentral* central, const std::string& peripheral_id) + : central_(central), peripheral_id_(peripheral_id) {} + + bool DiscoverServiceAndCharacteristics(const Uuid& service_uuid, + const std::vector& characteristic_uuids) override; + + // NOLINTNEXTLINE + absl::optional GetCharacteristic( + const Uuid& service_uuid, const Uuid& characteristic_uuid) override; + + // NOLINTNEXTLINE + absl::optional ReadCharacteristic( + const api::ble_v2::GattCharacteristic& characteristic) override; + + bool WriteCharacteristic(const api::ble_v2::GattCharacteristic& characteristic, + const ByteArray& value) override; + + void Disconnect() override; + + private: + GNCMBleCentral* central_; + std::string peripheral_id_; + absl::flat_hash_map gatt_characteristic_values_; + }; + BluetoothAdapter* adapter_; GNCMBlePeripheral* peripheral_; GNCMBleCentral* central_; - dispatch_queue_t callback_queue_; }; } // namespace ios diff --git a/internal/platform/implementation/ios/ble.mm b/internal/platform/implementation/ios/ble.mm index ad2e2dd6..5893328d 100644 --- a/internal/platform/implementation/ios/ble.mm +++ b/internal/platform/implementation/ios/ble.mm @@ -104,11 +104,19 @@ bool BleMedium::StopAdvertising() { bool BleMedium::StartScanning(const Uuid& service_uuid, TxPowerLevel tx_power_level, ScanCallback scan_callback) { - central_ = [[GNCMBleCentral alloc] - initWithServiceUUID:ObjCStringFromCppString(service_uuid.Get16BitAsString()) - scanResultHandler:^(NSString* serviceUUID, NSData* serviceData){ - // TODO(b/228751356): Add scan callback implementation. - }]; + if (!central_) { + central_ = [[GNCMBleCentral alloc] init]; + } + + [central_ startScanningWithServiceUUID:ObjCStringFromCppString(service_uuid.Get16BitAsString()) + scanResultHandler:^(NSString* peripheralID, NSData* serviceData) { + BleAdvertisementData advertisement_data; + advertisement_data.service_data = { + {service_uuid, ByteArrayFromNSData(serviceData)}}; + BlePeripheral& peripheral = adapter_->GetPeripheral(); + peripheral.SetPeripheralId(CppStringFromObjCString(peripheralID)); + scan_callback.advertisement_found_cb(peripheral, advertisement_data); + }]; return true; } @@ -129,7 +137,21 @@ std::unique_ptr BleMedium::StartGattServer( std::unique_ptr BleMedium::ConnectToGattServer( api::ble_v2::BlePeripheral& peripheral, TxPowerLevel tx_power_level, api::ble_v2::ClientGattConnectionCallback callback) { - return nullptr; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block NSError* connectedError; + BlePeripheral iosPeripheral = static_cast(peripheral); + std::string peripheral_id = iosPeripheral.GetPeripheralId(); + [central_ connectGattServerWithPeripheralID:ObjCStringFromCppString(peripheral_id) + gattConnectionResultHandler:^(NSError* _Nullable error) { + connectedError = error; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + + if (connectedError) { + return nullptr; + } + return std::make_unique(central_, peripheral_id); } std::unique_ptr BleMedium::OpenServerSocket( @@ -178,8 +200,84 @@ bool BleMedium::GattServer::UpdateCharacteristic( return true; } -void BleMedium::GattServer::Stop() { - [peripheral_ stopGATTService]; +void BleMedium::GattServer::Stop() { [peripheral_ stopGATTService]; } + +bool BleMedium::GattClient::DiscoverServiceAndCharacteristics( + const Uuid& service_uuid, const std::vector& characteristic_uuids) { + // Discover all characteristics that may contain the advertisement. + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + gatt_characteristic_values_.clear(); + CBUUID* serviceUUID = [CBUUID UUIDWithString:ObjCStringFromCppString(std::string(service_uuid))]; + + absl::flat_hash_map gatt_characteristics; + NSMutableArray* characteristicUUIDs = + [NSMutableArray arrayWithCapacity:characteristic_uuids.size()]; + for (const auto& characteristic_uuid : characteristic_uuids) { + [characteristicUUIDs addObject:[CBUUID UUIDWithString:ObjCStringFromCppString( + std::string(characteristic_uuid))]]; + gatt_characteristics.insert({std::string(characteristic_uuid), characteristic_uuid}); + } + + [central_ discoverGattService:serviceUUID + gattCharacteristics:characteristicUUIDs + peripheralID:ObjCStringFromCppString(peripheral_id_) + gattDiscoverResultHandler:^(NSDictionary* _Nullable characteristicValues) { + if (characteristicValues != nil) { + for (CBUUID* charUuid in characteristicValues) { + Uuid characteristic_uuid; + auto const& it = + gatt_characteristics.find(CppStringFromObjCString(charUuid.UUIDString)); + if (it == gatt_characteristics.end()) continue; + + api::ble_v2::GattCharacteristic characteristic = {.uuid = it->second, + .service_uuid = service_uuid}; + gatt_characteristic_values_.insert( + {characteristic, + ByteArrayFromNSData([characteristicValues objectForKey:charUuid])}); + } + } + + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + + if (gatt_characteristic_values_.empty()) { + return false; + } + return true; +} + +// NOLINTNEXTLINE +absl::optional BleMedium::GattClient::GetCharacteristic( + const Uuid& service_uuid, const Uuid& characteristic_uuid) { + api::ble_v2::GattCharacteristic characteristic = {.uuid = characteristic_uuid, + .service_uuid = service_uuid}; + auto const it = gatt_characteristic_values_.find(characteristic); + if (it == gatt_characteristic_values_.end()) { + return absl::nullopt; // NOLINT + } + return it->first; +} + +// NOLINTNEXTLINE +absl::optional BleMedium::GattClient::ReadCharacteristic( + const api::ble_v2::GattCharacteristic& characteristic) { + auto const it = gatt_characteristic_values_.find(characteristic); + if (it == gatt_characteristic_values_.end()) { + return absl::nullopt; // NOLINT + } + return it->second; +} + +bool BleMedium::GattClient::WriteCharacteristic( + const api::ble_v2::GattCharacteristic& characteristic, const ByteArray& value) { + // No op. + return false; +} + +void BleMedium::GattClient::Disconnect() { + [central_ disconnectGattServiceWithPeripheralID:ObjCStringFromCppString(peripheral_id_)]; } } // namespace ios diff --git a/internal/platform/implementation/ios/bluetooth_adapter.h b/internal/platform/implementation/ios/bluetooth_adapter.h index 82a08a83..8e7c4011 100644 --- a/internal/platform/implementation/ios/bluetooth_adapter.h +++ b/internal/platform/implementation/ios/bluetooth_adapter.h @@ -31,6 +31,11 @@ class BlePeripheral : public api::ble_v2::BlePeripheral { public: std::string GetAddress() const override; + std::string GetPeripheralId() const { return peripheral_id_; } + void SetPeripheralId(const std::string& peripheral_id) { + peripheral_id_ = peripheral_id; + } + private: // Only BluetoothAdapter may instantiate BlePeripheral. friend class BluetoothAdapter; @@ -38,6 +43,7 @@ class BlePeripheral : public api::ble_v2::BlePeripheral { explicit BlePeripheral(BluetoothAdapter* adapter) : adapter_(*adapter) {} BluetoothAdapter& adapter_; + std::string peripheral_id_; }; // Concrete BluetoothAdapter implementation.