diff --git a/internal/platform/ble_v2.h b/internal/platform/ble_v2.h index af123012..c5fd3d1d 100644 --- a/internal/platform/ble_v2.h +++ b/internal/platform/ble_v2.h @@ -168,7 +168,7 @@ class GattServer final { return impl_->UpdateCharacteristic(characteristic, value); } - void Stop() { return impl_->Stop(); } + void Stop() { if (impl_) return impl_->Stop(); } // Returns true if a gatt_server is usable. If this method returns false, // it is not safe to call any other method. diff --git a/internal/platform/implementation/ble_v2.h b/internal/platform/implementation/ble_v2.h index f9162f2b..0393a0db 100644 --- a/internal/platform/implementation/ble_v2.h +++ b/internal/platform/implementation/ble_v2.h @@ -15,6 +15,7 @@ #ifndef PLATFORM_API_BLE_V2_H_ #define PLATFORM_API_BLE_V2_H_ +#include #include #include #include @@ -113,14 +114,24 @@ struct GattCharacteristic { Uuid uuid; Uuid service_uuid; + std::vector permissions; + std::vector properties; // Hashable template friend H AbslHashValue(H h, const GattCharacteristic& s) { - return H::combine(std::move(h), s.uuid, s.service_uuid); + return H::combine(std::move(h), s.uuid, s.service_uuid, s.permissions, + s.properties); } bool operator==(const GattCharacteristic& rhs) const { - return this->uuid == rhs.uuid && this->service_uuid == rhs.service_uuid; + bool has_equal_permissions = + std::is_permutation(this->permissions.begin(), this->permissions.end(), + rhs.permissions.begin(), rhs.permissions.end()); + bool has_equal_properties = + std::is_permutation(this->properties.begin(), this->properties.end(), + rhs.properties.begin(), rhs.properties.end()); + return this->uuid == rhs.uuid && this->service_uuid == rhs.service_uuid && + has_equal_permissions && has_equal_properties; } }; diff --git a/internal/platform/implementation/ios/BUILD b/internal/platform/implementation/ios/BUILD index 8d2f4a5b..4ca78ac2 100644 --- a/internal/platform/implementation/ios/BUILD +++ b/internal/platform/implementation/ios/BUILD @@ -66,7 +66,6 @@ objc_library( "GNCUtils.h", ], deps = [ - "//third_party/objective_c/google_toolbox_for_mac:GTM_StringEncoding", "@aappleby_smhasher//:libmurmur3", ], ) diff --git a/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h b/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h index 1c515441..5eef0133 100644 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h +++ b/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#import + +@class CBCharacteristic; +@class CBUUID; NS_ASSUME_NONNULL_BEGIN @@ -26,16 +29,43 @@ NS_ASSUME_NONNULL_BEGIN */ @interface GNCMBlePeripheral : NSObject -- (instancetype)init NS_UNAVAILABLE; +- (instancetype)init NS_DESIGNATED_INITIALIZER; /** - * Initializes an `GNCMBlePeripheral` object. + * Adds GATT CBService. + * + * @param serviceUUID A GATT service ID to advertise for. + */ +- (void)addCBServiceWithUUID:(CBUUID *)serviceUUID; + +/** + * Adds GATT CBCharacteristic. + * + * @param characteristic A characteristic CBUUID. + */ +- (void)addCharacteristic:(CBCharacteristic *)characteristic; + +/** + * Updates GATT CBCharacteristic with value. + * + * @param value The NSData to advertise. + * @param characteristicUUID A characteristic CBUUID. + */ +- (void)updateValue:(NSData *)value forCharacteristic:(CBUUID *)characteristicUUID; + +/** + * Stops GATT server service. + */ +- (void)stopGATTService; + +/** + * Starts advertising with service UUID and advertisement data. * * @param serviceUUID A string that uniquely identifies the advertised service to search for. * @param advertisementData The data to advertise. */ -- (instancetype)initWithServiceUUID:(NSString *)serviceUUID - advertisementData:(NSData *)advertisementData NS_DESIGNATED_INITIALIZER; +- (BOOL)startAdvertisingWithServiceUUID:(NSString *)serviceUUID + advertisementData:(NSData *)advertisementData; @end diff --git a/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m b/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m index f3fcdc2a..14e79461 100644 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m +++ b/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m @@ -14,34 +14,41 @@ #import "internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h" -#include +#import #import "internal/platform/implementation/ios/GNCUtils.h" NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSUInteger, GNCMPeripheralState) { + GNCMPeripheralStateStopped, + GNCMPeripheralStateAdvertising, +}; + @interface GNCMBlePeripheral () @end @implementation GNCMBlePeripheral { - /** GATT service for advertisement. */ + /** Service UUID for advertisement. */ CBMutableService *_advertisementService; + /** GATT service for GATT connection. */ + CBMutableService *_GATTService; + /** GATT characteristics for GATT connection. */ + NSMutableArray *_gattCharacteristics; + /** CBUUID characteristic to NSData value dictionary for GATT connection. */ + NSMutableDictionary *_gattCharacteristicValues; /** Data to be advertised. */ NSData *_advertisementData; /** Peripheral manager used to advertise or connect to peripherals. */ CBPeripheralManager *_peripheralManager; /** Serial background queue for |peripheralManager|. */ dispatch_queue_t _selfQueue; + /** Peripheral state for stop or advertising. */ + GNCMPeripheralState _state; } -- (instancetype)initWithServiceUUID:(NSString *)serviceUUID - advertisementData:(NSData *)advertisementData { - self = [super init]; - if (self) { - _advertisementService = - [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:serviceUUID] primary:YES]; - _advertisementData = [advertisementData copy]; - +- (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("GNCPeripheralManagerQueue", DISPATCH_QUEUE_SERIAL); @@ -51,6 +58,7 @@ NS_ASSUME_NONNULL_BEGIN initWithDelegate:self queue:_selfQueue options:@{CBPeripheralManagerOptionShowPowerAlertKey : @NO}]; + _state = GNCMPeripheralStateStopped; } return self; } @@ -60,22 +68,68 @@ NS_ASSUME_NONNULL_BEGIN // dispatch_sync. This means delloc 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, ^{ - [self stopAdvertising]; + [self stopAdvertisingInternal]; }); } +- (void)addCBServiceWithUUID:(CBUUID *)serviceUUID { + if (!_GATTService) { + // If it has been called, then don't do it again. Initialize one time. + _GATTService = [[CBMutableService alloc] initWithType:serviceUUID primary:YES]; + _gattCharacteristics = [[NSMutableArray alloc] init]; + _gattCharacteristicValues = [[NSMutableDictionary alloc] init]; + } +} + +- (void)addCharacteristic:(CBCharacteristic *)characteristic { + if (_gattCharacteristics) { + [_gattCharacteristics addObject:characteristic]; + } + if (_gattCharacteristicValues) { + [_gattCharacteristicValues setObject:[[NSData alloc] init] forKey:characteristic.UUID]; + } +} + +- (void)updateValue:(NSData *)value forCharacteristic:(CBUUID *)characteristicUUID { + if ([_gattCharacteristicValues objectForKey:characteristicUUID]) { + [_gattCharacteristicValues setObject:value forKey:characteristicUUID]; + } +} + +- (void)stopGATTService { + if (!_GATTService) return; + dispatch_sync(_selfQueue, ^{ + [_peripheralManager removeService:_GATTService]; + }); +} + +- (BOOL)startAdvertisingWithServiceUUID:(NSString *)serviceUUID + advertisementData:(NSData *)advertisementData { + NSLog(@"[NEARBY] Client rquests startAdvertising"); + _advertisementService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:serviceUUID] + primary:YES]; + _advertisementData = [advertisementData copy]; + + if (_GATTService) { + if (_gattCharacteristics && _gattCharacteristics.count > 0) { + _GATTService.characteristics = _gattCharacteristics; + } + } + _state = GNCMPeripheralStateAdvertising; + return YES; +} + #pragma mark CBPeripheralManagerDelegate - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { - if (peripheral.state == CBManagerStatePoweredOn) { + NSLog(@"[NEARBY] peripheralManagerDidUpdateState %li", (long)peripheral.state); + if (peripheral.state == CBManagerStatePoweredOn && !peripheral.isAdvertising && + _state == GNCMPeripheralStateAdvertising) { NSLog(@"[NEARBY] CBPeripheralManager powered on; starting advertising"); - [_peripheralManager startAdvertising:@{ - CBAdvertisementDataServiceUUIDsKey : @[ _advertisementService.UUID ], - CBAdvertisementDataLocalNameKey : _advertisementData - }]; + [self startAdvertisingInternal]; } else { NSLog(@"[NEARBY] CBPeripheralManager not powered on; stopping advertising"); - [self stopAdvertising]; + [self stopAdvertisingInternal]; } } @@ -85,7 +139,7 @@ NS_ASSUME_NONNULL_BEGIN NSLog(@"[NEARBY] Error starting advertising: %@,", [error localizedDescription]); return; } - if (_peripheralManager.state != CBPeripheralManagerStatePoweredOn) { + if (_peripheralManager.state != CBManagerStatePoweredOn) { NSLog(@"[NEARBY] Error starting advertising: peripheral manager not on!"); return; } @@ -93,11 +147,53 @@ NS_ASSUME_NONNULL_BEGIN NSLog(@"[NEARBY] Peripheral manager started advertising"); } +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveReadRequest:(CBATTRequest *)request { + NSLog(@"[NEARBY] peripheralManager:didReceiveReadRequest"); + // This is called when a central asks to read a characteristic's value. + CBATTError error = CBATTErrorAttributeNotFound; + NSData *value = _gattCharacteristicValues[request.characteristic.UUID]; + if (value != nil && value.length > 0) { + if (request.offset > value.length) { + error = CBATTErrorInvalidOffset; + } else { + // Reply with the advertisement data. + NSRange rangeFromOffset = NSMakeRange(request.offset, value.length - request.offset); + request.value = [value subdataWithRange:rangeFromOffset]; + error = CBATTErrorSuccess; + } + } + [_peripheralManager respondToRequest:request withResult:error]; +} + #pragma mark Private +/** Signals the peripheral manager to start advertising. Must be called on _selfQueue */ +- (void)startAdvertisingInternal { + if (![_peripheralManager isAdvertising]) { + NSLog(@"[NEARBY] startAdvertisingInternal"); + + if (_GATTService) { + [_peripheralManager addService:_GATTService]; + } + [_peripheralManager startAdvertising:@{ + CBAdvertisementDataServiceUUIDsKey : @[ _advertisementService.UUID ], + CBAdvertisementDataLocalNameKey : _advertisementData + }]; + } +} + /** Signals the peripheral manager to stop advertising. Must be called on _selfQueue */ -- (void)stopAdvertising { - [_peripheralManager stopAdvertising]; +- (void)stopAdvertisingInternal { + if ([_peripheralManager isAdvertising]) { + NSLog(@"[NEARBY] stopAdvertisingInternal"); + _state = GNCMPeripheralStateStopped; + + if (_GATTService) { + [_peripheralManager removeService:_GATTService]; + } + [_peripheralManager stopAdvertising]; + } } @end diff --git a/internal/platform/implementation/ios/Tests/GNCBleTest.mm b/internal/platform/implementation/ios/Tests/GNCBleTest.mm index 67c0c851..70cc8b5e 100644 --- a/internal/platform/implementation/ios/Tests/GNCBleTest.mm +++ b/internal/platform/implementation/ios/Tests/GNCBleTest.mm @@ -28,6 +28,7 @@ using ::location::nearby::api::BluetoothAdapter; using ::location::nearby::api::ImplementationPlatform; 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; static const char *const kAdvertisementString = "\x0a\x0b\x0c\x0d"; @@ -74,4 +75,30 @@ static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh; XCTAssertTrue(_ble->StopScanning()); } +- (void)testGattServerWorking { + // Test creating gatt_server. + auto gatt_server = _ble->StartGattServer(/*ServerGattConnectionCallback=*/{}); + XCTAssert(gatt_server != nullptr); + + // 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}; + + // NOLINTNEXTLINE + absl::optional gatt_characteristic = + gatt_server->CreateCharacteristic(service_uuid, characteristic_uuid, permissions, properties); + XCTAssertTrue(gatt_characteristic.has_value()); + + // Test updating characteristic. + ByteArray any_byte("any"); + XCTAssertTrue( + gatt_server->UpdateCharacteristic(gatt_characteristic.value(), any_byte)); + + gatt_server->Stop(); +} + @end diff --git a/internal/platform/implementation/ios/ble.h b/internal/platform/implementation/ios/ble.h index ce0d0e28..1ce33a2f 100644 --- a/internal/platform/implementation/ios/ble.h +++ b/internal/platform/implementation/ios/ble.h @@ -15,10 +15,12 @@ #ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_IOS_BLE_H_ #define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_IOS_BLE_H_ +#import #import #include +#include "absl/types/optional.h" #include "internal/platform/cancellation_flag.h" #include "internal/platform/implementation/ble_v2.h" #include "internal/platform/implementation/bluetooth_adapter.h" @@ -56,6 +58,25 @@ class BleMedium : public api::ble_v2::BleMedium { bool IsExtendedAdvertisementsAvailable() override; private: + // A concrete implemenation for GattServer. + class GattServer : public api::ble_v2::GattServer { + public: + GattServer() = default; + explicit GattServer(GNCMBlePeripheral* peripheral) : peripheral_(peripheral) {} + + absl::optional CreateCharacteristic( + const Uuid& service_uuid, const Uuid& characteristic_uuid, + const std::vector& permissions, + const std::vector& properties) override; + + bool UpdateCharacteristic(const api::ble_v2::GattCharacteristic& characteristic, + const location::nearby::ByteArray& value) override; + void Stop() override; + + private: + GNCMBlePeripheral* peripheral_; + }; + BluetoothAdapter* adapter_; GNCMBlePeripheral* peripheral_; GNCMBleCentral* central_; diff --git a/internal/platform/implementation/ios/ble.mm b/internal/platform/implementation/ios/ble.mm index 1c149138..ad2e2dd6 100644 --- a/internal/platform/implementation/ios/ble.mm +++ b/internal/platform/implementation/ios/ble.mm @@ -27,6 +27,51 @@ namespace location { namespace nearby { namespace ios { +namespace { + +CBAttributePermissions PermissionToCBPermissions( + const std::vector& permissions) { + CBAttributePermissions characteristPermissions = 0; + for (const auto& permission : permissions) { + switch (permission) { + case api::ble_v2::GattCharacteristic::Permission::kRead: + characteristPermissions |= CBAttributePermissionsReadable; + break; + case api::ble_v2::GattCharacteristic::Permission::kWrite: + characteristPermissions |= CBAttributePermissionsWriteable; + break; + case api::ble_v2::GattCharacteristic::Permission::kLast: + case api::ble_v2::GattCharacteristic::Permission::kUnknown: + default:; // fall through + } + } + return characteristPermissions; +} + +CBCharacteristicProperties PropertiesToCBProperties( + const std::vector& properties) { + CBCharacteristicProperties characteristicProperties = 0; + for (const auto& property : properties) { + switch (property) { + case api::ble_v2::GattCharacteristic::Property::kRead: + characteristicProperties |= CBCharacteristicPropertyRead; + break; + case api::ble_v2::GattCharacteristic::Property::kWrite: + characteristicProperties |= CBCharacteristicPropertyWrite; + break; + case api::ble_v2::GattCharacteristic::Property::kIndicate: + characteristicProperties |= CBCharacteristicPropertyIndicate; + break; + case api::ble_v2::GattCharacteristic::Property::kLast: + case api::ble_v2::GattCharacteristic::Property::kUnknown: + default:; // fall through + } + } + return characteristicProperties; +} + +} // namespace + using ::location::nearby::api::ble_v2::BleAdvertisementData; using ::location::nearby::api::ble_v2::TxPowerLevel; using ScanCallback = ::location::nearby::api::ble_v2::BleMedium::ScanCallback; @@ -40,12 +85,15 @@ bool BleMedium::StartAdvertising( if (advertising_data.service_data.empty()) { return false; } - const std::string& service_uuid = advertising_data.service_data.begin()->first.Get16BitAsString(); + const auto& service_uuid = advertising_data.service_data.begin()->first.Get16BitAsString(); const ByteArray& service_data_bytes = advertising_data.service_data.begin()->second; - peripheral_ = - [[GNCMBlePeripheral alloc] initWithServiceUUID:ObjCStringFromCppString(service_uuid) - advertisementData:NSDataFromByteArray(service_data_bytes)]; + if (!peripheral_) { + peripheral_ = [[GNCMBlePeripheral alloc] init]; + } + + [peripheral_ startAdvertisingWithServiceUUID:ObjCStringFromCppString(service_uuid) + advertisementData:NSDataFromByteArray(service_data_bytes)]; return true; } @@ -72,7 +120,10 @@ bool BleMedium::StopScanning() { std::unique_ptr BleMedium::StartGattServer( api::ble_v2::ServerGattConnectionCallback callback) { - return nullptr; + if (!peripheral_) { + peripheral_ = [[GNCMBlePeripheral alloc] init]; + } + return std::make_unique(peripheral_); } std::unique_ptr BleMedium::ConnectToGattServer( @@ -95,6 +146,42 @@ std::unique_ptr BleMedium::Connect(const std::string& se bool BleMedium::IsExtendedAdvertisementsAvailable() { return false; } +// NOLINTNEXTLINE +absl::optional BleMedium::GattServer::CreateCharacteristic( + const Uuid& service_uuid, const Uuid& characteristic_uuid, + const std::vector& permissions, + const std::vector& properties) { + api::ble_v2::GattCharacteristic characteristic = {.uuid = characteristic_uuid, + .service_uuid = service_uuid, + .permissions = permissions, + .properties = properties}; + [peripheral_ + addCBServiceWithUUID:[CBUUID + UUIDWithString:ObjCStringFromCppString( + characteristic.service_uuid.Get16BitAsString())]]; + [peripheral_ + addCharacteristic:[[CBMutableCharacteristic alloc] + initWithType:[CBUUID UUIDWithString:ObjCStringFromCppString(std::string( + characteristic.uuid))] + properties:PropertiesToCBProperties(characteristic.properties) + value:nil + permissions:PermissionToCBPermissions(characteristic.permissions)]]; + return characteristic; +} + +bool BleMedium::GattServer::UpdateCharacteristic( + const api::ble_v2::GattCharacteristic& characteristic, + const location::nearby::ByteArray& value) { + [peripheral_ updateValue:NSDataFromByteArray(value) + forCharacteristic:[CBUUID UUIDWithString:ObjCStringFromCppString( + std::string(characteristic.uuid))]]; + return true; +} + +void BleMedium::GattServer::Stop() { + [peripheral_ stopGATTService]; +} + } // namespace ios } // namespace nearby } // namespace location