| // Copyright 2016 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| #import "CBAdvertisingDriver.h" |
| #import "CBDriver.h" |
| #import "CBLog.h" |
| #import "CBUtil.h" |
| |
| static const void *kRotateAdDelay = &kRotateAdDelay; |
| |
| @interface CBAdvertisingDriver () |
| @property(nonatomic, assign) AdvertisingState advertisingState; |
| @property(nonatomic, assign) BOOL rotateAd; |
| @property(nonatomic, strong) NSDate *requestedAdvertisingStart; |
| /** advertisedServices is the same as the keys of services, but the ordering maintained here is |
| how we do this rotation business. */ |
| @property(nonatomic, strong) NSMutableArray<CBUUID *> *_Nonnull advertisedServices; |
| /** All added services by uuid */ |
| @property(nonatomic, strong) NSMutableDictionary<CBUUID *, CBMutableService *> *_Nonnull services; |
| @property(nonatomic, strong) |
| NSMutableDictionary<CBUUID *, CBAddServiceHandler> *_Nonnull addServiceHandlers; |
| @property(nonatomic, strong) NSMutableDictionary<CBUUID *, NSDictionary<CBUUID *, NSData *> *> |
| *_Nonnull serviceCharacteristics; |
| @end |
| |
| @implementation CBAdvertisingDriver |
| |
| - (id _Nullable)initWithQueue:(dispatch_queue_t _Nonnull)queue { |
| if (self = [super init]) { |
| self.queue = queue; |
| self.peripheral = [[CBPeripheralManager alloc] initWithDelegate:self queue:self.queue]; |
| self.services = [NSMutableDictionary new]; |
| self.serviceCharacteristics = [NSMutableDictionary new]; |
| self.addServiceHandlers = [NSMutableDictionary new]; |
| self.advertisedServices = [NSMutableArray new]; |
| self.advertisingState = AdvertisingStateNotAdvertising; |
| self.rotateAdDelay = 1; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| CBDispatchSync(self.queue, ^{ |
| self.peripheral.delegate = nil; |
| if (self.isHardwarePoweredOn) { |
| [self.peripheral stopAdvertising]; |
| [self.peripheral removeAllServices]; |
| } |
| }); |
| } |
| |
| - (void)addService:(CBUUID *_Nonnull)uuid |
| characteristics:(NSDictionary<CBUUID *, NSData *> *_Nonnull)characteristics |
| callback:(CBAddServiceHandler _Nonnull)handler { |
| // Allow shutdown in between to occur. |
| __weak typeof(self) this = self; |
| dispatch_async(self.queue, ^{ |
| if (!this) { |
| return; |
| } |
| if (this.services[uuid]) { |
| handler(uuid, [NSError errorWithDomain:kCBDriverErrorDomain |
| code:CBDriverErrorServiceAlreadyAdded |
| userInfo:@{ |
| NSLocalizedDescriptionKey : @"Service already added" |
| }]); |
| return; |
| } |
| CBMutableService *service = |
| [CBMutableService cb_mutableService:uuid withReadOnlyCharacteristics:characteristics]; |
| // Save the service & characteristics for later reads |
| this.services[uuid] = service; |
| this.serviceCharacteristics[uuid] = characteristics; |
| // Insert this as the next service advertised |
| [this.advertisedServices insertObject:uuid atIndex:0]; |
| CBDebugLog(@"Advertised services is now %@", this.advertisedServices); |
| // We can't do anything if we're not powered on -- but it's queued to be added whenever it is |
| if (this.isHardwarePoweredOn) { |
| this.addServiceHandlers[uuid] = handler; |
| [this.peripheral addService:service]; |
| CBInfoLog(@"Added service %@", service); |
| [this stopAdvertising]; |
| [this startAdvertising]; |
| } else { |
| // Tell handler it was a success since we can't know any true error until the hardware is |
| // ready. |
| CBInfoLog(@"Queued service %@", service); |
| handler(service.UUID, nil); |
| } |
| }); |
| } |
| |
| - (NSUInteger)serviceCount { |
| return self.services.count; |
| } |
| |
| - (void)removeService:(CBUUID *_Nonnull)uuid { |
| CBInfoLog(@"removeService: %@", uuid.UUIDString); |
| CBDispatchSync(self.queue, ^{ |
| CBMutableService *service = self.services[uuid]; |
| if (!service) { |
| return; |
| } |
| if (self.isHardwarePoweredOn) { |
| [self.peripheral removeService:service]; |
| } |
| self.services[uuid] = nil; |
| self.serviceCharacteristics[uuid] = nil; |
| [self.advertisedServices removeObject:uuid]; |
| self.addServiceHandlers[uuid] = nil; |
| // Undo the service from our current ads |
| [self stopAdvertising]; |
| // startAdvertising will check the current count before enabling |
| [self startAdvertising]; |
| }); |
| } |
| |
| - (void)addAllServices { |
| [self threadSafetyCheck]; |
| if (!self.isHardwarePoweredOn) { |
| CBErrorLog(@"Can't add all services -- BLE hardware state isn't powered on"); |
| return; |
| } |
| [self.peripheral removeAllServices]; |
| for (CBMutableService *service in self.services.allValues) { |
| [self.peripheral addService:service]; |
| } |
| } |
| |
| - (void)startAdvertising { |
| [self threadSafetyCheck]; |
| if (self.advertisingState != AdvertisingStateNotAdvertising) { |
| CBErrorLog(@"Not starting advertising when in state %d", self.advertisingState); |
| return; |
| } |
| if (!self.advertisedServices.count) { |
| CBDebugLog(@"Nothing to advertise"); |
| return; |
| } |
| if (!self.isHardwarePoweredOn) { |
| CBInfoLog(@"Not powered on -- can't advertise yet"); |
| return; |
| } |
| NSDictionary *ad = @{CBAdvertisementDataServiceUUIDsKey : self.advertisedServices}; |
| self.advertisingState = AdvertisingStateStarting; |
| CBDebugLog(@"startAdvertising %@", self.advertisedServices); |
| [self invalidateRotateAdTimer]; |
| // When we get the callback advertising started then we reschedule rotation. |
| self.requestedAdvertisingStart = [NSDate date]; |
| [self.peripheral startAdvertising:ad]; |
| } |
| |
| - (void)stopAdvertising { |
| [self threadSafetyCheck]; |
| CBDebugLog(@"stopAdvertising"); |
| self.advertisingState = AdvertisingStateNotAdvertising; |
| if (self.isHardwarePoweredOn) { |
| [self.peripheral stopAdvertising]; |
| } |
| [self invalidateRotateAdTimer]; |
| } |
| |
| - (void)scheduleRotateAdTimer:(NSTimeInterval)delay { |
| self.rotateAd = YES; |
| __weak typeof(self) this = self; |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), self.queue, ^{ |
| [this rotateAdTimerDidFire]; |
| }); |
| } |
| |
| - (void)invalidateRotateAdTimer { |
| self.rotateAd = NO; |
| } |
| |
| - (void)rotateAdTimerDidFire { |
| [self threadSafetyCheck]; |
| if (!self.rotateAd) return; |
| switch (self.advertisingState) { |
| case AdvertisingStateNotAdvertising: |
| CBDebugLog(@"Not advertising - expiring timer"); |
| [self invalidateRotateAdTimer]; |
| return; |
| case AdvertisingStateStarting: |
| CBDebugLog(@"Advertising still starting, ignoring timer"); |
| return; |
| case AdvertisingStateAdvertising: |
| if (self.advertisedServices.count < 2) { |
| CBDebugLog(@"Not enough services to rotate bluetooth ad - expiring timer"); |
| [self invalidateRotateAdTimer]; |
| return; |
| } |
| break; |
| } |
| |
| // Rotate services |
| CBUUID *head = [self.advertisedServices firstObject]; |
| [self.advertisedServices removeObjectAtIndex:0]; |
| [self.advertisedServices addObject:head]; |
| [self stopAdvertising]; |
| [self startAdvertising]; // This will reschedule the next timer if needed |
| } |
| |
| - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { |
| switch (peripheral.state) { |
| case CBPeripheralManagerStatePoweredOn: |
| CBInfoLog(@"CBPeripheralManagerStatePoweredOn"); |
| if (self.services.count > 0) { |
| [self addAllServices]; |
| [self startAdvertising]; |
| } |
| break; |
| case CBPeripheralManagerStatePoweredOff: |
| CBInfoLog(@"CBPeripheralManagerStatePoweredOff"); |
| [self stopAdvertising]; |
| break; |
| case CBPeripheralManagerStateResetting: |
| CBInfoLog(@"CBPeripheralManagerStateResetting"); |
| [self stopAdvertising]; |
| break; |
| case CBPeripheralManagerStateUnauthorized: |
| CBInfoLog(@"CBPeripheralManagerStateUnauthorized"); |
| [self stopAdvertising]; |
| break; |
| case CBPeripheralManagerStateUnknown: |
| CBInfoLog(@"CBPeripheralManagerStateUnknown"); |
| [self stopAdvertising]; |
| break; |
| case CBPeripheralManagerStateUnsupported: |
| CBInfoLog(@"CBPeripheralManagerStateUnsupported"); |
| [self stopAdvertising]; |
| break; |
| } |
| } |
| |
| - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral |
| error:(nullable NSError *)error { |
| [self threadSafetyCheck]; |
| NSTimeInterval responseTime = fabs([self.requestedAdvertisingStart timeIntervalSinceNow]); |
| if (error) { |
| CBErrorLog(@"Error advertising: %@", error); |
| [self stopAdvertising]; |
| return; |
| } |
| CBDebugLog(@"Now advertising - %2.f msec to start", responseTime * 1000); |
| self.advertisingState = AdvertisingStateAdvertising; |
| if (self.advertisedServices.count > 1) { |
| // Guarantee at least 100msec for battery |
| NSTimeInterval howSoon = MAX(0.1, self.rotateAdDelay - responseTime); |
| [self scheduleRotateAdTimer:howSoon]; |
| } |
| } |
| |
| - (void)peripheralManager:(CBPeripheralManager *)peripheral |
| didAddService:(CBService *)service |
| error:(nullable NSError *)error { |
| [self threadSafetyCheck]; |
| CBAddServiceHandler handler = self.addServiceHandlers[service.UUID]; |
| self.addServiceHandlers[service.UUID] = nil; |
| if (!handler) { |
| CBDebugLog(@"Missing handler for adding service %@ with error %@", service, error); |
| return; |
| } |
| handler(service.UUID, error); |
| } |
| |
| - (void)peripheralManager:(CBPeripheralManager *)peripheral |
| central:(CBCentral *)central |
| didSubscribeToCharacteristic:(CBCharacteristic *)characteristic { |
| CBInfoLog(@"central %@ didSubscribeToCharacteristic %@", central, characteristic); |
| } |
| |
| - (void)peripheralManager:(CBPeripheralManager *)peripheral |
| central:(CBCentral *)central |
| didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic { |
| CBInfoLog(@"central %@ didUnsubscribeFromCharacteristic %@", central, characteristic); |
| } |
| |
| - (void)peripheralManager:(CBPeripheralManager *)peripheral |
| didReceiveReadRequest:(CBATTRequest *)request { |
| CBDebugLog(@"didReceiveReadRequest %@", request); |
| NSDictionary *characteristics = self.serviceCharacteristics[request.characteristic.service.UUID]; |
| if (!characteristics) { |
| [peripheral respondToRequest:request withResult:CBATTErrorAttributeNotFound]; |
| return; |
| } |
| NSData *data = characteristics[request.characteristic.UUID]; |
| if (!data) { |
| [peripheral respondToRequest:request withResult:CBATTErrorAttributeNotFound]; |
| return; |
| } |
| if (request.offset >= data.length) { |
| [peripheral respondToRequest:request withResult:CBATTErrorInvalidOffset]; |
| return; |
| } |
| request.value = [data subdataWithRange:NSMakeRange(request.offset, data.length - request.offset)]; |
| [peripheral respondToRequest:request withResult:CBATTErrorSuccess]; |
| } |
| |
| - (void)peripheralManager:(CBPeripheralManager *)peripheral |
| didReceiveWriteRequests:(NSArray<CBATTRequest *> *)requests { |
| CBDebugLog(@"didReceiveWriteRequests %@", requests); |
| for (CBATTRequest *request in requests) { |
| [peripheral respondToRequest:request withResult:CBATTErrorWriteNotPermitted]; |
| } |
| } |
| |
| - (void)threadSafetyCheck { |
| assert(!strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), |
| dispatch_queue_get_label(self.queue))); |
| } |
| |
| - (BOOL)isHardwarePoweredOn { |
| return self.peripheral.state == CBPeripheralManagerStatePoweredOn; |
| } |
| |
| - (NSString *)debugDescription { |
| if (!self.queue) return @"[CBScanningDriver missing queue -- broken state]"; |
| __block NSString *out = nil; |
| CBDispatchSync(self.queue, ^{ |
| NSString *state; |
| switch (self.advertisingState) { |
| case AdvertisingStateAdvertising: |
| state = @"advertising"; |
| break; |
| case AdvertisingStateNotAdvertising: |
| state = @"not advertising"; |
| break; |
| case AdvertisingStateStarting: |
| state = @"starting"; |
| break; |
| } |
| out = [NSString stringWithFormat:@"[CBAdvertisingDriver isHardwareOn=%d advertisingState=%@ " |
| @"rotateAd=%d serviceUuids=%@]", |
| self.isHardwarePoweredOn, state, self.rotateAd, |
| self.services.allKeys]; |
| }); |
| return out; |
| } |
| |
| @end |