blob: 2105989581e46948da0a9354d2e147da6f3d34ba [file] [log] [blame]
/*
*
* Copyright (c) 2020-2025 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/BleConnectionDelegateImpl.h>
#include <platform/Darwin/BleScannerDelegate.h>
#include <platform/Darwin/BleUtils.h>
#include <platform/LockTracker.h>
#include <setup_payload/SetupPayload.h>
#include <tracing/metric_event.h>
#include <algorithm>
#include <vector>
#import <CoreBluetooth/CoreBluetooth.h>
#import "PlatformMetricKeys.h"
using namespace chip::Ble;
using namespace chip::DeviceLayer;
using namespace chip::DeviceLayer::Internal;
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);
typedef NS_ENUM(uint8_t, BleConnectionMode) {
kScanning = 1,
kScanningWithTimeout,
kConnecting,
};
@interface MTRBleConnection : NSObject <CBCentralManagerDelegate, CBPeripheralDelegate>
@property (strong, nonatomic) dispatch_queue_t workQueue; // the CHIP work queue
@property (strong, nonatomic) CBCentralManager * centralManager;
@property (strong, nonatomic, nullable) CBPeripheral * peripheral;
// The discriminator that matches "peripheral"
@property (strong, nonatomic, nullable) NSNumber * matchedLongDiscriminator;
@property (strong, nonatomic, readonly, nullable) dispatch_source_t timer;
@property (assign, nonatomic, readonly) BleConnectionMode currentMode;
@property (strong, nonatomic) NSMutableDictionary<CBPeripheral *, NSDictionary *> * cachedPeripherals;
@property (assign, nonatomic) bool found;
@property (assign, nonatomic) std::vector<chip::SetupDiscriminator> desiredDiscriminators;
@property (assign, nonatomic) void * appState;
@property (assign, nonatomic) BleConnectionDelegate::OnConnectionCompleteFunct onConnectionComplete;
@property (assign, nonatomic) BleConnectionDelegate::OnConnectionByDiscriminatorsCompleteFunct onConnectionCompleteWithDiscriminator;
@property (assign, nonatomic) BleConnectionDelegate::OnConnectionErrorFunct onConnectionError;
@property (assign, nonatomic) chip::DeviceLayer::BleScannerDelegate * scannerDelegate;
@property (assign, nonatomic) chip::Ble::BleLayer * bleLayer;
- (instancetype)initWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm;
- (instancetype)initWithDiscriminators:(const chip::Span<const chip::SetupDiscriminator> &)desiredDiscriminators;
- (void)start;
- (void)stop;
- (void)updateWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm;
- (void)updateWithDiscriminators:(const chip::Span<const chip::SetupDiscriminator> &)desiredDiscriminators;
- (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 {
MTRBleConnection * ble;
void BleConnectionDelegateImpl::NewConnection(
Ble::BleLayer * bleLayer, void * appState, const SetupDiscriminator & deviceDiscriminator)
{
NewConnectionImpl(bleLayer, appState, chip::Span(&deviceDiscriminator, 1),
OnConnectionComplete, nullptr, OnConnectionError);
}
CHIP_ERROR BleConnectionDelegateImpl::NewConnection(BleLayer * bleLayer, void * appState, const Span<const SetupDiscriminator> & discriminators,
OnConnectionByDiscriminatorsCompleteFunct onConnectionComplete,
OnConnectionErrorFunct onConnectionError)
{
NewConnectionImpl(bleLayer, appState, discriminators, nullptr, onConnectionComplete, onConnectionError);
return CHIP_NO_ERROR;
}
void BleConnectionDelegateImpl::NewConnectionImpl(BleLayer * bleLayer, void * appState, const chip::Span<const SetupDiscriminator> & desiredDiscriminators,
BleConnectionDelegate::OnConnectionCompleteFunct onConnectionComplete,
BleConnectionDelegate::OnConnectionByDiscriminatorsCompleteFunct onConnectionCompleteWithDiscriminator,
BleConnectionDelegate::OnConnectionErrorFunct onConnectionError)
{
assertChipStackLockedByCurrentThread();
NSMutableArray * discriminatorStrings = [NSMutableArray arrayWithCapacity:desiredDiscriminators.size()];
for (auto & deviceDiscriminator : desiredDiscriminators) {
if (deviceDiscriminator.IsShortDiscriminator()) {
[discriminatorStrings addObject:[NSString stringWithFormat:@"%d (short, 0x%x)", deviceDiscriminator.GetShortValue(), deviceDiscriminator.GetShortValue()]];
} else {
[discriminatorStrings addObject:[NSString stringWithFormat:@"%d (long, 0x%03x)", deviceDiscriminator.GetLongValue(), deviceDiscriminator.GetLongValue()]];
}
}
// Some consumers of this code (like chip-tool) don't seem to
// implement logging in a way that supports %@ so far.
ChipLogProgress(Ble, "ConnectionDelegate NewConnection with discriminator list: [ %s ]",
[discriminatorStrings componentsJoinedByString:@", "].UTF8String);
// 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.bleLayer = bleLayer;
ble.appState = appState;
ble.onConnectionComplete = onConnectionComplete;
ble.onConnectionCompleteWithDiscriminator = onConnectionCompleteWithDiscriminator;
ble.onConnectionError = onConnectionError;
[ble updateWithDiscriminators:desiredDiscriminators];
return;
}
[ble stop];
ble = [[MTRBleConnection alloc] initWithDiscriminators:desiredDiscriminators];
ble.bleLayer = bleLayer;
ble.appState = appState;
ble.onConnectionComplete = onConnectionComplete;
ble.onConnectionCompleteWithDiscriminator = onConnectionCompleteWithDiscriminator;
ble.onConnectionError = onConnectionError;
ble.centralManager = [ble.centralManager initWithDelegate:ble queue:ble.workQueue];
}
void BleConnectionDelegateImpl::NewConnection(Ble::BleLayer * bleLayer, void * appState, BLE_CONNECTION_OBJECT connObj)
{
assertChipStackLockedByCurrentThread();
ChipLogProgress(Ble, "ConnectionDelegate NewConnection with conn obj: %p", connObj);
CBPeripheral * peripheral = CBPeripheralFromBleConnObject(connObj);
// The BLE_CONNECTION_OBJECT represents a CBPeripheral object. In order for it to be valid the central
// manager needs to still be running.
if (!ble || [ble isConnecting]) {
if (OnConnectionError) {
// Avoid calling back prior to returning
dispatch_async(PlatformMgrImpl().GetWorkQueue(), ^{
OnConnectionError(appState, CHIP_ERROR_INCORRECT_STATE);
});
}
return;
}
ble.bleLayer = 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"));
// 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 = [[MTRBleConnection alloc] initWithDelegate:delegate prewarm:prewarm];
// 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:ble.workQueue];
}
void BleConnectionDelegateImpl::StopScan()
{
assertChipStackLockedByCurrentThread();
ChipLogProgress(Ble, "ConnectionDelegate StopScan");
if (ble && !ble.isConnecting) {
[ble stop];
ble = nil;
}
}
CHIP_ERROR BleConnectionDelegateImpl::CancelConnection()
{
assertChipStackLockedByCurrentThread();
ChipLogProgress(Ble, "ConnectionDelegate CancelConnection");
if (ble && ble.isConnecting) {
[ble stop];
ble = nil;
}
return CHIP_NO_ERROR;
}
} // namespace Internal
} // namespace DeviceLayer
} // namespace chip
@interface MTRBleConnection ()
@property (nonatomic, readonly) int32_t totalDevicesAdded;
@property (nonatomic, readonly) int32_t totalDevicesRemoved;
@end
@implementation MTRBleConnection {
CBUUID * _chipServiceUUID;
}
- (instancetype)init
{
self = [super init];
if (self) {
_chipServiceUUID = CBUUIDFromBleUUID(chip::Ble::CHIP_BLE_SVC_ID);
_workQueue = chip::DeviceLayer::PlatformMgrImpl().GetWorkQueue();
_centralManager = [CBCentralManager alloc];
_found = false;
_cachedPeripherals = [[NSMutableDictionary alloc] init];
[self _resetCounters];
}
return self;
}
- (instancetype)initWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm
{
self = [self init];
if (self) {
_scannerDelegate = delegate;
if (prewarm) {
_currentMode = kScanningWithTimeout;
[self setupTimer:kPreWarmScanTimeoutInSeconds];
} else {
_currentMode = kScanning;
}
}
return self;
}
- (id)initWithDiscriminators:(const chip::Span<const chip::SetupDiscriminator> &)desiredDiscriminators
{
self = [self init];
if (self) {
_desiredDiscriminators = std::vector<chip::SetupDiscriminator>(desiredDiscriminators.begin(),
desiredDiscriminators.end());
_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
{
auto * onConnectionError = _onConnectionError;
auto * appState = _appState;
[self clearConnectionCallbacks];
if (onConnectionError != nullptr) {
onConnectionError(appState, error);
}
}
- (void)dispatchConnectionComplete:(CBPeripheral *)peripheral
{
auto * onConnectionComplete = _onConnectionComplete;
auto * onConnectionCompleteWithDiscriminator = _onConnectionCompleteWithDiscriminator;
auto * matchedLongDiscriminator = _matchedLongDiscriminator;
auto * appState = _appState;
[self clearConnectionCallbacks];
if (onConnectionCompleteWithDiscriminator != nullptr) {
// VerifyOrDie and VerifyOrDieWithMsg both seem to hit compiler errors:
// https://github.com/project-chip/connectedhomeip/issues/39135
if (matchedLongDiscriminator == nil) {
ChipLogError(Ble, "We should only have a peripheral without a discriminator if the "
"NewConnection entrypoint took a BLE_CONNECTION_OBJECT, and in that case "
"we would not be using onConnectionCompleteWithDiscriminator.");
VerifyOrDieWithoutLogging(matchedLongDiscriminator != nil);
}
onConnectionCompleteWithDiscriminator(appState, matchedLongDiscriminator.unsignedShortValue, BleConnObjectFromCBPeripheral(peripheral));
} else if (onConnectionComplete != nullptr) {
onConnectionComplete(appState, BleConnObjectFromCBPeripheral(peripheral));
}
}
- (void)clearConnectionCallbacks
{
_onConnectionComplete = nullptr;
_onConnectionError = nullptr;
_onConnectionCompleteWithDiscriminator = nullptr;
_appState = nullptr;
}
// Start CBCentralManagerDelegate
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
assertChipStackLockedByCurrentThread();
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
{
assertChipStackLockedByCurrentThread();
NSData * serviceData = advertisementData[CBAdvertisementDataServiceDataKey][_chipServiceUUID];
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 withLongDiscriminator:@(discriminator)];
// TODO: If we have multiple discriminators, consider continuing to
// scan for the other ones. We would need to not store the peripheral
// and discriminator in singleton members for that.
[self stopScanning];
} else {
[self addPeripheralToCache:peripheral data:serviceData];
}
}
- (BOOL)checkDiscriminator:(uint16_t)discriminator
{
return std::find_if(_desiredDiscriminators.begin(), _desiredDiscriminators.end(), [discriminator](const chip::SetupDiscriminator & desiredDiscriminator) {
return desiredDiscriminator.MatchesLongDiscriminator(discriminator);
}) != _desiredDiscriminators.end();
}
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
assertChipStackLockedByCurrentThread();
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
{
assertChipStackLockedByCurrentThread();
if (error != nil) {
ChipLogError(Ble, "Failed to discover services: %@", error);
}
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 isEqual:_chipServiceUUID] && !self.found) {
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredCharacteristics);
[peripheral discoverCharacteristics:nil forService:service];
self.found = true;
break;
}
}
if (!self.found || error != nil) {
ChipLogError(Ble, "Matter 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
{
assertChipStackLockedByCurrentThread();
MATTER_LOG_METRIC_END(kMetricBLEDiscoveredCharacteristics, CHIP_ERROR(chip::ChipError::Range::kOS, static_cast<uint32_t>(error.code)));
if (error != nil) {
ChipLogError(Ble, "Failed to discover characteristics: %@", error);
}
// XXX error ?
[self dispatchConnectionComplete:peripheral];
}
- (void)peripheral:(CBPeripheral *)peripheral
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
assertChipStackLockedByCurrentThread();
if (nil == error) {
ChipBleUUID svcId = BleUUIDFromCBUUD(characteristic.service.UUID);
ChipBleUUID charId = BleUUIDFromCBUUD(characteristic.UUID);
_bleLayer->HandleWriteConfirmation(BleConnObjectFromCBPeripheral(peripheral), &svcId, &charId);
} else {
ChipLogError(Ble, "Failed to write characteristic: %@", error);
MATTER_LOG_METRIC(kMetricBLEWriteChrValueFailed, BLE_ERROR_GATT_WRITE_FAILED);
_bleLayer->HandleConnectionError(BleConnObjectFromCBPeripheral(peripheral), BLE_ERROR_GATT_WRITE_FAILED);
}
}
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
assertChipStackLockedByCurrentThread();
bool isNotifying = characteristic.isNotifying;
if (nil == error) {
ChipBleUUID svcId = BleUUIDFromCBUUD(characteristic.service.UUID);
ChipBleUUID charId = BleUUIDFromCBUUD(characteristic.UUID);
if (isNotifying) {
_bleLayer->HandleSubscribeComplete(BleConnObjectFromCBPeripheral(peripheral), &svcId, &charId);
} else {
_bleLayer->HandleUnsubscribeComplete(BleConnObjectFromCBPeripheral(peripheral), &svcId, &charId);
}
} else {
ChipLogError(Ble, "BLE:Error subscribing/unsubcribing some characteristic on the device: [%s]",
[error.localizedDescription UTF8String]);
if (isNotifying) {
MATTER_LOG_METRIC(kMetricBLEUpdateNotificationStateForChrFailed, BLE_ERROR_GATT_WRITE_FAILED);
// we're still notifying, so we must failed the unsubscription
_bleLayer->HandleConnectionError(BleConnObjectFromCBPeripheral(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);
_bleLayer->HandleConnectionError(BleConnObjectFromCBPeripheral(peripheral), BLE_ERROR_GATT_SUBSCRIBE_FAILED);
}
}
}
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
assertChipStackLockedByCurrentThread();
if (nil == error) {
ChipBleUUID svcId = BleUUIDFromCBUUD(characteristic.service.UUID);
ChipBleUUID charId = BleUUIDFromCBUUD(characteristic.UUID);
auto * value = characteristic.value; // read immediately before dispatching
// 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 to allocate buffer for incoming BLE data");
MATTER_LOG_METRIC(kMetricBLEUpdateValueForChrFailed, CHIP_ERROR_NO_MEMORY);
_bleLayer->HandleConnectionError(BleConnObjectFromCBPeripheral(peripheral), CHIP_ERROR_NO_MEMORY);
} else if (!_bleLayer->HandleIndicationReceived(BleConnObjectFromCBPeripheral(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 to handle incoming BLE data");
MATTER_LOG_METRIC(kMetricBLEUpdateValueForChrFailed, CHIP_ERROR_INCORRECT_STATE);
}
} else {
ChipLogError(Ble, "Failed to receive characteristic indication: %@", error);
MATTER_LOG_METRIC(kMetricBLEUpdateValueForChrFailed, BLE_ERROR_GATT_INDICATE_FAILED);
_bleLayer->HandleConnectionError(BleConnObjectFromCBPeripheral(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 withLongDiscriminator:_matchedLongDiscriminator];
} else {
[self startScanning];
}
}
- (void)stop
{
[self detachScannerDelegate];
_found = false;
[self stopScanning];
[self removePeripheralsFromCache];
if (_peripheral) {
// Close all BLE connections before we release CB objects
// TODO: It's not clear that calling back into BLELayer is right here, since this method is
// called indirectly by BLELayer::CancelBleIncompleteConnection. There don't seem to be any
// other instances of platform delegates making this upwards call. It was introduced here:
// https://github.com/project-chip/connectedhomeip/pull/20299/files
_bleLayer->CloseAllBleConnections();
_peripheral = nil;
}
if (_centralManager) {
_centralManager.delegate = nil;
_centralManager = 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:@[ _chipServiceUUID ] options:scanOptions];
}
- (void)stopScanning
{
if (!_centralManager) {
return;
}
MATTER_LOG_METRIC_END(kMetricBLEScan);
[self _resetCounters];
[self clearTimer];
[_centralManager stopScan];
}
- (void)connect:(CBPeripheral *)peripheral withLongDiscriminator:(nullable NSNumber *)longDiscriminator
{
if (!_centralManager || !peripheral) {
return;
}
MATTER_LOG_METRIC_END(kMetricBLEDiscoveredMatchingPeripheral);
MATTER_LOG_METRIC_BEGIN(kMetricBLEConnectPeripheral);
_peripheral = peripheral;
_matchedLongDiscriminator = longDiscriminator;
auto * connectOptions = @{
@"kCBOptionUsecase" : @(271),
};
[_centralManager connectPeripheral:peripheral options:connectOptions];
}
- (void)detachScannerDelegate
{
auto * existingDelegate = _scannerDelegate;
if (existingDelegate) {
_scannerDelegate = nullptr;
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"];
ChipBLEDeviceIdentificationInfo info;
memcpy(&info, [serviceData bytes], sizeof(info));
delegate->OnBleScanAdd(BleConnObjectFromCBPeripheral(cachedPeripheral), info);
}
_scannerDelegate = delegate;
}
if (prewarm) {
_currentMode = kScanningWithTimeout;
[self setupTimer:kPreWarmScanTimeoutInSeconds];
} else {
_currentMode = kScanning;
[self clearTimer];
}
}
- (void)updateWithDiscriminators:(const chip::Span<const chip::SetupDiscriminator> &)desiredDiscriminators
{
[self detachScannerDelegate];
_desiredDiscriminators = std::vector<chip::SetupDiscriminator>(desiredDiscriminators.begin(),
desiredDiscriminators.end());
_currentMode = kConnecting;
CBPeripheral * peripheral = nil;
NSNumber * discriminator = 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;
discriminator = @(info.GetDeviceDiscriminator());
break;
}
}
[self removePeripheralsFromCache];
if (peripheral) {
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredMatchingPeripheral);
ChipLogProgress(Ble, "Connecting to cached device: %p", peripheral);
[self connect:peripheral withLongDiscriminator:discriminator];
// 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 withLongDiscriminator:nil];
[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) {
ChipBLEDeviceIdentificationInfo info;
auto bytes = (const uint8_t *) [data bytes];
memcpy(&info, bytes, sizeof(info));
delegate->OnBleScanAdd(BleConnObjectFromCBPeripheral(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) {
delegate->OnBleScanRemove(BleConnObjectFromCBPeripheral(peripheral));
}
}
}
- (void)removePeripheralsFromCache
{
for (CBPeripheral * peripheral in [_cachedPeripherals allKeys]) {
[self removePeripheralFromCache:peripheral];
}
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ %p mode=%d cbstate=%p peripheral=%@>",
self.class, self, _currentMode, _appState, _peripheral];
}
@end