simplersble 1.1.1-dev28

The all-in-one Bluetooth library that makes it easy to add wireless connectivity to your projects.
Documentation
#import "LocalPeripheralBaseMacOS.h"

#include <exception>
#include <memory>
#include <vector>

#import "LocalCharacteristicMac.h"
#import "LocalPeripheralMac.h"
#import "LoggingInternal.h"

namespace {

const void* kPeripheralManagerQueueKey = &kPeripheralManagerQueueKey;
constexpr NSTimeInterval kOperationTimeout = 5.0;

NSString* clientIdentifier(CBCentral* central) { return central.identifier.UUIDString.lowercaseString; }

}  // namespace

@interface LocalPeripheralBaseMacOS ()

@property(nonatomic, assign) SimpleBLE::Local::PeripheralMac* peripheral;
@property(nonatomic, strong) dispatch_queue_t managerQueue;
@property(nonatomic, strong) CBPeripheralManager* manager;
@property(nonatomic, strong) NSCondition* condition;
@property(nonatomic, assign) NSUInteger pendingServiceCount;
@property(nonatomic, assign) BOOL advertisingCompleted;
@property(nonatomic, assign) BOOL active;
@property(nonatomic, copy, nullable) NSString* operationError;
@property(nonatomic, strong) NSMapTable<CBMutableCharacteristic*, NSData*>* pendingUpdates;

- (BOOL)isOnManagerQueue;
- (void)performOnManagerQueueAndWait:(dispatch_block_t)block;

@end

@implementation LocalPeripheralBaseMacOS

- (instancetype)init:(SimpleBLE::Local::PeripheralMac*)peripheral {
    self = [super init];
    if (self) {
        _peripheral = peripheral;
        _condition = [[NSCondition alloc] init];
        _pendingUpdates = [NSMapTable strongToStrongObjectsMapTable];

        dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
        _managerQueue = dispatch_queue_create("LocalPeripheralBaseMacOS.managerQueue", attributes);
        dispatch_queue_set_specific(_managerQueue, kPeripheralManagerQueueKey, (__bridge void*)self, nullptr);
        _manager = [[CBPeripheralManager alloc] initWithDelegate:self queue:_managerQueue options:nil];
    }
    return self;
}

- (void*)underlying {
    return (__bridge void*)self.manager;
}

- (nullable NSString*)startWithServices:(NSArray<CBMutableService*>*)services
                      advertisementData:(NSDictionary<NSString*, id>*)advertisementData {
    if ([self isOnManagerQueue]) {
        return @"A local peripheral cannot be started from a CoreBluetooth peripheral callback.";
    }

    NSDate* deadline = [NSDate dateWithTimeIntervalSinceNow:kOperationTimeout];
    [self.condition lock];
    while (self.manager.state == CBManagerStateUnknown) {
        if (![self.condition waitUntilDate:deadline]) {
            break;
        }
    }

    if (self.manager.state != CBManagerStatePoweredOn) {
        NSString* error = [NSString
            stringWithFormat:@"CoreBluetooth peripheral manager is not powered on (state %ld).", (long)self.manager.state];
        [self.condition unlock];
        return error;
    }

    self.pendingServiceCount = services.count;
    self.advertisingCompleted = NO;
    self.operationError = nil;
    [self.condition unlock];

    dispatch_async(self.managerQueue, ^{
      for (CBMutableService* service in services) {
          [self.manager addService:service];
      }
    });

    [self.condition lock];
    while (self.pendingServiceCount > 0) {
        if (![self.condition waitUntilDate:deadline]) {
            self.operationError = @"Timed out while publishing local GATT services.";
            break;
        }
    }
    NSString* serviceError = self.operationError;
    [self.condition unlock];

    if (serviceError != nil) {
        [self stop];
        return serviceError;
    }

    dispatch_async(self.managerQueue, ^{
      [self.manager startAdvertising:advertisementData];
    });

    [self.condition lock];
    while (!self.advertisingCompleted) {
        if (![self.condition waitUntilDate:deadline]) {
            self.operationError = @"Timed out while starting local peripheral advertising.";
            break;
        }
    }
    NSString* advertisingError = self.operationError;
    [self.condition unlock];

    if (advertisingError != nil) {
        [self stop];
    }
    return advertisingError;
}

- (void)stop {
    [self performOnManagerQueueAndWait:^{
      self.active = NO;
      [self.manager stopAdvertising];
      [self.manager removeAllServices];
      [self.pendingUpdates removeAllObjects];
    }];
}

- (bool)isAdvertising {
    if ([self isOnManagerQueue]) {
        return self.manager.isAdvertising;
    }

    __block BOOL advertising = NO;
    dispatch_sync(self.managerQueue, ^{
      advertising = self.manager.isAdvertising;
    });
    return advertising;
}

- (void)publishValue:(NSData*)value forCharacteristic:(CBMutableCharacteristic*)characteristic {
    dispatch_async(self.managerQueue, ^{
      if (!self.active) {
          return;
      }
      if (![self.manager updateValue:value forCharacteristic:characteristic onSubscribedCentrals:nil]) {
          [self.pendingUpdates setObject:value forKey:characteristic];
      } else {
          [self.pendingUpdates removeObjectForKey:characteristic];
      }
    });
}

- (void)detach {
    [self performOnManagerQueueAndWait:^{
      self.active = NO;
      [self.manager stopAdvertising];
      [self.manager removeAllServices];
      [self.pendingUpdates removeAllObjects];
      self.peripheral = nullptr;
      self.manager.delegate = nil;
    }];
}

- (BOOL)isOnManagerQueue {
    return dispatch_get_specific(kPeripheralManagerQueueKey) == (__bridge void*)self;
}

- (void)performOnManagerQueueAndWait:(dispatch_block_t)block {
    if ([self isOnManagerQueue]) {
        block();
    } else {
        dispatch_sync(self.managerQueue, block);
    }
}

#pragma mark - CBPeripheralManagerDelegate

- (void)peripheralManagerDidUpdateState:(CBPeripheralManager*)peripheral {
    self.active = peripheral.isAdvertising;
    if (!self.active) {
        [self.pendingUpdates removeAllObjects];
    }
    [self.condition lock];
    [self.condition broadcast];
    [self.condition unlock];
}

- (void)peripheralManager:(CBPeripheralManager*)peripheral didAddService:(CBService*)service error:(nullable NSError*)error {
    [self.condition lock];
    if (error != nil && self.operationError == nil) {
        self.operationError = error.localizedDescription;
    }
    if (self.pendingServiceCount > 0) {
        --self.pendingServiceCount;
    }
    [self.condition broadcast];
    [self.condition unlock];
}

- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager*)peripheral error:(nullable NSError*)error {
    [self.condition lock];
    if (error != nil) {
        self.operationError = error.localizedDescription;
    }
    self.active = error == nil;
    self.advertisingCompleted = YES;
    [self.condition broadcast];
    [self.condition unlock];
}

- (void)peripheralManager:(CBPeripheralManager*)peripheral
                         central:(CBCentral*)central
    didSubscribeToCharacteristic:(CBCharacteristic*)characteristic {
    auto* localPeripheral = self.peripheral;
    if (localPeripheral != nullptr) {
        localPeripheral->handle_subscribed((__bridge void*)characteristic, [clientIdentifier(central) UTF8String]);
    }
}

- (void)peripheralManager:(CBPeripheralManager*)peripheral
                             central:(CBCentral*)central
    didUnsubscribeFromCharacteristic:(CBCharacteristic*)characteristic {
    auto* localPeripheral = self.peripheral;
    if (localPeripheral != nullptr) {
        localPeripheral->handle_unsubscribed((__bridge void*)characteristic, [clientIdentifier(central) UTF8String]);
    }
}

- (void)peripheralManager:(CBPeripheralManager*)peripheral didReceiveReadRequest:(CBATTRequest*)request {
    auto* localPeripheral = self.peripheral;
    if (localPeripheral == nullptr) {
        [peripheral respondToRequest:request withResult:CBATTErrorUnlikelyError];
        return;
    }

    auto characteristic = localPeripheral->characteristic_for((__bridge void*)request.characteristic);
    if (!characteristic || !characteristic->can_read()) {
        [peripheral respondToRequest:request withResult:CBATTErrorReadNotPermitted];
        return;
    }

    try {
        SimpleBLE::ByteArray value = characteristic->handle_read();
        if (request.offset > value.size()) {
            [peripheral respondToRequest:request withResult:CBATTErrorInvalidOffset];
            return;
        }

        const auto* bytes = value.empty() ? nullptr : value.data() + request.offset;
        request.value = [NSData dataWithBytes:bytes length:value.size() - request.offset];
        [peripheral respondToRequest:request withResult:CBATTErrorSuccess];
    } catch (const std::exception& ex) {
        SIMPLEBLE_LOG_ERROR(fmt::format("Exception while handling local characteristic read: {}", ex.what()));
        [peripheral respondToRequest:request withResult:CBATTErrorUnlikelyError];
    } catch (...) {
        SIMPLEBLE_LOG_ERROR("Unknown exception while handling local characteristic read");
        [peripheral respondToRequest:request withResult:CBATTErrorUnlikelyError];
    }
}

- (void)peripheralManager:(CBPeripheralManager*)peripheral didReceiveWriteRequests:(NSArray<CBATTRequest*>*)requests {
    if (requests.count == 0) {
        return;
    }

    auto* localPeripheral = self.peripheral;
    if (localPeripheral == nullptr) {
        [peripheral respondToRequest:requests.firstObject withResult:CBATTErrorUnlikelyError];
        return;
    }

    std::vector<std::pair<std::shared_ptr<SimpleBLE::Local::CharacteristicMac>, SimpleBLE::ByteArray>> writes;
    writes.reserve(requests.count);

    for (CBATTRequest* request in requests) {
        auto characteristic = localPeripheral->characteristic_for((__bridge void*)request.characteristic);
        if (!characteristic || !characteristic->can_write()) {
            [peripheral respondToRequest:request withResult:CBATTErrorWriteNotPermitted];
            return;
        }
        if (request.offset != 0) {
            [peripheral respondToRequest:request withResult:CBATTErrorInvalidOffset];
            return;
        }
        if (request.value == nil) {
            [peripheral respondToRequest:request withResult:CBATTErrorInvalidAttributeValueLength];
            return;
        }

        writes.emplace_back(characteristic, SimpleBLE::ByteArray(static_cast<const uint8_t*>(request.value.bytes), request.value.length));
    }

    try {
        for (auto& [characteristic, value] : writes) {
            characteristic->handle_write(std::move(value));
        }
        [peripheral respondToRequest:requests.firstObject withResult:CBATTErrorSuccess];
    } catch (const std::exception& ex) {
        SIMPLEBLE_LOG_ERROR(fmt::format("Exception while handling local characteristic write: {}", ex.what()));
        [peripheral respondToRequest:requests.firstObject withResult:CBATTErrorUnlikelyError];
    } catch (...) {
        SIMPLEBLE_LOG_ERROR("Unknown exception while handling local characteristic write");
        [peripheral respondToRequest:requests.firstObject withResult:CBATTErrorUnlikelyError];
    }
}

- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager*)peripheral {
    NSMutableArray<CBMutableCharacteristic*>* characteristics = [NSMutableArray array];
    for (CBMutableCharacteristic* characteristic in self.pendingUpdates.keyEnumerator) {
        [characteristics addObject:characteristic];
    }
    for (CBMutableCharacteristic* characteristic in characteristics) {
        NSData* value = [self.pendingUpdates objectForKey:characteristic];
        if (![peripheral updateValue:value forCharacteristic:characteristic onSubscribedCentrals:nil]) {
            return;
        }
        [self.pendingUpdates removeObjectForKey:characteristic];
    }
}

@end