blob: 7b5d68bab135237ef9ae7e7e72987fca7e341b29 [file] [log] [blame]
/*
*
* Copyright (c) 2020-2021 Project CHIP Authors
* Copyright (c) 2015-2017 Nest Labs, Inc.
*
* 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
*
* http://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.
*/
/**
* @file
* Provides an implementation of BleConnectionDelegate for Darwin platforms.
*/
#if !__has_feature(objc_arc)
#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
#include <ble/Ble.h>
#include <lib/support/logging/CHIPLogging.h>
#include <platform/CHIPDeviceLayer.h>
#include <platform/Darwin/BleConnectionDelegate.h>
#include <platform/Darwin/BleScannerDelegate.h>
#include <platform/LockTracker.h>
#include <setup_payload/SetupPayload.h>
#include <tracing/metric_event.h>
#import "MTRUUIDHelper.h"
#import "PlatformMetricKeys.h"
using namespace chip::Ble;
using namespace chip::DeviceLayer;
using namespace chip::Tracing::DarwinPlatform;
constexpr uint64_t kScanningWithDiscriminatorTimeoutInSeconds = 60;
constexpr uint64_t kPreWarmScanTimeoutInSeconds = 120;
constexpr uint64_t kCachePeripheralTimeoutInSeconds
= static_cast<uint64_t>(CHIP_DEVICE_CONFIG_BLE_SLOW_ADVERTISING_INTERVAL_MAX / 1000.0 * 8.0 * 0.625);
constexpr char kBleWorkQueueName[] = "org.csa-iot.matter.framework.ble.workqueue";
typedef NS_ENUM(uint8_t, BleConnectionMode) {
kUndefined = 0,
kScanning,
kScanningWithTimeout,
kConnecting,
};
@interface BleConnection : NSObject <CBCentralManagerDelegate, CBPeripheralDelegate>
@property (strong, nonatomic) dispatch_queue_t chipWorkQueue;
@property (strong, nonatomic) dispatch_queue_t workQueue;
@property (strong, nonatomic) CBCentralManager * centralManager;
@property (strong, nonatomic) CBPeripheral * peripheral;
@property (strong, nonatomic) CBUUID * shortServiceUUID;
@property (nonatomic, readonly, nullable) dispatch_source_t timer;
@property (nonatomic, readonly) BleConnectionMode currentMode;
@property (strong, nonatomic) NSMutableDictionary<CBPeripheral *, NSDictionary *> * cachedPeripherals;
@property (unsafe_unretained, nonatomic) bool found;
@property (unsafe_unretained, nonatomic) chip::SetupDiscriminator deviceDiscriminator;
@property (unsafe_unretained, nonatomic) void * appState;
@property (unsafe_unretained, nonatomic) BleConnectionDelegate::OnConnectionCompleteFunct onConnectionComplete;
@property (unsafe_unretained, nonatomic) BleConnectionDelegate::OnConnectionErrorFunct onConnectionError;
@property (unsafe_unretained, nonatomic) chip::DeviceLayer::BleScannerDelegate * scannerDelegate;
@property (unsafe_unretained, nonatomic) chip::Ble::BleLayer * mBleLayer;
- (id)initWithQueue:(dispatch_queue_t)queue;
- (id)initWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm queue:(dispatch_queue_t)queue;
- (id)initWithDiscriminator:(const chip::SetupDiscriminator &)deviceDiscriminator queue:(dispatch_queue_t)queue;
- (void)setBleLayer:(chip::Ble::BleLayer *)bleLayer;
- (void)start;
- (void)stop;
- (void)updateWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm;
- (void)updateWithDiscriminator:(const chip::SetupDiscriminator &)deviceDiscriminator;
- (void)updateWithPeripheral:(CBPeripheral *)peripheral;
- (BOOL)isConnecting;
- (void)addPeripheralToCache:(CBPeripheral *)peripheral data:(NSData *)data;
- (void)removePeripheralFromCache:(CBPeripheral *)peripheral;
- (void)removePeripheralsFromCache;
@end
namespace chip {
namespace DeviceLayer {
namespace Internal {
BleConnection * ble;
dispatch_queue_t bleWorkQueue;
void BleConnectionDelegateImpl::NewConnection(
Ble::BleLayer * bleLayer, void * appState, const SetupDiscriminator & inDeviceDiscriminator)
{
assertChipStackLockedByCurrentThread();
// Make a copy of the device discriminator for the block to capture.
SetupDiscriminator deviceDiscriminator = inDeviceDiscriminator;
ChipLogProgress(Ble, "ConnectionDelegate NewConnection with discriminator");
if (!bleWorkQueue) {
bleWorkQueue = dispatch_queue_create(kBleWorkQueueName, DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
}
dispatch_async(bleWorkQueue, ^{
// If the previous connection delegate was not a try to connect to something, just reuse it instead of
// creating a brand new connection but update the discriminator and the ble layer members.
if (ble and ![ble isConnecting]) {
[ble setBleLayer:bleLayer];
ble.appState = appState;
ble.onConnectionComplete = OnConnectionComplete;
ble.onConnectionError = OnConnectionError;
[ble updateWithDiscriminator:deviceDiscriminator];
return;
}
[ble stop];
ble = [[BleConnection alloc] initWithDiscriminator:deviceDiscriminator queue:bleWorkQueue];
[ble setBleLayer:bleLayer];
ble.appState = appState;
ble.onConnectionComplete = OnConnectionComplete;
ble.onConnectionError = OnConnectionError;
ble.centralManager = [ble.centralManager initWithDelegate:ble queue:bleWorkQueue];
});
}
void BleConnectionDelegateImpl::NewConnection(Ble::BleLayer * bleLayer, void * appState, BLE_CONNECTION_OBJECT connObj)
{
assertChipStackLockedByCurrentThread();
ChipLogProgress(Ble, "ConnectionDelegate NewConnection with conn obj: %p", connObj);
if (!bleWorkQueue) {
bleWorkQueue = dispatch_queue_create(kBleWorkQueueName, DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
}
CBPeripheral * peripheral = (__bridge CBPeripheral *) connObj; // bridge (and retain) before dispatching
dispatch_async(bleWorkQueue, ^{
// The BLE_CONNECTION_OBJECT represent a CBPeripheral object. In order for it to be valid the central
// manager needs to still be running.
if (!ble || [ble isConnecting]) {
if (OnConnectionError) {
auto workQueue = chip::DeviceLayer::PlatformMgrImpl().GetWorkQueue();
dispatch_async(workQueue, ^{
OnConnectionError(appState, CHIP_ERROR_INCORRECT_STATE);
});
}
return;
}
[ble setBleLayer:bleLayer];
ble.appState = appState;
ble.onConnectionComplete = OnConnectionComplete;
ble.onConnectionError = OnConnectionError;
[ble updateWithPeripheral:peripheral];
});
}
void BleConnectionDelegateImpl::StartScan(BleScannerDelegate * delegate, BleScanMode mode)
{
assertChipStackLockedByCurrentThread();
bool prewarm = (mode == BleScanMode::kPreWarm);
ChipLogProgress(Ble, "ConnectionDelegate StartScan (%s)", (prewarm ? "pre-warm" : "default"));
if (!bleWorkQueue) {
bleWorkQueue = dispatch_queue_create(kBleWorkQueueName, DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
}
dispatch_async(bleWorkQueue, ^{
// Pre-warming is best-effort, don't cancel an ongoing scan or connection attempt
if (prewarm && ble) {
// TODO: Once we get rid of the separate BLE queue we can just return CHIP_ERROR_BUSY.
// That will also allow these cases to be distinguished in our metric.
ChipLogProgress(Ble, "Not starting pre-warm scan, an operation is already in progress");
if (delegate) {
dispatch_async(PlatformMgrImpl().GetWorkQueue(), ^{
delegate->OnBleScanStopped();
});
}
return;
}
// If the previous connection delegate was not a try to connect to something, just reuse it instead of
// creating a brand new connection but update the discriminator and the ble layer members.
if (ble and ![ble isConnecting]) {
[ble updateWithDelegate:delegate prewarm:prewarm];
return;
}
[ble stop];
ble = [[BleConnection alloc] initWithDelegate:delegate prewarm:prewarm queue:bleWorkQueue];
// Do _not_ set onConnectionComplete and onConnectionError
// here. The connection callbacks we have expect an appState
// that we do not have here, and in any case connection
// complete/error make no sense for a scan.
ble.centralManager = [ble.centralManager initWithDelegate:ble queue:bleWorkQueue];
});
}
void BleConnectionDelegateImpl::StopScan()
{
ChipLogProgress(Ble, "ConnectionDelegate StopScan");
DoCancel();
}
CHIP_ERROR BleConnectionDelegateImpl::CancelConnection()
{
ChipLogProgress(Ble, "ConnectionDelegate CancelConnection");
return DoCancel();
}
CHIP_ERROR BleConnectionDelegateImpl::DoCancel()
{
assertChipStackLockedByCurrentThread();
if (bleWorkQueue == nil) {
return CHIP_NO_ERROR;
}
dispatch_async(bleWorkQueue, ^{
[ble stop];
ble = nil;
});
bleWorkQueue = nil;
return CHIP_NO_ERROR;
}
} // namespace Internal
} // namespace DeviceLayer
} // namespace chip
@interface BleConnection ()
@property (nonatomic, readonly) int32_t totalDevicesAdded;
@property (nonatomic, readonly) int32_t totalDevicesRemoved;
@end
@implementation BleConnection
- (id)initWithQueue:(dispatch_queue_t)queue
{
self = [super init];
if (self) {
self.shortServiceUUID = [MTRUUIDHelper GetShortestServiceUUID:&chip::Ble::CHIP_BLE_SVC_ID];
_chipWorkQueue = chip::DeviceLayer::PlatformMgrImpl().GetWorkQueue();
_workQueue = queue;
_centralManager = [CBCentralManager alloc];
_found = false;
_cachedPeripherals = [[NSMutableDictionary alloc] init];
_currentMode = kUndefined;
[self _resetCounters];
}
return self;
}
- (id)initWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm queue:(dispatch_queue_t)queue
{
self = [self initWithQueue:queue];
if (self) {
_scannerDelegate = delegate;
if (prewarm) {
_currentMode = kScanningWithTimeout;
[self setupTimer:kPreWarmScanTimeoutInSeconds];
} else {
_currentMode = kScanning;
}
}
return self;
}
- (id)initWithDiscriminator:(const chip::SetupDiscriminator &)deviceDiscriminator queue:(dispatch_queue_t)queue
{
self = [self initWithQueue:queue];
if (self) {
_deviceDiscriminator = deviceDiscriminator;
_currentMode = kConnecting;
[self setupTimer:kScanningWithDiscriminatorTimeoutInSeconds];
}
return self;
}
- (BOOL)isConnecting
{
return _currentMode == kConnecting;
}
- (void)setupTimer:(uint64_t)timeout
{
[self clearTimer];
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _workQueue);
dispatch_source_set_event_handler(_timer, ^{
ChipLogProgress(Ble, "ConnectionDelegate timeout");
[self stop];
[self dispatchConnectionError:BLE_ERROR_APP_CLOSED_CONNECTION];
});
auto value = static_cast<int64_t>(timeout * NSEC_PER_SEC);
dispatch_source_set_timer(_timer, dispatch_walltime(nullptr, value), DISPATCH_TIME_FOREVER, 5 * NSEC_PER_SEC);
dispatch_resume(_timer);
}
- (void)clearTimer
{
if (_timer) {
dispatch_source_cancel(_timer);
_timer = nil;
}
}
// All our callback dispatch must happen on _chipWorkQueue
- (void)dispatchConnectionError:(CHIP_ERROR)error
{
dispatch_async(_chipWorkQueue, ^{
if (self.onConnectionError != nil) {
self.onConnectionError(self.appState, error);
}
});
}
- (void)dispatchConnectionComplete:(CBPeripheral *)peripheral
{
dispatch_async(_chipWorkQueue, ^{
if (self.onConnectionComplete != nil) {
self.onConnectionComplete(self.appState, (__bridge void *) peripheral);
}
});
}
// Start CBCentralManagerDelegate
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
MATTER_LOG_METRIC(kMetricBLECentralManagerState, static_cast<uint32_t>(central.state));
switch (central.state) {
case CBManagerStatePoweredOn:
ChipLogDetail(Ble, "CBManagerState: ON");
[self start];
break;
case CBManagerStatePoweredOff:
ChipLogDetail(Ble, "CBManagerState: OFF");
[self stop];
[self dispatchConnectionError:BLE_ERROR_APP_CLOSED_CONNECTION];
break;
case CBManagerStateUnauthorized:
ChipLogDetail(Ble, "CBManagerState: Unauthorized");
break;
case CBManagerStateResetting:
ChipLogDetail(Ble, "CBManagerState: RESETTING");
break;
case CBManagerStateUnsupported:
ChipLogDetail(Ble, "CBManagerState: UNSUPPORTED");
break;
case CBManagerStateUnknown:
ChipLogDetail(Ble, "CBManagerState: UNKNOWN");
break;
}
}
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI
{
NSDictionary * servicesData = [advertisementData objectForKey:CBAdvertisementDataServiceDataKey];
NSData * serviceData;
for (CBUUID * serviceUUID in servicesData) {
if ([serviceUUID.data isEqualToData:_shortServiceUUID.data]) {
serviceData = [servicesData objectForKey:serviceUUID];
break;
}
}
if (!serviceData) {
return;
}
NSNumber * isConnectable = [advertisementData objectForKey:CBAdvertisementDataIsConnectable];
MATTER_LOG_METRIC_END(kMetricBLEDiscoveredPeripheral, [isConnectable boolValue]);
if ([isConnectable boolValue] == NO) {
ChipLogError(Ble, "A device (%p) with a matching Matter UUID has been discovered but it is not connectable.", peripheral);
return;
}
const uint8_t * bytes = (const uint8_t *) [serviceData bytes];
if ([serviceData length] != sizeof(ChipBLEDeviceIdentificationInfo)) {
NSMutableString * hexString = [NSMutableString stringWithCapacity:([serviceData length] * 2)];
for (NSUInteger i = 0; i < [serviceData length]; i++) {
[hexString appendString:[NSString stringWithFormat:@"%02lx", (unsigned long) bytes[i]]];
}
ChipLogError(Ble,
"A device (%p) with a matching Matter UUID has been discovered but the service data len does not match our expectation "
"(serviceData = %s)",
peripheral, [hexString UTF8String]);
MATTER_LOG_METRIC(kMetricBLEBadServiceDataLength, static_cast<uint32_t>([serviceData length]));
return;
}
uint8_t opCode = bytes[0];
if (opCode != 0 && opCode != 1) {
ChipLogError(Ble,
"A device (%p) with a matching Matter UUID has been discovered but the service data opCode not match our expectation "
"(opCode = %u).",
peripheral, opCode);
MATTER_LOG_METRIC(kMetricBLEBadOpCode, opCode);
return;
}
uint16_t discriminator = (bytes[1] | (bytes[2] << 8)) & 0xfff;
if ([self isConnecting]) {
if (![self checkDiscriminator:discriminator]) {
ChipLogError(Ble,
"A device (%p) with a matching Matter UUID has been discovered but the service data discriminator not match our "
"expectation (discriminator = %u).",
peripheral, discriminator);
MATTER_LOG_METRIC(kMetricBLEMismatchedDiscriminator);
return;
}
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredMatchingPeripheral);
ChipLogProgress(Ble, "Connecting to device %p with discriminator: %d", peripheral, discriminator);
[self connect:peripheral];
[self stopScanning];
} else {
[self addPeripheralToCache:peripheral data:serviceData];
}
}
- (BOOL)checkDiscriminator:(uint16_t)discriminator
{
return _deviceDiscriminator.MatchesLongDiscriminator(discriminator);
}
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
MATTER_LOG_METRIC_END(kMetricBLEConnectPeripheral);
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredServices);
[peripheral setDelegate:self];
[peripheral discoverServices:nil];
[self stopScanning];
}
// End CBCentralManagerDelegate
// Start CBPeripheralDelegate
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
if (nil != error) {
ChipLogError(Ble, "BLE:Error finding Chip Service in the device: [%s]", [error.localizedDescription UTF8String]);
}
MATTER_LOG_METRIC_END(kMetricBLEDiscoveredServices, CHIP_ERROR(chip::ChipError::Range::kOS, static_cast<uint32_t>(error.code)));
for (CBService * service in peripheral.services) {
if ([service.UUID.data isEqualToData:_shortServiceUUID.data] && !self.found) {
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredCharacteristics);
[peripheral discoverCharacteristics:nil forService:service];
self.found = true;
break;
}
}
if (!self.found || error != nil) {
ChipLogError(Ble, "Service not found on the device.");
MATTER_LOG_METRIC(kMetricBLEDiscoveredServices, CHIP_ERROR_INCORRECT_STATE);
[self dispatchConnectionError:CHIP_ERROR_INCORRECT_STATE];
}
}
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
MATTER_LOG_METRIC_END(kMetricBLEDiscoveredCharacteristics, CHIP_ERROR(chip::ChipError::Range::kOS, static_cast<uint32_t>(error.code)));
if (nil != error) {
ChipLogError(
Ble, "BLE:Error finding Characteristics in Chip service on the device: [%s]", [error.localizedDescription UTF8String]);
}
// XXX error ?
[self dispatchConnectionComplete:peripheral];
}
- (void)peripheral:(CBPeripheral *)peripheral
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
if (nil == error) {
chip::Ble::ChipBleUUID svcId;
chip::Ble::ChipBleUUID charId;
[BleConnection fillServiceWithCharacteristicUuids:characteristic svcId:&svcId charId:&charId];
dispatch_async(_chipWorkQueue, ^{
_mBleLayer->HandleWriteConfirmation((__bridge void *) peripheral, &svcId, &charId);
});
} else {
ChipLogError(
Ble, "BLE:Error writing Characteristics in Chip service on the device: [%s]", [error.localizedDescription UTF8String]);
dispatch_async(_chipWorkQueue, ^{
MATTER_LOG_METRIC(kMetricBLEWriteChrValueFailed, BLE_ERROR_GATT_WRITE_FAILED);
_mBleLayer->HandleConnectionError((__bridge void *) peripheral, BLE_ERROR_GATT_WRITE_FAILED);
});
}
}
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
bool isNotifying = characteristic.isNotifying;
if (nil == error) {
chip::Ble::ChipBleUUID svcId;
chip::Ble::ChipBleUUID charId;
[BleConnection fillServiceWithCharacteristicUuids:characteristic svcId:&svcId charId:&charId];
dispatch_async(_chipWorkQueue, ^{
if (isNotifying) {
_mBleLayer->HandleSubscribeComplete((__bridge void *) peripheral, &svcId, &charId);
} else {
_mBleLayer->HandleUnsubscribeComplete((__bridge void *) peripheral, &svcId, &charId);
}
});
} else {
ChipLogError(Ble, "BLE:Error subscribing/unsubcribing some characteristic on the device: [%s]",
[error.localizedDescription UTF8String]);
dispatch_async(_chipWorkQueue, ^{
if (isNotifying) {
MATTER_LOG_METRIC(kMetricBLEUpdateNotificationStateForChrFailed, BLE_ERROR_GATT_WRITE_FAILED);
// we're still notifying, so we must failed the unsubscription
_mBleLayer->HandleConnectionError((__bridge void *) peripheral, BLE_ERROR_GATT_UNSUBSCRIBE_FAILED);
} else {
// we're not notifying, so we must failed the subscription
MATTER_LOG_METRIC(kMetricBLEUpdateNotificationStateForChrFailed, BLE_ERROR_GATT_SUBSCRIBE_FAILED);
_mBleLayer->HandleConnectionError((__bridge void *) peripheral, BLE_ERROR_GATT_SUBSCRIBE_FAILED);
}
});
}
}
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
if (nil == error) {
chip::Ble::ChipBleUUID svcId;
chip::Ble::ChipBleUUID charId;
[BleConnection fillServiceWithCharacteristicUuids:characteristic svcId:&svcId charId:&charId];
auto * value = characteristic.value; // read immediately before dispatching
dispatch_async(_chipWorkQueue, ^{
// build a inet buffer from the rxEv and send to blelayer.
auto msgBuf = chip::System::PacketBufferHandle::NewWithData(value.bytes, value.length);
if (msgBuf.IsNull()) {
ChipLogError(Ble, "Failed at allocating buffer for incoming BLE data");
MATTER_LOG_METRIC(kMetricBLEUpdateValueForChrFailed, CHIP_ERROR_NO_MEMORY);
_mBleLayer->HandleConnectionError((__bridge void *) peripheral, CHIP_ERROR_NO_MEMORY);
} else if (!_mBleLayer->HandleIndicationReceived((__bridge void *) peripheral, &svcId, &charId, std::move(msgBuf))) {
// since this error comes from device manager core
// we assume it would do the right thing, like closing the connection
ChipLogError(Ble, "Failed at handling incoming BLE data");
MATTER_LOG_METRIC(kMetricBLEUpdateValueForChrFailed, CHIP_ERROR_INCORRECT_STATE);
}
});
} else {
ChipLogError(
Ble, "BLE:Error receiving indication of Characteristics on the device: [%s]", [error.localizedDescription UTF8String]);
dispatch_async(_chipWorkQueue, ^{
MATTER_LOG_METRIC(kMetricBLEUpdateValueForChrFailed, BLE_ERROR_GATT_INDICATE_FAILED);
_mBleLayer->HandleConnectionError((__bridge void *) peripheral, BLE_ERROR_GATT_INDICATE_FAILED);
});
}
}
// End CBPeripheralDelegate
- (void)start
{
// If a peripheral has already been found, try to connect to it once BLE starts,
// otherwise start scanning to find the peripheral to connect to.
if (_peripheral != nil) {
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredMatchingPeripheral);
[self connect:_peripheral];
} else {
[self startScanning];
}
}
- (void)stop
{
[self detachScannerDelegate];
_found = false;
[self stopScanning];
[self removePeripheralsFromCache];
if (!_centralManager && !_peripheral) {
return;
}
// Properly closing the underlying ble connections needs to happens
// on the chip work queue. At the same time the SDK is trying to
// properly unsubscribe and shutdown the connection, so if we nullify
// the centralManager and the peripheral members too early it won't be
// able to reach those.
// This is why closing connections happens as 2 async steps.
dispatch_async(_chipWorkQueue, ^{
if (_peripheral) {
_mBleLayer->CloseAllBleConnections();
}
dispatch_async(_workQueue, ^{
_centralManager.delegate = nil;
_centralManager = nil;
_peripheral = nil;
if (chip::DeviceLayer::Internal::ble == self) {
chip::DeviceLayer::Internal::ble = nil;
}
});
});
}
- (void)_resetCounters
{
_totalDevicesAdded = 0;
_totalDevicesRemoved = 0;
}
- (void)startScanning
{
if (!_centralManager) {
return;
}
MATTER_LOG_METRIC_BEGIN(kMetricBLEScan);
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredPeripheral);
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredMatchingPeripheral);
[self _resetCounters];
auto scanOptions = @{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES };
[_centralManager scanForPeripheralsWithServices:@[ _shortServiceUUID ] options:scanOptions];
}
- (void)stopScanning
{
if (!_centralManager) {
return;
}
MATTER_LOG_METRIC_END(kMetricBLEScan);
[self _resetCounters];
[self clearTimer];
[_centralManager stopScan];
}
- (void)connect:(CBPeripheral *)peripheral
{
if (!_centralManager || !peripheral) {
return;
}
MATTER_LOG_METRIC_END(kMetricBLEDiscoveredMatchingPeripheral);
MATTER_LOG_METRIC_BEGIN(kMetricBLEConnectPeripheral);
_peripheral = peripheral;
[_centralManager connectPeripheral:peripheral options:nil];
}
- (void)detachScannerDelegate
{
auto * existingDelegate = _scannerDelegate;
if (existingDelegate) {
_scannerDelegate = nullptr;
dispatch_async(_chipWorkQueue, ^{
existingDelegate->OnBleScanStopped();
});
}
}
- (void)updateWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm
{
[self detachScannerDelegate];
if (delegate) {
for (CBPeripheral * cachedPeripheral in _cachedPeripherals) {
NSData * serviceData = _cachedPeripherals[cachedPeripheral][@"data"];
dispatch_async(_chipWorkQueue, ^{
ChipBLEDeviceIdentificationInfo info;
memcpy(&info, [serviceData bytes], sizeof(info));
delegate->OnBleScanAdd((__bridge void *) cachedPeripheral, info);
});
}
_scannerDelegate = delegate;
}
if (prewarm) {
_currentMode = kScanningWithTimeout;
[self setupTimer:kPreWarmScanTimeoutInSeconds];
} else {
_currentMode = kScanning;
[self clearTimer];
}
}
- (void)updateWithDiscriminator:(const chip::SetupDiscriminator &)deviceDiscriminator
{
[self detachScannerDelegate];
_deviceDiscriminator = deviceDiscriminator;
_currentMode = kConnecting;
CBPeripheral * peripheral = nil;
for (CBPeripheral * cachedPeripheral in _cachedPeripherals) {
NSData * serviceData = _cachedPeripherals[cachedPeripheral][@"data"];
ChipBLEDeviceIdentificationInfo info;
memcpy(&info, [serviceData bytes], sizeof(info));
if ([self checkDiscriminator:info.GetDeviceDiscriminator()]) {
peripheral = cachedPeripheral;
break;
}
}
[self removePeripheralsFromCache];
if (peripheral) {
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredMatchingPeripheral);
ChipLogProgress(Ble, "Connecting to cached device: %p", peripheral);
[self connect:peripheral];
// The cached peripheral might be obsolete, so continue scanning until didConnectPeripheral is triggered.
} else {
[self setupTimer:kScanningWithDiscriminatorTimeoutInSeconds];
}
}
- (void)updateWithPeripheral:(CBPeripheral *)peripheral
{
[self detachScannerDelegate];
_currentMode = kConnecting;
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredMatchingPeripheral);
ChipLogProgress(Ble, "Connecting to device: %p", peripheral);
[self connect:peripheral];
[self stopScanning];
}
- (void)addPeripheralToCache:(CBPeripheral *)peripheral data:(NSData *)data
{
dispatch_source_t timeoutTimer;
bool shouldLogData = true;
if ([_cachedPeripherals objectForKey:peripheral]) {
shouldLogData = ![data isEqualToData:_cachedPeripherals[peripheral][@"data"]];
if (shouldLogData) {
ChipLogProgress(Ble, "Updating peripheral %p from the cache", peripheral);
}
timeoutTimer = _cachedPeripherals[peripheral][@"timer"];
} else {
ChipLogProgress(Ble, "Adding peripheral %p to the cache", peripheral);
auto delegate = _scannerDelegate;
if (delegate) {
dispatch_async(_chipWorkQueue, ^{
ChipBLEDeviceIdentificationInfo info;
auto bytes = (const uint8_t *) [data bytes];
memcpy(&info, bytes, sizeof(info));
delegate->OnBleScanAdd((__bridge void *) peripheral, info);
});
}
timeoutTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _workQueue);
dispatch_source_set_event_handler(timeoutTimer, ^{
MATTER_LOG_METRIC(kMetricBLEPeripheralRemoved, ++self->_totalDevicesRemoved);
[self removePeripheralFromCache:peripheral];
});
dispatch_resume(timeoutTimer);
}
auto timeout = static_cast<int64_t>(kCachePeripheralTimeoutInSeconds * NSEC_PER_SEC);
dispatch_source_set_timer(timeoutTimer, dispatch_walltime(nullptr, timeout), DISPATCH_TIME_FOREVER, 5 * NSEC_PER_SEC);
// Add only unique count of devices found
if (!_cachedPeripherals[peripheral]) {
MATTER_LOG_METRIC(kMetricBLEPeripheralAdded, ++_totalDevicesAdded);
}
_cachedPeripherals[peripheral] = @{
@"data" : data,
@"timer" : timeoutTimer,
};
if (shouldLogData) {
ChipBLEDeviceIdentificationInfo info;
auto bytes = (const uint8_t *) [data bytes];
memcpy(&info, bytes, sizeof(info));
ChipLogProgress(Ble, " - Version: %u", info.GetAdvertisementVersion());
ChipLogProgress(Ble, " - Discriminator: %u", info.GetDeviceDiscriminator());
ChipLogProgress(Ble, " - VendorId: %u", info.GetVendorId());
ChipLogProgress(Ble, " - ProductId: %u", info.GetProductId());
}
}
- (void)removePeripheralFromCache:(CBPeripheral *)peripheral
{
auto entry = [_cachedPeripherals objectForKey:peripheral];
if (entry) {
ChipLogProgress(Ble, "Removing peripheral %p from the cache", peripheral);
dispatch_source_cancel(entry[@"timer"]);
[_cachedPeripherals removeObjectForKey:peripheral];
auto delegate = _scannerDelegate;
if (delegate) {
dispatch_async(_chipWorkQueue, ^{
delegate->OnBleScanRemove((__bridge void *) peripheral);
});
}
}
}
- (void)removePeripheralsFromCache
{
for (CBPeripheral * peripheral in [_cachedPeripherals allKeys]) {
[self removePeripheralFromCache:peripheral];
}
}
/**
* private static method to copy service and characteristic UUIDs from CBCharacteristic to a pair of ChipBleUUID objects.
* this is used in calls into Chip layer to decouple it from CoreBluetooth
*
* @param[in] characteristic the source characteristic
* @param[in] svcId the destination service UUID
* @param[in] charId the destination characteristic UUID
*
*/
+ (void)fillServiceWithCharacteristicUuids:(CBCharacteristic *)characteristic
svcId:(chip::Ble::ChipBleUUID *)svcId
charId:(chip::Ble::ChipBleUUID *)charId
{
static const size_t FullUUIDLength = 16;
if ((FullUUIDLength != sizeof(charId->bytes)) || (FullUUIDLength != sizeof(svcId->bytes))
|| (FullUUIDLength != characteristic.UUID.data.length)) {
// we're dead. we expect the data length to be the same (16-byte) across the board
ChipLogError(Ble, "UUID of characteristic is incompatible");
return;
}
memcpy(charId->bytes, characteristic.UUID.data.bytes, sizeof(charId->bytes));
memset(svcId->bytes, 0, sizeof(svcId->bytes));
// Expand service UUID back to 16-byte long as that's what the BLE Layer expects
// this is a buffer pre-filled with BLE service UUID Base
// byte 0 to 3 are reserved for shorter versions of BLE service UUIDs
// For 4-byte service UUIDs, all bytes from 0 to 3 are used
// For 2-byte service UUIDs, byte 0 and 1 shall be 0
uint8_t serviceFullUUID[FullUUIDLength]
= { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB };
switch (characteristic.service.UUID.data.length) {
case 2:
// copy the 2-byte service UUID onto the right offset
memcpy(serviceFullUUID + 2, characteristic.service.UUID.data.bytes, 2);
break;
case 4:
// flow through
case 16:
memcpy(serviceFullUUID, characteristic.service.UUID.data.bytes, characteristic.service.UUID.data.length);
break;
default:
// we're dead. we expect the data length to be the same (16-byte) across the board
ChipLogError(Ble, "Service UUIDs are incompatible");
}
memcpy(svcId->bytes, serviceFullUUID, sizeof(svcId->bytes));
}
- (void)setBleLayer:(chip::Ble::BleLayer *)bleLayer
{
_mBleLayer = bleLayer;
}
@end