Rework Darwin BLE layer (#37704)
* Rename Ble*Delegate headers to match .mm / class names
* Simplify BLEManagerImpl, we don't need to allocate the DelegateImpls separately
* Add some tests that exercise BLE code by mocking Core Bluetooth
Build with Bluetooth enabled in CI and enable Bluetooth mocking for all tests.
* Retain the underlying CBPeripheral in MTRCommissionableBrowserResult
This avoids a dangling pointer if the BleConnectionDelegateImpl drops the
peripheral from the cache while a client still holds the browser result.
Also document what BLE_CONNECTION_OBJECT is on Darwin.
* Use the CHIP queue instead of a separate BLE queue
* Tidy up BLE delegate headers
* Factor out connObj bridge casts into helper functions
* Simplify UUID handling, CBUUID does most of it already
* Review comments / tidyup
Add assertChipStackLockedByCurrentThread() to CB delegate methods.
Use assign instead of unsafe_unretained for non-retainable types.
Add comment to BLE Base UUID.
* Avoid leaks false positive in CoreBluetooth mock
* Final round of review comments
diff --git a/.github/workflows/darwin.yaml b/.github/workflows/darwin.yaml
index e2ee439..229c6cc 100644
--- a/.github/workflows/darwin.yaml
+++ b/.github/workflows/darwin.yaml
@@ -116,11 +116,10 @@
export TEST_RUNNER_ASAN_OPTIONS=__CURRENT_VALUE__:detect_stack_use_after_return=1
- # Disable BLE (CHIP_IS_BLE=NO) because the app does not have the permission to use it and that may crash the CI.
xcodebuild test -target "Matter" -scheme "Matter Framework Tests" \
-resultBundlePath /tmp/darwin/framework-tests/TestResults.xcresult \
-sdk macosx ${{ matrix.options.arguments }} \
- CHIP_IS_BLE=NO GCC_PREPROCESSOR_DEFINITIONS='${inherited} ${{ matrix.options.defines }}' \
+ GCC_PREPROCESSOR_DEFINITIONS='${inherited} ${{ matrix.options.defines }}' \
> >(tee /tmp/darwin/framework-tests/darwin-tests.log) 2> >(tee /tmp/darwin/framework-tests/darwin-tests-err.log >&2)
- name: Generate Summary
if: always()
diff --git a/src/controller/python/chip/ble/darwin/Scanning.mm b/src/controller/python/chip/ble/darwin/Scanning.mm
index 564a984..404008f 100644
--- a/src/controller/python/chip/ble/darwin/Scanning.mm
+++ b/src/controller/python/chip/ble/darwin/Scanning.mm
@@ -1,7 +1,7 @@
#include <ble/Ble.h>
#include <lib/support/CHIPMem.h>
#include <lib/support/logging/CHIPLogging.h>
-#include <platform/Darwin/MTRUUIDHelper.h>
+#include <platform/Darwin/BleUtils.h>
#import <CoreBluetooth/CoreBluetooth.h>
@@ -45,7 +45,7 @@
{
self = [super init];
if (self) {
- self.shortServiceUUID = [MTRUUIDHelper GetShortestServiceUUID:&chip::Ble::CHIP_BLE_SVC_ID];
+ self.shortServiceUUID = chip::DeviceLayer::Internal::CBUUIDFromBleUUID(chip::Ble::CHIP_BLE_SVC_ID);
_workQueue = dispatch_queue_create("com.chip.python.ble.work_queue", DISPATCH_QUEUE_SERIAL);
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _workQueue);
@@ -78,7 +78,7 @@
NSDictionary * servicesData = [advertisementData objectForKey:CBAdvertisementDataServiceDataKey];
for (CBUUID * serviceUUID in servicesData) {
- if (![serviceUUID.data isEqualToData:_shortServiceUUID.data]) {
+ if (![serviceUUID isEqualTo:_shortServiceUUID]) {
continue;
}
NSData * serviceData = [servicesData objectForKey:serviceUUID];
diff --git a/src/darwin/Framework/CHIP/MTRCommissionableBrowser.mm b/src/darwin/Framework/CHIP/MTRCommissionableBrowser.mm
index 8a111a6..0bbdc12 100644
--- a/src/darwin/Framework/CHIP/MTRCommissionableBrowser.mm
+++ b/src/darwin/Framework/CHIP/MTRCommissionableBrowser.mm
@@ -26,9 +26,11 @@
#include <controller/CHIPDeviceController.h>
#include <lib/dnssd/platform/Dnssd.h>
#include <platform/CHIPDeviceLayer.h>
+#include <platform/Darwin/BleUtils.h>
using namespace chip::Dnssd;
using namespace chip::DeviceLayer;
+using namespace chip::DeviceLayer::Internal;
#if CONFIG_NETWORK_LAYER_BLE
#include <platform/Darwin/BleScannerDelegate.h>
@@ -39,6 +41,8 @@
using namespace chip::Tracing::DarwinFramework;
+@class CBPeripheral;
+
@implementation MTRCommissionableBrowserResultInterfaces
@end
@@ -48,6 +52,7 @@
@property (nonatomic) NSNumber * productID;
@property (nonatomic) NSNumber * discriminator;
@property (nonatomic) BOOL commissioningMode;
+@property (nonatomic, strong, nullable) CBPeripheral * peripheral;
@end
@implementation MTRCommissionableBrowserResult
@@ -302,6 +307,7 @@
result.discriminator = @(info.GetDeviceDiscriminator());
result.commissioningMode = YES;
result.params = chip::MakeOptional(chip::Controller::SetUpCodePairerParameters(connObj, false /* connected */));
+ result.peripheral = CBPeripheralFromBleConnObject(connObj); // avoid params holding a dangling pointer
MATTER_LOG_METRIC(kMetricBLEDevicesAdded, ++mBLEDevicesAdded);
diff --git a/src/darwin/Framework/CHIPTests/MTRBleTests.m b/src/darwin/Framework/CHIPTests/MTRBleTests.m
new file mode 100644
index 0000000..8ec6698
--- /dev/null
+++ b/src/darwin/Framework/CHIPTests/MTRBleTests.m
@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2024 Project CHIP Authors
+ *
+ * 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.
+ */
+
+#import "MTRMockCB.h"
+#import "MTRTestCase.h"
+#import "MTRTestKeys.h"
+#import "MTRTestStorage.h"
+
+#import <Matter/Matter.h>
+#import <XCTest/XCTest.h>
+#import <stdatomic.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MTRBleTests : MTRTestCase
+@end
+
+@interface TestBrowserDelegate : NSObject <MTRCommissionableBrowserDelegate>
+@property (strong) void (^onDidFindCommissionableDevice)(MTRDeviceController *, MTRCommissionableBrowserResult *);
+@property (strong) void (^onDidRemoveCommissionableDevice)(MTRDeviceController *, MTRCommissionableBrowserResult *);
+@end
+
+@implementation TestBrowserDelegate
+
+- (void)controller:(nonnull MTRDeviceController *)controller didFindCommissionableDevice:(MTRCommissionableBrowserResult *)device
+{
+ __auto_type block = self.onDidFindCommissionableDevice;
+ if (block) {
+ block(controller, device);
+ }
+}
+
+- (void)controller:(nonnull MTRDeviceController *)controller didRemoveCommissionableDevice:(MTRCommissionableBrowserResult *)device
+{
+ __auto_type block = self.onDidRemoveCommissionableDevice;
+ if (block) {
+ block(controller, device);
+ }
+}
+
+@end
+
+MTRDeviceController * sController;
+
+@implementation MTRBleTests
+
+- (void)setUp
+{
+ [super setUp];
+
+ [self.class.mockCoreBluetooth reset];
+
+ sController = [MTRTestCase createControllerOnTestFabric];
+ XCTAssertNotNil(sController);
+}
+
+- (void)tearDown
+{
+ [sController shutdown];
+ sController = nil;
+ [[MTRDeviceControllerFactory sharedInstance] stopControllerFactory];
+
+ [super tearDown];
+}
+
+- (void)testBleCommissionableBrowserResultAdditionAndRemoval
+{
+ __block MTRCommissionableBrowserResult * device;
+ XCTestExpectation * didFindDevice = [self expectationWithDescription:@"did find device"];
+ TestBrowserDelegate * delegate = [[TestBrowserDelegate alloc] init];
+ delegate.onDidFindCommissionableDevice = ^(MTRDeviceController * controller, MTRCommissionableBrowserResult * result) {
+ if ([result.instanceName isEqualToString:@"BLE"]) { // TODO: This is a lame API
+ XCTAssertNil(device);
+ XCTAssertEqualObjects(result.vendorID, @0xfff1);
+ XCTAssertEqualObjects(result.productID, @0x1234);
+ XCTAssertEqualObjects(result.discriminator, @0x444);
+ device = result;
+ [didFindDevice fulfill];
+ }
+ };
+
+ XCTestExpectation * didRemoveDevice = [self expectationWithDescription:@"did remove device"];
+ delegate.onDidRemoveCommissionableDevice = ^(MTRDeviceController * controller, MTRCommissionableBrowserResult * result) {
+ if ([result.instanceName isEqualToString:@"BLE"]) {
+ XCTAssertNotNil(device);
+ XCTAssertEqual(result, device);
+ [didRemoveDevice fulfill];
+ }
+ };
+
+ XCTAssertTrue([sController startBrowseForCommissionables:delegate queue:dispatch_get_main_queue()]);
+
+ NSUUID * peripheralID = [NSUUID UUID];
+ [self.class.mockCoreBluetooth addMockCommissionableMatterDeviceWithIdentifier:peripheralID vendorID:@0xfff1 productID:@0x1234 discriminator:@0x444];
+ [self.class.mockCoreBluetooth removeMockPeripheralWithIdentifier:peripheralID];
+
+ // BleConnectionDelegateImpl kCachePeripheralTimeoutInSeconds is approximately 10 seconds
+ [self waitForExpectations:@[ didFindDevice, didRemoveDevice ] timeout:14 enforceOrder:YES];
+ XCTAssertTrue([sController stopBrowseForCommissionables]);
+}
+
+- (void)testBleCommissionAfterStopBrowseUAF
+{
+ __block MTRCommissionableBrowserResult * device;
+ XCTestExpectation * didFindDevice = [self expectationWithDescription:@"did find device"];
+ TestBrowserDelegate * delegate = [[TestBrowserDelegate alloc] init];
+ delegate.onDidFindCommissionableDevice = ^(MTRDeviceController * controller, MTRCommissionableBrowserResult * result) {
+ if ([result.instanceName isEqualToString:@"BLE"]) {
+ XCTAssertNil(device);
+ XCTAssertEqualObjects(result.vendorID, @0xfff1);
+ XCTAssertEqualObjects(result.productID, @0x1234);
+ XCTAssertEqualObjects(result.discriminator, @0x444);
+ device = result;
+ [didFindDevice fulfill];
+ }
+ };
+
+ XCTAssertTrue([sController startBrowseForCommissionables:delegate queue:dispatch_get_main_queue()]);
+
+ NSUUID * peripheralID = [NSUUID UUID];
+ [self.class.mockCoreBluetooth addMockCommissionableMatterDeviceWithIdentifier:peripheralID vendorID:@0xfff1 productID:@0x1234 discriminator:@0x444];
+ [self waitForExpectations:@[ didFindDevice ] timeout:2 enforceOrder:YES];
+
+ XCTAssertTrue([sController stopBrowseForCommissionables]);
+
+ // Attempt to use the MTRCommissionableBrowserResult after we stopped browsing
+ // This used to result in a UAF because BLE_CONNECTION_OBJECT is a void*
+ // carrying a CBPeripheral without retaining it. When browsing is stopped,
+ // BleConnectionDelegateImpl releases all cached CBPeripherals.
+ MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithSetupPasscode:@54321 discriminator:@0x444];
+ [sController setupCommissioningSessionWithDiscoveredDevice:device
+ payload:payload
+ newNodeID:@999
+ error:NULL];
+ [sController cancelCommissioningForNodeID:@999 error:NULL];
+}
+
+- (void)testShutdownBlePowerOffRaceUAF
+{
+ // Attempt a PASE connection over BLE, this will call BleConnectionDelegateImpl::NewConnection()
+ MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithSetupPasscode:@54321 discriminator:@0xb1e];
+ payload.discoveryCapabilities = MTRDiscoveryCapabilitiesBLE;
+ NSError * error;
+ XCTAssertTrue([sController setupCommissioningSessionWithPayload:payload newNodeID:@999 error:&error],
+ "setupCommissioningSessionWithPayload failed: %@", error);
+
+ // Create a race between shutdown and a CBManager callback that used to provoke a UAF.
+ // Note that on the order of 100 iterations can be necessary to reproduce the crash.
+ __block atomic_int tasks = 2;
+ dispatch_semaphore_t done = dispatch_semaphore_create(0);
+
+ dispatch_block_t shutdown = ^{
+ // Shut down the controller. This causes the SetupCodePairer to call
+ // BleConnectionDelegateImpl::CancelConnection(), then the SetupCodePairer
+ // is deallocated along with the DeviceCommissioner
+ [sController shutdown];
+ sController = nil;
+ if (atomic_fetch_sub(&tasks, 1) == 1) {
+ dispatch_semaphore_signal(done);
+ }
+ };
+ dispatch_block_t powerOff = ^{
+ // Cause CBPeripheralManager to signal a state change that
+ // triggers a callback to the SetupCodePairer
+ self.class.mockCoreBluetooth.state = CBManagerStatePoweredOff;
+ if (atomic_fetch_sub(&tasks, 1) == 1) {
+ dispatch_semaphore_signal(done);
+ }
+ };
+
+ dispatch_queue_t pool = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
+ dispatch_async(pool, shutdown);
+ dispatch_async(pool, powerOff);
+ dispatch_wait(done, DISPATCH_TIME_FOREVER);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIPTests/MTRCommissionableBrowserTests.m b/src/darwin/Framework/CHIPTests/MTRCommissionableBrowserTests.m
index d2b5f79..e5e8354 100644
--- a/src/darwin/Framework/CHIPTests/MTRCommissionableBrowserTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRCommissionableBrowserTests.m
@@ -17,6 +17,7 @@
#import <Matter/Matter.h>
+#import "MTRMockCB.h"
#import "MTRTestCase+ServerAppRunner.h"
#import "MTRTestCase.h"
#import "MTRTestKeys.h"
@@ -24,21 +25,20 @@
// Fixture 1: chip-all-clusters-app --KVS "$(mktemp -t chip-test-kvs)" --interface-id -1
-static const uint16_t kLocalPort = 5541;
static const uint16_t kTestVendorId = 0xFFF1u;
-static const __auto_type kTestProductIds = @[ @(0x8000u), @(0x8001u) ];
-static const __auto_type kTestDiscriminators = @[ @(2000), @(3839u), @(3840u) ];
+static const __auto_type kTestProductIds = @[ @(0x8000u), @(0x8001u), @(0x8002u) ];
+static const __auto_type kTestDiscriminators = @[ @(2000), @(3839u), @(3840u), @(0xb1e) ];
static const uint16_t kDiscoverDeviceTimeoutInSeconds = 10;
-static const uint16_t kExpectedDiscoveredDevicesCount = 3;
+static const uint16_t kExpectedDiscoveredDevicesCount = 4;
// Singleton controller we use.
static MTRDeviceController * sController = nil;
-static NSString * kInstanceNameKey = @"instanceName";
-static NSString * kVendorIDKey = @"vendorID";
-static NSString * kProductIDKey = @"productID";
-static NSString * kDiscriminatorKey = @"discriminator";
-static NSString * kCommissioningModeKey = @"commissioningMode";
+static NSString * const kInstanceNameKey = @"instanceName";
+static NSString * const kVendorIDKey = @"vendorID";
+static NSString * const kProductIDKey = @"productID";
+static NSString * const kDiscriminatorKey = @"discriminator";
+static NSString * const kCommissioningModeKey = @"commissioningMode";
static NSDictionary<NSString *, id> * ResultSnapshot(MTRCommissionableBrowserResult * result)
{
@@ -112,7 +112,9 @@
__auto_type discriminator = device.discriminator;
__auto_type commissioningMode = device.commissioningMode;
- XCTAssertEqual(instanceName.length, 16); // The instance name is random, so just ensure the len is right.
+ if (![instanceName isEqual:@"BLE"]) {
+ XCTAssertEqual(instanceName.length, 16); // The instance name is random, so just ensure the len is right.
+ }
XCTAssertEqualObjects(vendorId, @(kTestVendorId));
XCTAssertTrue([kTestProductIds containsObject:productId]);
XCTAssertTrue([kTestDiscriminators containsObject:discriminator]);
@@ -162,28 +164,9 @@
{
[super setUp];
- __auto_type * factory = [MTRDeviceControllerFactory sharedInstance];
- XCTAssertNotNil(factory);
+ sController = [MTRTestCase createControllerOnTestFabric];
- __auto_type * storage = [[MTRTestStorage alloc] init];
- __auto_type * factoryParams = [[MTRDeviceControllerFactoryParams alloc] initWithStorage:storage];
- factoryParams.port = @(kLocalPort);
-
- BOOL ok = [factory startControllerFactory:factoryParams error:nil];
- XCTAssertTrue(ok);
-
- __auto_type * testKeys = [[MTRTestKeys alloc] init];
- XCTAssertNotNil(testKeys);
-
- __auto_type * params = [[MTRDeviceControllerStartupParams alloc] initWithIPK:testKeys.ipk fabricID:@(1) nocSigner:testKeys];
- params.vendorID = @(kTestVendorId);
-
- MTRDeviceController * controller = [factory createControllerOnNewFabric:params error:nil];
- XCTAssertNotNil(controller);
-
- sController = controller;
-
- // Start the helper apps our tests use.
+ // Start the helper apps our tests use. Note these payloads match kTestDiscriminators etc.
for (NSString * payload in @[
@"MT:Y.K90SO527JA0648G00",
@"MT:-24J0AFN00I40648G00",
@@ -197,10 +180,10 @@
+ (void)tearDown
{
- MTRDeviceController * controller = sController;
- XCTAssertNotNil(controller);
- [controller shutdown];
- XCTAssertFalse([controller isRunning]);
+ XCTAssertNotNil(sController);
+ [sController shutdown];
+ XCTAssertFalse([sController isRunning]);
+ sController = nil;
[[MTRDeviceControllerFactory sharedInstance] stopControllerFactory];
@@ -211,6 +194,7 @@
{
[super setUp];
[self setContinueAfterFailure:NO];
+ [self.class.mockCoreBluetooth reset];
}
- (void)test001_StartBrowseAndStopBrowse
@@ -218,11 +202,26 @@
__auto_type delegate = [[DeviceScannerDelegate alloc] init];
dispatch_queue_t dispatchQueue = dispatch_queue_create("com.chip.discover", DISPATCH_QUEUE_SERIAL);
+ XCTestExpectation * bleScanExpectation = [self expectationWithDescription:@"did start BLE scan"];
+ self.class.mockCoreBluetooth.onScanForPeripheralsWithServicesOptions = ^(NSArray<CBUUID *> * _Nullable serviceUUIDs, NSDictionary<NSString *, id> * _Nullable options) {
+ XCTAssertEqual(serviceUUIDs.count, 1);
+ [bleScanExpectation fulfill];
+ };
+
// Start browsing
XCTAssertTrue([sController startBrowseForCommissionables:delegate queue:dispatchQueue]);
+ [self waitForExpectations:@[ bleScanExpectation ] timeout:1];
+
+ XCTestExpectation * bleStopExpectation = [self expectationWithDescription:@"did stop BLE scan"];
+ self.class.mockCoreBluetooth.onStopScan = ^{
+ [bleStopExpectation fulfill];
+ };
+
// Stop browsing
XCTAssertTrue([sController stopBrowseForCommissionables]);
+
+ [self waitForExpectations:@[ bleStopExpectation ] timeout:1];
}
- (void)test002_StartBrowseAndStopBrowseMultipleTimes
@@ -264,12 +263,18 @@
XCTAssertTrue([sController stopBrowseForCommissionables]);
}
-- (void)test005_StartBrowseGetCommissionableOverMdns
+- (void)test005_StartBrowseGetCommissionableOverMdnsAndBle
{
__auto_type expectation = [self expectationWithDescription:@"Commissionable devices Found"];
__auto_type delegate = [[DeviceScannerDelegate alloc] initWithExpectation:expectation];
dispatch_queue_t dispatchQueue = dispatch_queue_create("com.chip.discover", DISPATCH_QUEUE_SERIAL);
+ // Mock a commissionable device advertising over BLE
+ [self.class.mockCoreBluetooth addMockCommissionableMatterDeviceWithIdentifier:[NSUUID UUID]
+ vendorID:@(kTestVendorId)
+ productID:@0x8002
+ discriminator:@0xb1e];
+
// Start browsing
XCTAssertTrue([sController startBrowseForCommissionables:delegate queue:dispatchQueue]);
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRMockCB.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRMockCB.h
new file mode 100644
index 0000000..022b7b8
--- /dev/null
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRMockCB.h
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2025 Project CHIP Authors
+ *
+ * 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.
+ */
+
+#import <CoreBluetooth/CoreBluetooth.h>
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Instantiating this class will intercept any future calls that
+/// allocate `CBCentralManager` objects to return mock objects instead.
+/// Only one instance of this class should exist at any one time.
+@interface MTRMockCB : NSObject
+
+/// Stops mock `CBCentralManager` allocations and disables existing mocks.
+/// Failure to call this method may result in leaks or other issues.
+- (void)stopMocking;
+
+/// Resets `on*` hooks and mocked peripherals but does not stop mocking.
+- (void)reset;
+
+// Adds a mocked peripheral and causes any mocked `CBCentralManager`
+// instances to discover this as a `CBPeripheral`, if they are scanning.
+// The provided identifier becomes the `identifier` of the `CBPeripheral`.
+- (void)addMockPeripheralWithIdentifier:(NSUUID *)identifier
+ services:(NSArray<CBUUID *> *)services
+ advertisementData:(nullable NSDictionary<NSString *, id> *)advertisementData;
+
+// Convenience version of `addMockPeripheralWithIdentifier:...` that
+// advertises the relevant service with advertisement data as defined
+// in the "Matter BLE Service Data payload format" section of the spec.
+- (void)addMockCommissionableMatterDeviceWithIdentifier:(NSUUID *)identifier
+ vendorID:(NSNumber *)vendorID
+ productID:(NSNumber *)productID
+ discriminator:(NSNumber *)discriminator;
+
+- (void)removeMockPeripheralWithIdentifier:(NSUUID *)identifier;
+
+/// Mocked state. Defaults to CBManagerStatePoweredOn.
+@property (readwrite, assign) CBManagerState state;
+
+@property (readwrite, strong, nullable) void (^onScanForPeripheralsWithServicesOptions)
+ (NSArray<CBUUID *> * _Nullable serviceUUIDs, NSDictionary<NSString *, id> * _Nullable options);
+
+@property (readwrite, strong, nullable) void (^onStopScan)(void);
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRMockCB.m b/src/darwin/Framework/CHIPTests/TestHelpers/MTRMockCB.m
new file mode 100644
index 0000000..0098e27
--- /dev/null
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRMockCB.m
@@ -0,0 +1,690 @@
+/**
+ * Copyright (c) 2024 Project CHIP Authors
+ *
+ * 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.
+ */
+
+#import "MTRMockCB.h"
+
+#import "MTRDefines_Internal.h"
+
+#import <XCTest/XCTest.h>
+#import <objc/runtime.h>
+#import <os/log.h>
+#import <stdatomic.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MTRMockCBPeripheralDetails : NSObject
+
+- (instancetype)initWithIdentifier:(NSUUID *)identifier;
+
+@property (readonly, nonatomic, weak) MTRMockCB * mock;
+@property (readonly, nonatomic, strong) NSUUID * identifier;
+@property (readonly, nonatomic, copy) NSString * name;
+@property (readonly, nonatomic, copy) NSDictionary<NSString *, id> * advertisementData;
+
+@property (nonatomic, assign) CBPeripheralState state;
+@property (nonatomic, nullable, copy) NSDictionary<NSString *, id> * extraAdvertisementData;
+@property (nonatomic, copy) NSArray<CBUUID *> * services;
+
+@end
+
+@interface MTRMockCBCentralManager : NSObject
+
+- (instancetype)_initWithMock:(MTRMockCB *)mock;
+- (void)_didUpdateState;
+- (void)_maybeDiscoverPeripheral:(MTRMockCBPeripheralDetails *)details;
+
+// MARK: CBManager
+
+@property (nonatomic, assign, readonly) CBManagerState state;
+@property (nonatomic, assign, readonly) CBManagerAuthorization authorization;
+@property (class, nonatomic, assign, readonly) CBManagerAuthorization authorization;
+
+// MARK: CBCentralManager
+
+@property (nonatomic, weak, nullable) id<CBCentralManagerDelegate> delegate;
+@property (nonatomic, assign, readonly) BOOL isScanning;
+
++ (BOOL)supportsFeatures:(CBCentralManagerFeature)features;
+
+- (instancetype)init;
+- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
+ queue:(nullable dispatch_queue_t)queue;
+- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
+ queue:(nullable dispatch_queue_t)queue
+ options:(nullable NSDictionary<NSString *, id> *)options;
+
+- (NSArray<CBPeripheral *> *)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> *)identifiers;
+- (NSArray<CBPeripheral *> *)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> *)serviceUUIDs;
+- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;
+- (void)stopScan;
+- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary<NSString *, id> *)options;
+- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;
+- (void)registerForConnectionEventsWithOptions:(nullable NSDictionary<CBConnectionEventMatchingOption, id> *)options;
+
+@end
+
+@interface MTRMockCBPeripheral : NSObject <NSCopying>
+
+- (instancetype)_initWithDetails:(MTRMockCBPeripheralDetails *)details manager:(MTRMockCBCentralManager *)manager;
+
+@property (readonly, strong, nonatomic) MTRMockCBCentralManager * manager; // not API, but used by BlePlatformDelegateImpl via KVC
+
+// MARK: CBPeer
+
+@property (readonly, nonatomic) NSUUID * identifier;
+
+// MARK: CBPeripheral
+
+@property (weak, nonatomic, nullable) id<CBPeripheralDelegate> delegate;
+@property (retain, readonly, nullable) NSString * name;
+@property (retain, readonly, nullable) NSNumber * RSSI;
+@property (readonly) CBPeripheralState state;
+@property (retain, readonly, nullable) NSArray<CBService *> * services;
+@property (readonly) BOOL canSendWriteWithoutResponse;
+@property (readonly) BOOL ancsAuthorized;
+
+- (void)readRSSI;
+- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs;
+- (void)discoverIncludedServices:(nullable NSArray<CBUUID *> *)includedServiceUUIDs forService:(CBService *)service;
+- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)characteristicUUIDs forService:(CBService *)service;
+- (void)readValueForCharacteristic:(CBCharacteristic *)characteristic;
+- (NSUInteger)maximumWriteValueLengthForType:(CBCharacteristicWriteType)type;
+- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;
+- (void)setNotifyValue:(BOOL)enabled forCharacteristic:(CBCharacteristic *)characteristic;
+- (void)discoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic;
+- (void)readValueForDescriptor:(CBDescriptor *)descriptor;
+- (void)writeValue:(NSData *)data forDescriptor:(CBDescriptor *)descriptor;
+- (void)openL2CAPChannel:(CBL2CAPPSM)PSM;
+
+@end
+
+static NSString * CBManagerStateAsString(CBManagerState state)
+{
+ switch (state) {
+ case CBManagerStateUnknown:
+ return @"CBManagerStateUnknown";
+ case CBManagerStateResetting:
+ return @"CBManagerStateResetting";
+ case CBManagerStateUnsupported:
+ return @"CBManagerStateUnsupported";
+ case CBManagerStateUnauthorized:
+ return @"CBManagerStateUnauthorized";
+ case CBManagerStatePoweredOff:
+ return @"CBManagerStatePoweredOff";
+ case CBManagerStatePoweredOn:
+ return @"CBManagerStatePoweredOn";
+ }
+ return [NSString stringWithFormat:@"CBManagerState(%ld)", (long) state];
+}
+
+@implementation MTRMockCB {
+@package
+ os_log_t _log;
+ dispatch_queue_t _queue;
+ os_block_t _invalidate;
+ NSHashTable<MTRMockCBCentralManager *> * _managers;
+ NSMutableDictionary<NSUUID *, MTRMockCBPeripheralDetails *> * _peripherals;
+ CBManagerState _state;
+}
+
+static void InterceptClassMethod(__strong os_block_t * inOutCleanup, Class cls, SEL sel, id block)
+{
+ Method method = class_getClassMethod(cls, sel); // may return an inherited method
+ if (!method) {
+ NSString * reason = [NSString stringWithFormat:@"+[%@ %@] does not exist",
+ NSStringFromClass(cls), NSStringFromSelector(sel)];
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil];
+ }
+ IMP originalImp = method_getImplementation(method);
+
+ // Try to add the method first, in case it came from a super class.
+ // Note we need to pass the meta-class to class_addMethod().
+ IMP newImp = imp_implementationWithBlock(block);
+ if (class_addMethod(object_getClass(cls), sel, newImp, method_getTypeEncoding(method))) {
+ method = class_getClassMethod(cls, sel); // look up again so we clean up the method we added
+ } else {
+ method_setImplementation(method, newImp);
+ }
+
+ os_block_t nextCleanup = *inOutCleanup;
+ *inOutCleanup = ^{
+ // This isn't 100% correct if we added an override of a super-class method, because
+ // there is no API for removing a method. Instead we directly point it at the
+ // inherited implementation; this is good enough for our purposes here.
+ method_setImplementation(method, originalImp);
+ imp_removeBlock(newImp); // otherwise the block might leak
+ (void) block; // keep an obvious reference to avoid `leaks` false positives before cleanup
+ nextCleanup();
+ };
+}
+
+- (instancetype)init
+{
+ self = [super init];
+ _log = os_log_create("com.csa.matter", "mock");
+
+ static atomic_flag sInitialized = ATOMIC_FLAG_INIT;
+ if (atomic_flag_test_and_set(&sInitialized)) {
+ os_log_error(_log, "CoreBluetooth mocking is already enabled");
+ return nil;
+ }
+
+ mtr_weakify(self);
+ _invalidate = ^{
+ mtr_strongify(self);
+ self->_invalidate = nil;
+ atomic_flag_clear(&sInitialized);
+ };
+
+ _queue = dispatch_queue_create("mock.cb", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
+ dispatch_queue_set_specific(_queue, (__bridge void *) self, @YES, nil); // mark our queue
+
+ _managers = [NSHashTable weakObjectsHashTable];
+ _peripherals = [[NSMutableDictionary alloc] init];
+ _state = CBManagerStatePoweredOn;
+
+ os_log(_log, "Enabling CoreBluetooth mocking");
+
+ // Replace implementations of class methods we need to mock. We don't need to intercept
+ // any instance methods directly, because we're returning a mock object from `alloc`.
+ InterceptClassMethod(&_invalidate, CBCentralManager.class, @selector(alloc), ^id NS_RETURNS_RETAINED(void) {
+ mtr_strongify(self);
+ return self ? [[MTRMockCBCentralManager alloc] _initWithMock:self] : nil;
+ });
+ InterceptClassMethod(&_invalidate, CBCentralManager.class, @selector(supportsFeatures:), ^BOOL(CBCentralManagerFeature features) {
+ return [MTRMockCBCentralManager supportsFeatures:features];
+ });
+ InterceptClassMethod(&_invalidate, CBManager.class, @selector(authorization), ^CBManagerAuthorization(void) {
+ return [MTRMockCBCentralManager authorization];
+ });
+
+ return self;
+}
+
+- (void)sync:(void (^NS_NOESCAPE)(BOOL isValid))block
+{
+ // Allow `sync` to work like a recursive lock for convenience.
+ if (dispatch_get_specific((__bridge void *) self) != NULL) {
+ block(_invalidate != nil);
+ } else {
+ dispatch_sync(_queue, ^{
+ block(_invalidate != nil);
+ });
+ }
+}
+
+- (BOOL)isValid
+{
+ __block BOOL result;
+ [self sync:^(BOOL isValid) {
+ result = isValid;
+ }];
+ return result;
+}
+
+- (void)reset
+{
+ [self sync:^(BOOL isValid) {
+ [_peripherals removeAllObjects];
+ _onScanForPeripheralsWithServicesOptions = nil;
+ _onStopScan = nil;
+ }];
+}
+
+- (void)stopMocking
+{
+ [self sync:^(BOOL isValid) {
+ if (isValid) {
+ os_log(_log, "Disabling CoreBluetooth mocking");
+
+ _invalidate();
+ _invalidate = nil;
+
+ NSArray<MTRMockCBCentralManager *> * managers = [_managers allObjects];
+ _managers = nil;
+ _peripherals = nil;
+
+ if (_state != CBManagerStatePoweredOff) {
+ _state = CBManagerStatePoweredOff;
+ for (MTRMockCBCentralManager * manager in managers) {
+ [manager _didUpdateState];
+ }
+ }
+ }
+ }];
+}
+
+- (CBManagerState)state
+{
+ __block CBManagerState result;
+ [self sync:^(BOOL isValid) {
+ result = _state;
+ }];
+ return result;
+}
+
+- (void)setState:(CBManagerState)state
+{
+ [self sync:^(BOOL isValid) {
+ if (isValid && state != _state) {
+ _state = state;
+ for (MTRMockCBCentralManager * manager in _managers) {
+ [manager _didUpdateState];
+ }
+ }
+ }];
+}
+
+- (void)addMockPeripheralWithIdentifier:(NSUUID *)identifier
+ services:(nonnull NSArray<CBUUID *> *)services
+ advertisementData:(nullable NSDictionary<NSString *, id> *)advertisementData
+{
+ [self sync:^(BOOL isValid) {
+ if (isValid) {
+ MTRMockCBPeripheralDetails * details = _peripherals[identifier];
+ if (!details) {
+ details = [[MTRMockCBPeripheralDetails alloc] initWithIdentifier:identifier];
+ _peripherals[identifier] = details;
+ }
+ details.services = services;
+ details.extraAdvertisementData = advertisementData;
+
+ for (MTRMockCBCentralManager * manager in _managers) {
+ [manager _maybeDiscoverPeripheral:details];
+ }
+ }
+ }];
+}
+
+- (void)addMockCommissionableMatterDeviceWithIdentifier:(NSUUID *)identifier
+ vendorID:(NSNumber *)vendorID
+ productID:(NSNumber *)productID
+ discriminator:(NSNumber *)discriminator
+{
+ // Note: CBUUID transparently expands 16 or 32 bit UUIDs to 128 bit as required,
+ // however the current BLEConnectionDelegateImpl has its own comparison logic
+ // that does not treat short and long UUIDs as equivalent and only works with
+ // short UUIDs. The full UUID is "0000fff6-0000-1000-8000-00805f9b34fb".
+ CBUUID * matterServiceUUID = [CBUUID UUIDWithString:@"fff6"];
+
+ // See "Matter BLE Service Data payload format" in the spec, all fields little endian.
+ const uint8_t serviceDataBytes[] = {
+ 0x00, // OpCode 0x00 (Commissionable)
+ discriminator.unsignedIntValue & 0xff, (discriminator.unsignedIntValue >> 8) & 0xf,
+ vendorID.unsignedIntValue & 0xff, (vendorID.unsignedIntValue >> 8) & 0xff,
+ productID.unsignedIntValue & 0xff, (productID.unsignedIntValue >> 8) & 0xff,
+ 0x00, // Flags
+ };
+ NSData * matterServiceData = [NSData dataWithBytes:serviceDataBytes length:sizeof(serviceDataBytes)];
+
+ [self addMockPeripheralWithIdentifier:[NSUUID UUID]
+ services:@[ matterServiceUUID ]
+ advertisementData:@{ CBAdvertisementDataServiceDataKey : @ { matterServiceUUID : matterServiceData } }];
+}
+
+- (void)removeMockPeripheralWithIdentifier:(NSUUID *)identifier
+{
+ [self sync:^(BOOL isValid) {
+ if (isValid) {
+ _peripherals[identifier] = nil;
+ }
+ }];
+}
+
+@end
+
+@implementation MTRMockCBPeripheralDetails
+
+- (instancetype)initWithIdentifier:(NSUUID *)identifier
+{
+ self = [super init];
+ _identifier = identifier;
+ return self;
+}
+
+- (NSString *)name
+{
+ return _identifier.UUIDString;
+}
+
+- (NSDictionary<NSString *, id> *)advertisementData
+{
+ NSMutableDictionary * data = [[NSMutableDictionary alloc] init];
+ data[CBAdvertisementDataLocalNameKey] = self.name;
+ data[CBAdvertisementDataServiceUUIDsKey] = self.services;
+ data[CBAdvertisementDataIsConnectable] = @YES;
+ if (_extraAdvertisementData) {
+ [data addEntriesFromDictionary:_extraAdvertisementData];
+ }
+ return [data copy];
+}
+
+- (BOOL)matchesAnyServices:(nullable NSArray<CBUUID *> *)serviceUUIDs
+{
+ if (!serviceUUIDs) {
+ return YES; // special case
+ }
+ for (CBUUID * expected in serviceUUIDs) {
+ if ([self.services containsObject:expected]) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+@end
+
+@implementation MTRMockCBCentralManager {
+@package
+ MTRMockCB * _mock; // retain cycle (broken by stopMocking)
+ BOOL _initialized;
+
+ id<CBCentralManagerDelegate> __weak _Nullable _delegate;
+ dispatch_queue_t _delegateQueue;
+ NSDictionary<NSString *, id> * _Nullable _options;
+
+ NSArray<CBUUID *> * _Nullable _scanServiceUUIDs;
+ NSDictionary<NSString *, id> * _Nullable _scanOptions;
+}
+
+- (instancetype)_initWithMock:(MTRMockCB *)mock
+{
+ self = [super init];
+ _mock = mock;
+ return self;
+}
+
+- (BOOL)isKindOfClass:(Class)aClass
+{
+ return [super isKindOfClass:aClass] || aClass == CBManager.class || aClass == CBCentralManager.class;
+}
+
+- (NSString *)description
+{
+ __block NSString * result;
+ [_mock sync:^(BOOL isValid) {
+ result = [NSString stringWithFormat:@"<%@ %p %@ %@>",
+ self.class, self, CBManagerStateAsString(self.state), isValid ? @"valid" : @"defunct"];
+ }];
+ return result;
+}
+
+// MARK: CBManager
+
++ (CBManagerAuthorization)authorization
+{
+ return CBManagerAuthorizationAllowedAlways;
+}
+
+- (CBManagerAuthorization)authorization
+{
+ return [[self class] authorization];
+}
+
+- (CBManagerState)state
+{
+ return _mock.state;
+}
+
+// MARK: CBCentralManager
+
++ (BOOL)supportsFeatures:(CBCentralManagerFeature)features
+{
+ return NO;
+}
+
+- (instancetype)init
+{
+ return [self initWithDelegate:nil queue:nil options:nil];
+}
+
+- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
+ queue:(nullable dispatch_queue_t)queue
+{
+ return [self initWithDelegate:delegate queue:queue options:nil];
+}
+
+- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
+ queue:(nullable dispatch_queue_t)queue
+ options:(nullable NSDictionary<NSString *, id> *)options
+{
+ XCTAssertFalse(_initialized);
+ _initialized = YES;
+
+ _delegate = delegate;
+ _delegateQueue = queue ?: dispatch_get_main_queue();
+ _options = options;
+
+ [_mock sync:^(BOOL isValid) {
+ if (isValid) {
+ [_mock->_managers addObject:self];
+ [self _didUpdateState];
+ }
+ }];
+ return self;
+}
+
+- (NSArray<CBPeripheral *> *)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> *)identifiers
+{
+ return nil;
+}
+
+- (NSArray<CBPeripheral *> *)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> *)serviceUUIDs
+{
+ return nil;
+}
+
+- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs
+ options:(nullable NSDictionary<NSString *, id> *)options
+{
+ [_mock sync:^(BOOL isValid) {
+ _scanServiceUUIDs = [serviceUUIDs copy];
+ _scanOptions = [options copy];
+ _isScanning = YES;
+
+ if (isValid) {
+ __auto_type callback = _mock.onScanForPeripheralsWithServicesOptions;
+ if (callback) {
+ callback(serviceUUIDs, options);
+ }
+
+ for (MTRMockCBPeripheralDetails * details in _mock->_peripherals.allValues) {
+ [self _maybeDiscoverPeripheral:details];
+ }
+ }
+ }];
+}
+
+- (void)stopScan
+{
+ [_mock sync:^(BOOL isValid) {
+ _scanServiceUUIDs = nil;
+ _scanOptions = nil;
+ _isScanning = NO;
+
+ if (isValid) {
+ __auto_type callback = _mock.onStopScan;
+ if (callback) {
+ callback();
+ }
+ }
+ }];
+}
+
+- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary<NSString *, id> *)options
+{
+}
+
+- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral
+{
+}
+
+- (void)registerForConnectionEventsWithOptions:(nullable NSDictionary<CBConnectionEventMatchingOption, id> *)options
+{
+}
+
+// MARK: Internals
+
+- (void)_didUpdateState
+{
+ os_log(_mock->_log, "%@ didUpdateState", self);
+ dispatch_async(_delegateQueue, ^{
+ [self->_delegate centralManagerDidUpdateState:(id) self];
+ });
+}
+
+- (void)_maybeDiscoverPeripheral:(MTRMockCBPeripheralDetails *)details
+{
+ if (_isScanning && [details matchesAnyServices:_scanServiceUUIDs] &&
+ [_delegate respondsToSelector:@selector(centralManager:didDiscoverPeripheral:advertisementData:RSSI:)]) {
+ // TODO: Cache CBPeripheral mocks as long as the client keeps them alive?
+ MTRMockCBPeripheral * peripheral = [[MTRMockCBPeripheral alloc] _initWithDetails:details manager:self];
+ NSDictionary<NSString *, id> * advertisementData = details.advertisementData;
+ os_log(_mock->_log, "%@ didDiscoverPeripheral %@", self, peripheral);
+ dispatch_async(_delegateQueue, ^{
+ [self->_delegate centralManager:(id) self
+ didDiscoverPeripheral:(id) peripheral
+ advertisementData:advertisementData
+ RSSI:@127 /* Reserved for "RSSI not available" */];
+ });
+ }
+}
+
+@end
+
+@implementation MTRMockCBPeripheral {
+ MTRMockCBPeripheralDetails * _details;
+ MTRMockCBCentralManager * _manager;
+}
+
+- (instancetype)_initWithDetails:(MTRMockCBPeripheralDetails *)details manager:(MTRMockCBCentralManager *)manager
+{
+ self = [super init];
+ _details = details;
+ _manager = manager;
+ return self;
+}
+
+- (BOOL)isKindOfClass:(Class)aClass
+{
+ return [super isKindOfClass:aClass] || aClass == CBPeer.class || aClass == CBPeripheral.class;
+}
+
+- (NSString *)description
+{
+ __block NSString * result;
+ [_manager->_mock sync:^(BOOL isValid) {
+ result = [NSString stringWithFormat:@"<%@ %p %@ %@>", self.class, self, self.identifier, isValid ? @"valid" : @"defunct"];
+ }];
+ return result;
+}
+
+// MARK: CBPeer <NSCopying>
+
+- (BOOL)isEqual:(id)object
+{
+ return [object class] == [self class] && [((MTRMockCBPeripheral *) object).identifier isEqualTo:self.identifier];
+}
+
+- (NSUInteger)hash
+{
+ return self.identifier.hash;
+}
+
+- (id)copyWithZone:(nullable NSZone *)zone
+{
+ return self; // identifier is not mutable (and this is what CBPeer does)
+}
+
+- (NSUUID *)identifier
+{
+ return _details.identifier;
+}
+
+// MARK: CBPeripheral
+
+- (nullable NSString *)name
+{
+ return _details.identifier.UUIDString;
+}
+
+- (nullable NSNumber *)RSSI
+{
+ return nil;
+}
+
+- (CBPeripheralState)state
+{
+ return _details.state;
+}
+
+- (nullable NSArray<CBService *> *)services
+{
+ return nil;
+}
+
+- (void)readRSSI
+{
+}
+
+- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs
+{
+}
+
+- (void)discoverIncludedServices:(nullable NSArray<CBUUID *> *)includedServiceUUIDs forService:(CBService *)service
+{
+}
+
+- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)characteristicUUIDs forService:(CBService *)service
+{
+}
+
+- (void)readValueForCharacteristic:(CBCharacteristic *)characteristic
+{
+}
+
+- (NSUInteger)maximumWriteValueLengthForType:(CBCharacteristicWriteType)type
+{
+ return 512;
+}
+
+- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type
+{
+}
+
+- (void)setNotifyValue:(BOOL)enabled forCharacteristic:(CBCharacteristic *)characteristic
+{
+}
+
+- (void)discoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic
+{
+}
+
+- (void)readValueForDescriptor:(CBDescriptor *)descriptor
+{
+}
+
+- (void)writeValue:(NSData *)data forDescriptor:(CBDescriptor *)descriptor
+{
+}
+
+- (void)openL2CAPChannel:(CBL2CAPPSM)PSM
+{
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.h
index 3b404f6..d698ea8 100644
--- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.h
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.h
@@ -22,14 +22,27 @@
#define HAVE_NSTASK 1
#endif
+@class MTRDeviceController;
+@class MTRMockCB;
+
NS_ASSUME_NONNULL_BEGIN
@interface MTRTestCase : XCTestCase
+
// It would be nice to do the leak-detection automatically, but running "leaks"
// on every single sub-test is slow, and some of our tests seem to have leaks
// outside Matter.framework. So have it be opt-in for now, and improve later.
@property (nonatomic) BOOL detectLeaks;
+// Creates a device controller on a new fabric with test keys and test storage.
++ (MTRDeviceController *)createControllerOnTestFabric;
+
+// Provides access to the mock CoreBlueooth instance managed automatically by
+// this class. Bluetooth mocking is enabled for all tests (even those that don't
+// actively interact with it) to avoid issues with accessing the real Bluetooth
+// implementation in CI.
+@property (class, readonly) MTRMockCB * mockCoreBluetooth;
+
#if HAVE_NSTASK
/**
* Create an NSTask for the given path. Path should be relative to the Matter
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.mm b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.mm
index 915df78..02be960 100644
--- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.mm
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.mm
@@ -14,14 +14,20 @@
* limitations under the License.
*/
+#import "MTRTestCase.h"
+
+#import "MTRMockCB.h"
+#import "MTRTestKeys.h"
+#import "MTRTestStorage.h"
+
#include <stdlib.h>
#include <unistd.h>
-#import "MTRTestCase.h"
-
#if HAVE_NSTASK
// Tasks that are not scoped to a specific test, but rather to a specific test suite.
-static NSMutableSet<NSTask *> * runningCrossTestTasks;
+static NSMutableSet<NSTask *> * sRunningCrossTestTasks;
+
+static MTRMockCB * sMockCB;
static void ClearTaskSet(NSMutableSet<NSTask *> * __strong & tasks)
{
@@ -43,16 +49,26 @@
{
[super setUp];
+ sMockCB = [[MTRMockCB alloc] init];
+
#if HAVE_NSTASK
- runningCrossTestTasks = [[NSMutableSet alloc] init];
+ sRunningCrossTestTasks = [[NSMutableSet alloc] init];
#endif // HAVE_NSTASK
}
+ (void)tearDown
{
#if HAVE_NSTASK
- ClearTaskSet(runningCrossTestTasks);
+ ClearTaskSet(sRunningCrossTestTasks);
#endif // HAVE_NSTASK
+
+ [sMockCB stopMocking];
+ sMockCB = nil;
+}
+
++ (MTRMockCB *)mockCoreBluetooth
+{
+ return sMockCB;
}
- (void)setUp
@@ -106,6 +122,22 @@
[super tearDown];
}
++ (id)createControllerOnTestFabric
+{
+ __auto_type * storage = [[MTRTestStorage alloc] init];
+ __auto_type * factoryParams = [[MTRDeviceControllerFactoryParams alloc] initWithStorage:storage];
+ __auto_type * factory = MTRDeviceControllerFactory.sharedInstance;
+ XCTAssertTrue([factory startControllerFactory:factoryParams error:nil]);
+
+ __auto_type * testKeys = [[MTRTestKeys alloc] init];
+ __auto_type * params = [[MTRDeviceControllerStartupParams alloc] initWithIPK:testKeys.ipk fabricID:@1 nocSigner:testKeys];
+ params.vendorID = @0xFFF1;
+ MTRDeviceController * controller = [factory createControllerOnNewFabric:params error:nil];
+ XCTAssertNotNil(controller);
+
+ return controller;
+}
+
#if HAVE_NSTASK
- (NSTask *)createTaskForPath:(NSString *)path
{
@@ -147,7 +179,7 @@
{
[self doLaunchTask:task];
- [runningCrossTestTasks addObject:task];
+ [sRunningCrossTestTasks addObject:task];
}
#endif // HAVE_NSTASK
diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
index 940b1eb..6bdeae3 100644
--- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
+++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
@@ -132,10 +132,12 @@
3DA1A3552ABAB3B4004F0BB9 /* MTRAsyncWorkQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DA1A3522ABAB3B4004F0BB9 /* MTRAsyncWorkQueue.h */; };
3DA1A3562ABAB3B4004F0BB9 /* MTRAsyncWorkQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3DA1A3532ABAB3B4004F0BB9 /* MTRAsyncWorkQueue.mm */; };
3DA1A3582ABABF6A004F0BB9 /* MTRAsyncWorkQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DA1A3572ABABF69004F0BB9 /* MTRAsyncWorkQueueTests.m */; };
+ 3DB9DAE52D67EE5A00704FAB /* MTRBleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DB9DAE42D67EE5A00704FAB /* MTRBleTests.m */; };
3DECCB6E29347D2D00585AEC /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DECCB6D29347D2C00585AEC /* Security.framework */; };
3DECCB702934AECD00585AEC /* MTRLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DECCB6F2934AC1C00585AEC /* MTRLogging.h */; settings = {ATTRIBUTES = (Public, ); }; };
3DECCB722934AFE200585AEC /* MTRLogging.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3DECCB712934AFE200585AEC /* MTRLogging.mm */; };
3DECCB742934C21B00585AEC /* MTRDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DECCB732934C21B00585AEC /* MTRDefines.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 3DF5219E2D62C3E5008F8E52 /* MTRMockCB.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DF5219D2D62C3E5008F8E52 /* MTRMockCB.m */; };
3DFCB3292966684500332B35 /* MTRCertificateInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DFCB3282966684500332B35 /* MTRCertificateInfoTests.m */; };
3DFCB32C29678C9500332B35 /* MTRConversion.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DFCB32B29678C9500332B35 /* MTRConversion.h */; };
51029DF6293AA6100087AFB0 /* MTROperationalCertificateIssuer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51029DF5293AA6100087AFB0 /* MTROperationalCertificateIssuer.mm */; };
@@ -653,18 +655,21 @@
3DA1A3522ABAB3B4004F0BB9 /* MTRAsyncWorkQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRAsyncWorkQueue.h; sourceTree = "<group>"; };
3DA1A3532ABAB3B4004F0BB9 /* MTRAsyncWorkQueue.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRAsyncWorkQueue.mm; sourceTree = "<group>"; };
3DA1A3572ABABF69004F0BB9 /* MTRAsyncWorkQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRAsyncWorkQueueTests.m; sourceTree = "<group>"; };
+ 3DB9DAE42D67EE5A00704FAB /* MTRBleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTRBleTests.m; sourceTree = "<group>"; };
+ 3DB9DAE92D754C7200704FAB /* BleUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BleUtils.h; sourceTree = "<group>"; };
+ 3DB9DAEA2D754C7200704FAB /* BleUtils.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BleUtils.mm; sourceTree = "<group>"; };
3DECCB6D29347D2C00585AEC /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; };
3DECCB6F2934AC1C00585AEC /* MTRLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRLogging.h; sourceTree = "<group>"; };
3DECCB712934AFE200585AEC /* MTRLogging.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRLogging.mm; sourceTree = "<group>"; };
3DECCB732934C21B00585AEC /* MTRDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRDefines.h; sourceTree = "<group>"; };
- 3DF521682D5E90BE008F8E52 /* BleApplicationDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BleApplicationDelegate.h; sourceTree = "<group>"; };
+ 3DF521682D5E90BE008F8E52 /* BleApplicationDelegateImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BleApplicationDelegateImpl.h; sourceTree = "<group>"; };
3DF521692D5E90BE008F8E52 /* BleApplicationDelegateImpl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BleApplicationDelegateImpl.mm; sourceTree = "<group>"; };
- 3DF5216A2D5E90BE008F8E52 /* BleConnectionDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BleConnectionDelegate.h; sourceTree = "<group>"; };
+ 3DF5216A2D5E90BE008F8E52 /* BleConnectionDelegateImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BleConnectionDelegateImpl.h; sourceTree = "<group>"; };
3DF5216B2D5E90BE008F8E52 /* BleConnectionDelegateImpl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BleConnectionDelegateImpl.mm; sourceTree = "<group>"; };
3DF5216C2D5E90BE008F8E52 /* BLEManagerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BLEManagerImpl.h; sourceTree = "<group>"; };
3DF5216D2D5E90BE008F8E52 /* BLEManagerImpl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = BLEManagerImpl.cpp; sourceTree = "<group>"; };
3DF5216E2D5E90BE008F8E52 /* BlePlatformConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BlePlatformConfig.h; sourceTree = "<group>"; };
- 3DF5216F2D5E90BE008F8E52 /* BlePlatformDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BlePlatformDelegate.h; sourceTree = "<group>"; };
+ 3DF5216F2D5E90BE008F8E52 /* BlePlatformDelegateImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BlePlatformDelegateImpl.h; sourceTree = "<group>"; };
3DF521702D5E90BE008F8E52 /* BlePlatformDelegateImpl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BlePlatformDelegateImpl.mm; sourceTree = "<group>"; };
3DF521712D5E90BE008F8E52 /* BleScannerDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BleScannerDelegate.h; sourceTree = "<group>"; };
3DF521722D5E90BE008F8E52 /* BUILD.gn */ = {isa = PBXFileReference; lastKnownFileType = text; path = BUILD.gn; sourceTree = "<group>"; };
@@ -695,8 +700,6 @@
3DF5218B2D5E90BE008F8E52 /* Logging.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Logging.mm; sourceTree = "<group>"; };
3DF5218C2D5E90BE008F8E52 /* MdnsError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MdnsError.h; sourceTree = "<group>"; };
3DF5218D2D5E90BE008F8E52 /* MdnsError.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MdnsError.cpp; sourceTree = "<group>"; };
- 3DF5218E2D5E90BE008F8E52 /* MTRUUIDHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRUUIDHelper.h; sourceTree = "<group>"; };
- 3DF5218F2D5E90BE008F8E52 /* MTRUUIDHelperImpl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRUUIDHelperImpl.mm; sourceTree = "<group>"; };
3DF521902D5E90BE008F8E52 /* NetworkCommissioningDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NetworkCommissioningDriver.h; sourceTree = "<group>"; };
3DF521912D5E90BE008F8E52 /* PlatformManagerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlatformManagerImpl.h; sourceTree = "<group>"; };
3DF521922D5E90BE008F8E52 /* PlatformManagerImpl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PlatformManagerImpl.cpp; sourceTree = "<group>"; };
@@ -709,6 +712,8 @@
3DF521992D5E90BE008F8E52 /* Tracing.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Tracing.mm; sourceTree = "<group>"; };
3DF5219A2D5E90BE008F8E52 /* UserDefaults.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserDefaults.h; sourceTree = "<group>"; };
3DF5219B2D5E90BE008F8E52 /* UserDefaults.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = UserDefaults.mm; sourceTree = "<group>"; };
+ 3DF5219C2D62C3E5008F8E52 /* MTRMockCB.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRMockCB.h; sourceTree = "<group>"; };
+ 3DF5219D2D62C3E5008F8E52 /* MTRMockCB.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTRMockCB.m; sourceTree = "<group>"; };
3DFCB3282966684500332B35 /* MTRCertificateInfoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTRCertificateInfoTests.m; sourceTree = "<group>"; };
3DFCB32A2966827F00332B35 /* MTRDefines_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRDefines_Internal.h; sourceTree = "<group>"; };
3DFCB32B29678C9500332B35 /* MTRConversion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRConversion.h; sourceTree = "<group>"; };
@@ -1400,16 +1405,18 @@
isa = PBXGroup;
children = (
3DF521722D5E90BE008F8E52 /* BUILD.gn */,
- 3DF521682D5E90BE008F8E52 /* BleApplicationDelegate.h */,
+ 3DF521682D5E90BE008F8E52 /* BleApplicationDelegateImpl.h */,
3DF521692D5E90BE008F8E52 /* BleApplicationDelegateImpl.mm */,
- 3DF5216A2D5E90BE008F8E52 /* BleConnectionDelegate.h */,
+ 3DF5216A2D5E90BE008F8E52 /* BleConnectionDelegateImpl.h */,
3DF5216B2D5E90BE008F8E52 /* BleConnectionDelegateImpl.mm */,
3DF5216C2D5E90BE008F8E52 /* BLEManagerImpl.h */,
3DF5216D2D5E90BE008F8E52 /* BLEManagerImpl.cpp */,
3DF5216E2D5E90BE008F8E52 /* BlePlatformConfig.h */,
- 3DF5216F2D5E90BE008F8E52 /* BlePlatformDelegate.h */,
+ 3DF5216F2D5E90BE008F8E52 /* BlePlatformDelegateImpl.h */,
3DF521702D5E90BE008F8E52 /* BlePlatformDelegateImpl.mm */,
3DF521712D5E90BE008F8E52 /* BleScannerDelegate.h */,
+ 3DB9DAE92D754C7200704FAB /* BleUtils.h */,
+ 3DB9DAEA2D754C7200704FAB /* BleUtils.mm */,
3DF521732D5E90BE008F8E52 /* CHIPDevicePlatformConfig.h */,
3DF521742D5E90BE008F8E52 /* CHIPDevicePlatformEvent.h */,
3DF521752D5E90BE008F8E52 /* CHIPPlatformConfig.h */,
@@ -1437,8 +1444,6 @@
3DF5218B2D5E90BE008F8E52 /* Logging.mm */,
3DF5218C2D5E90BE008F8E52 /* MdnsError.h */,
3DF5218D2D5E90BE008F8E52 /* MdnsError.cpp */,
- 3DF5218E2D5E90BE008F8E52 /* MTRUUIDHelper.h */,
- 3DF5218F2D5E90BE008F8E52 /* MTRUUIDHelperImpl.mm */,
3DF521902D5E90BE008F8E52 /* NetworkCommissioningDriver.h */,
3DF521912D5E90BE008F8E52 /* PlatformManagerImpl.h */,
3DF521922D5E90BE008F8E52 /* PlatformManagerImpl.cpp */,
@@ -1480,6 +1485,8 @@
75B0D01C2B71B46F002074DD /* MTRDeviceTestDelegate.h */,
75B0D01D2B71B47F002074DD /* MTRDeviceTestDelegate.m */,
75139A6C2B7FE19100E3A919 /* MTRTestDeclarations.h */,
+ 3DF5219C2D62C3E5008F8E52 /* MTRMockCB.h */,
+ 3DF5219D2D62C3E5008F8E52 /* MTRMockCB.m */,
);
path = TestHelpers;
sourceTree = "<group>";
@@ -1796,6 +1803,7 @@
3DA1A3572ABABF69004F0BB9 /* MTRAsyncWorkQueueTests.m */,
3D3928D62BBCEA3D00CDEBB2 /* MTRAvailabilityTests.m */,
51669AEF2913204400F4AA36 /* MTRBackwardsCompatTests.m */,
+ 3DB9DAE42D67EE5A00704FAB /* MTRBleTests.m */,
3DFCB3282966684500332B35 /* MTRCertificateInfoTests.m */,
517BF3F2282B62CB00A8B7DB /* MTRCertificateTests.m */,
51339B1E2A0DA64D00C798C1 /* MTRCertificateValidityTests.m */,
@@ -2594,6 +2602,7 @@
8874C1322B69C7060084BEFD /* MTRMetricsTests.m in Sources */,
1E5801C328941C050033A199 /* MTRTestOTAProvider.m in Sources */,
5A6FEC9D27B5E48900F25F42 /* MTRXPCProtocolTests.m in Sources */,
+ 3DB9DAE52D67EE5A00704FAB /* MTRBleTests.m in Sources */,
1EE0805E2A44875E008A03C2 /* MTRCommissionableBrowserTests.m in Sources */,
518D3F832AA132DC008E0007 /* MTRTestPerControllerStorage.m in Sources */,
51339B1F2A0DA64D00C798C1 /* MTRCertificateValidityTests.m in Sources */,
@@ -2605,6 +2614,7 @@
75B0D01E2B71B47F002074DD /* MTRDeviceTestDelegate.m in Sources */,
3D0C484B29DA4FA0006D811F /* MTRErrorTests.m in Sources */,
3DA1A3582ABABF6A004F0BB9 /* MTRAsyncWorkQueueTests.m in Sources */,
+ 3DF5219E2D62C3E5008F8E52 /* MTRMockCB.m in Sources */,
51742B4A29CB5FC1009974FE /* MTRTestResetCommissioneeHelper.m in Sources */,
5AE6D4E427A99041001F2493 /* MTRDeviceTests.m in Sources */,
510CECA8297F72970064E0B3 /* MTROperationalCertificateIssuerTests.m in Sources */,
diff --git a/src/platform/Darwin/BLEManagerImpl.cpp b/src/platform/Darwin/BLEManagerImpl.cpp
index 8ff634c..080d75e 100644
--- a/src/platform/Darwin/BLEManagerImpl.cpp
+++ b/src/platform/Darwin/BLEManagerImpl.cpp
@@ -1,6 +1,6 @@
/*
*
- * Copyright (c) 2020 Project CHIP Authors
+ * Copyright (c) 2020-2025 Project CHIP Authors
* Copyright (c) 2018 Nest Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,9 +26,9 @@
#include <ble/Ble.h>
#include <lib/core/Global.h>
#include <lib/support/logging/CHIPLogging.h>
-#include <platform/Darwin/BleApplicationDelegate.h>
-#include <platform/Darwin/BleConnectionDelegate.h>
-#include <platform/Darwin/BlePlatformDelegate.h>
+#include <platform/Darwin/BleApplicationDelegateImpl.h>
+#include <platform/Darwin/BleConnectionDelegateImpl.h>
+#include <platform/Darwin/BlePlatformDelegateImpl.h>
#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
@@ -43,61 +43,29 @@
CHIP_ERROR BLEManagerImpl::_Init()
{
- CHIP_ERROR err;
+ ChipLogDetail(DeviceLayer, "Initializing BLE Manager");
- ChipLogDetail(DeviceLayer, "%s", __FUNCTION__);
-
- // Initialize the Chip BleLayer.
- BleApplicationDelegateImpl * appDelegate = new BleApplicationDelegateImpl();
- BleConnectionDelegateImpl * connDelegate = new BleConnectionDelegateImpl();
- BlePlatformDelegateImpl * platformDelegate = new BlePlatformDelegateImpl();
-
- mApplicationDelegate = appDelegate;
- mConnectionDelegate = connDelegate;
- mPlatformDelegate = platformDelegate;
-
- err = BleLayer::Init(platformDelegate, connDelegate, appDelegate, &DeviceLayer::SystemLayer());
-
- if (CHIP_NO_ERROR != err)
- {
- _Shutdown();
- }
-
- return err;
+ // Initialize the CHIP BleLayer. The application, connection, and platform delegate
+ // implementations are all stateless classes that we inherit from privately.
+ return BleLayer::Init(this, this, this, &DeviceLayer::SystemLayer());
}
void BLEManagerImpl::_Shutdown()
{
- if (mApplicationDelegate)
- {
- delete mApplicationDelegate;
- mApplicationDelegate = nullptr;
- }
-
- if (mConnectionDelegate)
- {
- delete mConnectionDelegate;
- mConnectionDelegate = nullptr;
- }
-
- if (mPlatformDelegate)
- {
- delete mPlatformDelegate;
- mPlatformDelegate = nullptr;
- }
+ // Nothing to do
}
CHIP_ERROR BLEManagerImpl::StartScan(BleScannerDelegate * delegate, BleScanMode mode)
{
- VerifyOrReturnError(mConnectionDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
- static_cast<BleConnectionDelegateImpl *>(mConnectionDelegate)->StartScan(delegate, mode);
+ VerifyOrReturnError(BleLayer::IsInitialized(), CHIP_ERROR_INCORRECT_STATE);
+ BleConnectionDelegateImpl::StartScan(delegate, mode);
return CHIP_NO_ERROR;
}
CHIP_ERROR BLEManagerImpl::StopScan()
{
- VerifyOrReturnError(mConnectionDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
- static_cast<BleConnectionDelegateImpl *>(mConnectionDelegate)->StopScan();
+ VerifyOrReturnError(BleLayer::IsInitialized(), CHIP_ERROR_INCORRECT_STATE);
+ BleConnectionDelegateImpl::StopScan();
return CHIP_NO_ERROR;
}
diff --git a/src/platform/Darwin/BLEManagerImpl.h b/src/platform/Darwin/BLEManagerImpl.h
index 194dadb..48439b9 100644
--- a/src/platform/Darwin/BLEManagerImpl.h
+++ b/src/platform/Darwin/BLEManagerImpl.h
@@ -1,6 +1,6 @@
/*
*
- * Copyright (c) 2020 Project CHIP Authors
+ * Copyright (c) 2020-2025 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,8 +23,12 @@
#pragma once
+#include <ble/Ble.h>
#include <lib/core/Global.h>
#include <lib/support/CodeUtils.h>
+#include <platform/Darwin/BleApplicationDelegateImpl.h>
+#include <platform/Darwin/BleConnectionDelegateImpl.h>
+#include <platform/Darwin/BlePlatformDelegateImpl.h>
#include <platform/Darwin/BleScannerDelegate.h>
#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
@@ -38,14 +42,18 @@
/**
* Concrete implementation of the BLEManagerImpl singleton object for the Darwin platforms.
*/
-class BLEManagerImpl final : public BLEManager, private BleLayer
+class BLEManagerImpl final : public BLEManager,
+ private BleLayer,
+ private BleApplicationDelegateImpl,
+ private BleConnectionDelegateImpl,
+ private BlePlatformDelegateImpl
{
// Allow the BLEManager interface class to delegate method calls to
// the implementation methods provided by this class.
friend BLEManager;
public:
- CHIP_ERROR ConfigureBle(uint32_t aNodeId, bool aIsCentral) { return CHIP_NO_ERROR; }
+ CHIP_ERROR ConfigureBle(uint32_t bleDeviceId, bool aIsCentral) { return CHIP_NO_ERROR; }
CHIP_ERROR StartScan(BleScannerDelegate * delegate, BleScanMode mode = BleScanMode::kDefault);
CHIP_ERROR StopScan();
@@ -70,10 +78,6 @@
friend BLEManagerImpl & BLEMgrImpl(void);
static Global<BLEManagerImpl> sInstance;
-
- BleConnectionDelegate * mConnectionDelegate = nullptr;
- BlePlatformDelegate * mPlatformDelegate = nullptr;
- BleApplicationDelegate * mApplicationDelegate = nullptr;
};
/**
diff --git a/src/platform/Darwin/BUILD.gn b/src/platform/Darwin/BUILD.gn
index e263c8f..57ad9c7 100644
--- a/src/platform/Darwin/BUILD.gn
+++ b/src/platform/Darwin/BUILD.gn
@@ -124,14 +124,14 @@
if (chip_enable_ble) {
sources += [
- "BleApplicationDelegate.h",
+ "BleApplicationDelegateImpl.h",
"BleApplicationDelegateImpl.mm",
- "BleConnectionDelegate.h",
+ "BleConnectionDelegateImpl.h",
"BleConnectionDelegateImpl.mm",
- "BlePlatformDelegate.h",
+ "BlePlatformDelegateImpl.h",
"BlePlatformDelegateImpl.mm",
- "MTRUUIDHelper.h",
- "MTRUUIDHelperImpl.mm",
+ "BleUtils.h",
+ "BleUtils.mm",
]
}
}
diff --git a/src/platform/Darwin/BleApplicationDelegate.h b/src/platform/Darwin/BleApplicationDelegateImpl.h
similarity index 86%
rename from src/platform/Darwin/BleApplicationDelegate.h
rename to src/platform/Darwin/BleApplicationDelegateImpl.h
index 72aba4e..e9d985a 100644
--- a/src/platform/Darwin/BleApplicationDelegate.h
+++ b/src/platform/Darwin/BleApplicationDelegateImpl.h
@@ -1,6 +1,6 @@
/*
*
- * Copyright (c) 2020 Project CHIP Authors
+ * Copyright (c) 2020-2025 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@
class BleApplicationDelegateImpl : public Ble::BleApplicationDelegate
{
public:
- virtual void NotifyChipConnectionClosed(BLE_CONNECTION_OBJECT connObj);
+ void NotifyChipConnectionClosed(BLE_CONNECTION_OBJECT connObj) override;
};
} // namespace Internal
diff --git a/src/platform/Darwin/BleApplicationDelegateImpl.mm b/src/platform/Darwin/BleApplicationDelegateImpl.mm
index 883f2a2..01fc44e 100644
--- a/src/platform/Darwin/BleApplicationDelegateImpl.mm
+++ b/src/platform/Darwin/BleApplicationDelegateImpl.mm
@@ -1,6 +1,6 @@
/*
*
- * Copyright (c) 2020 Project CHIP Authors
+ * Copyright (c) 2020-2025 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,10 +25,7 @@
#endif
#include <ble/Ble.h>
-#include <platform/Darwin/BleApplicationDelegate.h>
-
-using namespace ::chip;
-using namespace ::chip::Ble;
+#include <platform/Darwin/BleApplicationDelegateImpl.h>
namespace chip {
namespace DeviceLayer {
diff --git a/src/platform/Darwin/BleConnectionDelegate.h b/src/platform/Darwin/BleConnectionDelegateImpl.h
similarity index 95%
rename from src/platform/Darwin/BleConnectionDelegate.h
rename to src/platform/Darwin/BleConnectionDelegateImpl.h
index 585624a..bb0afd4 100644
--- a/src/platform/Darwin/BleConnectionDelegate.h
+++ b/src/platform/Darwin/BleConnectionDelegateImpl.h
@@ -1,6 +1,6 @@
/*
*
- * Copyright (c) 2020 Project CHIP Authors
+ * Copyright (c) 2020-2025 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/platform/Darwin/BleConnectionDelegateImpl.mm b/src/platform/Darwin/BleConnectionDelegateImpl.mm
index 7b5d68b..d29525e 100644
--- a/src/platform/Darwin/BleConnectionDelegateImpl.mm
+++ b/src/platform/Darwin/BleConnectionDelegateImpl.mm
@@ -1,6 +1,6 @@
/*
*
- * Copyright (c) 2020-2021 Project CHIP Authors
+ * Copyright (c) 2020-2025 Project CHIP Authors
* Copyright (c) 2015-2017 Nest Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,24 +28,26 @@
#include <ble/Ble.h>
#include <lib/support/logging/CHIPLogging.h>
#include <platform/CHIPDeviceLayer.h>
-#include <platform/Darwin/BleConnectionDelegate.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>
-#import "MTRUUIDHelper.h"
+#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);
-constexpr char kBleWorkQueueName[] = "org.csa-iot.matter.framework.ble.workqueue";
typedef NS_ENUM(uint8_t, BleConnectionMode) {
kUndefined = 0,
@@ -56,25 +58,23 @@
@interface BleConnection : NSObject <CBCentralManagerDelegate, CBPeripheralDelegate>
-@property (strong, nonatomic) dispatch_queue_t chipWorkQueue;
-@property (strong, nonatomic) dispatch_queue_t workQueue;
+@property (strong, nonatomic) dispatch_queue_t workQueue; // the CHIP work queue
@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;
+@property (assign, nonatomic) bool found;
+@property (assign, nonatomic) chip::SetupDiscriminator deviceDiscriminator;
+@property (assign, nonatomic) void * appState;
+@property (assign, nonatomic) BleConnectionDelegate::OnConnectionCompleteFunct onConnectionComplete;
+@property (assign, nonatomic) BleConnectionDelegate::OnConnectionErrorFunct onConnectionError;
+@property (assign, nonatomic) chip::DeviceLayer::BleScannerDelegate * scannerDelegate;
+@property (assign, 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;
+- (instancetype)initWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm;
+- (instancetype)initWithDiscriminator:(const chip::SetupDiscriminator &)deviceDiscriminator;
+
- (void)setBleLayer:(chip::Ble::BleLayer *)bleLayer;
- (void)start;
- (void)stop;
@@ -92,7 +92,6 @@
namespace DeviceLayer {
namespace Internal {
BleConnection * ble;
- dispatch_queue_t bleWorkQueue;
void BleConnectionDelegateImpl::NewConnection(
Ble::BleLayer * bleLayer, void * appState, const SetupDiscriminator & inDeviceDiscriminator)
@@ -103,30 +102,24 @@
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];
+ // 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.centralManager = [ble.centralManager initWithDelegate:ble queue:bleWorkQueue];
- });
+ [ble updateWithDiscriminator:deviceDiscriminator];
+ return;
+ }
+
+ [ble stop];
+ ble = [[BleConnection alloc] initWithDiscriminator:deviceDiscriminator];
+ [ble setBleLayer:bleLayer];
+ ble.appState = appState;
+ ble.onConnectionComplete = OnConnectionComplete;
+ ble.onConnectionError = OnConnectionError;
+ ble.centralManager = [ble.centralManager initWithDelegate:ble queue:ble.workQueue];
}
void BleConnectionDelegateImpl::NewConnection(Ble::BleLayer * bleLayer, void * appState, BLE_CONNECTION_OBJECT connObj)
@@ -134,31 +127,25 @@
assertChipStackLockedByCurrentThread();
ChipLogProgress(Ble, "ConnectionDelegate NewConnection with conn obj: %p", connObj);
+ CBPeripheral * peripheral = CBPeripheralFromBleConnObject(connObj);
- if (!bleWorkQueue) {
- bleWorkQueue = dispatch_queue_create(kBleWorkQueueName, DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
+ // 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;
}
- 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];
- });
+ [ble setBleLayer:bleLayer];
+ ble.appState = appState;
+ ble.onConnectionComplete = OnConnectionComplete;
+ ble.onConnectionError = OnConnectionError;
+ [ble updateWithPeripheral:peripheral];
}
void BleConnectionDelegateImpl::StartScan(BleScannerDelegate * delegate, BleScanMode mode)
@@ -168,39 +155,33 @@
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);
+ // 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;
}
- 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;
+ }
- // 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];
- });
+ [ble stop];
+ ble = [[BleConnection 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()
@@ -218,16 +199,8 @@
CHIP_ERROR BleConnectionDelegateImpl::DoCancel()
{
assertChipStackLockedByCurrentThread();
- if (bleWorkQueue == nil) {
- return CHIP_NO_ERROR;
- }
-
- dispatch_async(bleWorkQueue, ^{
- [ble stop];
- ble = nil;
- });
-
- bleWorkQueue = nil;
+ [ble stop];
+ ble = nil;
return CHIP_NO_ERROR;
}
} // namespace Internal
@@ -239,15 +212,16 @@
@property (nonatomic, readonly) int32_t totalDevicesRemoved;
@end
-@implementation BleConnection
+@implementation BleConnection {
+ CBUUID * _chipServiceUUID;
+}
-- (id)initWithQueue:(dispatch_queue_t)queue
+- (instancetype)init
{
self = [super init];
if (self) {
- self.shortServiceUUID = [MTRUUIDHelper GetShortestServiceUUID:&chip::Ble::CHIP_BLE_SVC_ID];
- _chipWorkQueue = chip::DeviceLayer::PlatformMgrImpl().GetWorkQueue();
- _workQueue = queue;
+ _chipServiceUUID = CBUUIDFromBleUUID(chip::Ble::CHIP_BLE_SVC_ID);
+ _workQueue = chip::DeviceLayer::PlatformMgrImpl().GetWorkQueue();
_centralManager = [CBCentralManager alloc];
_found = false;
_cachedPeripherals = [[NSMutableDictionary alloc] init];
@@ -258,9 +232,9 @@
return self;
}
-- (id)initWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm queue:(dispatch_queue_t)queue
+- (instancetype)initWithDelegate:(chip::DeviceLayer::BleScannerDelegate *)delegate prewarm:(bool)prewarm
{
- self = [self initWithQueue:queue];
+ self = [self init];
if (self) {
_scannerDelegate = delegate;
if (prewarm) {
@@ -274,9 +248,9 @@
return self;
}
-- (id)initWithDiscriminator:(const chip::SetupDiscriminator &)deviceDiscriminator queue:(dispatch_queue_t)queue
+- (id)initWithDiscriminator:(const chip::SetupDiscriminator &)deviceDiscriminator
{
- self = [self initWithQueue:queue];
+ self = [self init];
if (self) {
_deviceDiscriminator = deviceDiscriminator;
_currentMode = kConnecting;
@@ -318,26 +292,23 @@
// 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);
- }
- });
+ 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);
- }
- });
+ if (self.onConnectionComplete != nil) {
+ self.onConnectionComplete(self.appState, BleConnObjectFromCBPeripheral(peripheral));
+ }
}
// Start CBCentralManagerDelegate
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
+ assertChipStackLockedByCurrentThread();
MATTER_LOG_METRIC(kMetricBLECentralManagerState, static_cast<uint32_t>(central.state));
switch (central.state) {
@@ -370,15 +341,9 @@
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;
- }
- }
+ assertChipStackLockedByCurrentThread();
+ NSData * serviceData = advertisementData[CBAdvertisementDataServiceDataKey][_chipServiceUUID];
if (!serviceData) {
return;
}
@@ -443,8 +408,10 @@
- (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];
@@ -456,6 +423,8 @@
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
+ assertChipStackLockedByCurrentThread();
+
if (nil != error) {
ChipLogError(Ble, "BLE:Error finding Chip Service in the device: [%s]", [error.localizedDescription UTF8String]);
}
@@ -463,7 +432,7 @@
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) {
+ if ([service.UUID isEqual:_chipServiceUUID] && !self.found) {
MATTER_LOG_METRIC_BEGIN(kMetricBLEDiscoveredCharacteristics);
[peripheral discoverCharacteristics:nil forService:service];
self.found = true;
@@ -480,6 +449,7 @@
- (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 (nil != error) {
@@ -495,20 +465,17 @@
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
+ assertChipStackLockedByCurrentThread();
+
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);
- });
+ ChipBleUUID svcId = BleUUIDFromCBUUD(characteristic.service.UUID);
+ ChipBleUUID charId = BleUUIDFromCBUUD(characteristic.UUID);
+ _mBleLayer->HandleWriteConfirmation(BleConnObjectFromCBPeripheral(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);
- });
+ MATTER_LOG_METRIC(kMetricBLEWriteChrValueFailed, BLE_ERROR_GATT_WRITE_FAILED);
+ _mBleLayer->HandleConnectionError(BleConnObjectFromCBPeripheral(peripheral), BLE_ERROR_GATT_WRITE_FAILED);
}
}
@@ -516,34 +483,31 @@
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
+ assertChipStackLockedByCurrentThread();
+
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);
- }
- });
+ ChipBleUUID svcId = BleUUIDFromCBUUD(characteristic.service.UUID);
+ ChipBleUUID charId = BleUUIDFromCBUUD(characteristic.UUID);
+ if (isNotifying) {
+ _mBleLayer->HandleSubscribeComplete(BleConnObjectFromCBPeripheral(peripheral), &svcId, &charId);
+ } else {
+ _mBleLayer->HandleUnsubscribeComplete(BleConnObjectFromCBPeripheral(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);
- }
- });
+
+ if (isNotifying) {
+ MATTER_LOG_METRIC(kMetricBLEUpdateNotificationStateForChrFailed, BLE_ERROR_GATT_WRITE_FAILED);
+ // we're still notifying, so we must failed the unsubscription
+ _mBleLayer->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);
+ _mBleLayer->HandleConnectionError(BleConnObjectFromCBPeripheral(peripheral), BLE_ERROR_GATT_SUBSCRIBE_FAILED);
+ }
}
}
@@ -551,34 +515,31 @@
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
+ assertChipStackLockedByCurrentThread();
+
if (nil == error) {
- chip::Ble::ChipBleUUID svcId;
- chip::Ble::ChipBleUUID charId;
- [BleConnection fillServiceWithCharacteristicUuids:characteristic svcId:&svcId charId:&charId];
+ ChipBleUUID svcId = BleUUIDFromCBUUD(characteristic.service.UUID);
+ ChipBleUUID charId = BleUUIDFromCBUUD(characteristic.UUID);
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);
+ // 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);
- }
- });
+ if (msgBuf.IsNull()) {
+ ChipLogError(Ble, "Failed at allocating buffer for incoming BLE data");
+ MATTER_LOG_METRIC(kMetricBLEUpdateValueForChrFailed, CHIP_ERROR_NO_MEMORY);
+ _mBleLayer->HandleConnectionError(BleConnObjectFromCBPeripheral(peripheral), CHIP_ERROR_NO_MEMORY);
+ } else if (!_mBleLayer->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 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);
- });
+ MATTER_LOG_METRIC(kMetricBLEUpdateValueForChrFailed, BLE_ERROR_GATT_INDICATE_FAILED);
+ _mBleLayer->HandleConnectionError(BleConnObjectFromCBPeripheral(peripheral), BLE_ERROR_GATT_INDICATE_FAILED);
}
}
@@ -603,30 +564,20 @@
[self stopScanning];
[self removePeripheralsFromCache];
- if (!_centralManager && !_peripheral) {
- return;
+ if (_peripheral) {
+ // Close all BLE connections before we release CB objects
+ _mBleLayer->CloseAllBleConnections();
+ _peripheral = nil;
}
- // 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();
- }
+ if (_centralManager) {
+ _centralManager.delegate = nil;
+ _centralManager = nil;
+ }
- dispatch_async(_workQueue, ^{
- _centralManager.delegate = nil;
- _centralManager = nil;
- _peripheral = nil;
- if (chip::DeviceLayer::Internal::ble == self) {
- chip::DeviceLayer::Internal::ble = nil;
- }
- });
- });
+ if (chip::DeviceLayer::Internal::ble == self) {
+ chip::DeviceLayer::Internal::ble = nil;
+ }
}
- (void)_resetCounters
@@ -647,7 +598,7 @@
[self _resetCounters];
auto scanOptions = @{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES };
- [_centralManager scanForPeripheralsWithServices:@[ _shortServiceUUID ] options:scanOptions];
+ [_centralManager scanForPeripheralsWithServices:@[ _chipServiceUUID ] options:scanOptions];
}
- (void)stopScanning
@@ -680,9 +631,7 @@
auto * existingDelegate = _scannerDelegate;
if (existingDelegate) {
_scannerDelegate = nullptr;
- dispatch_async(_chipWorkQueue, ^{
- existingDelegate->OnBleScanStopped();
- });
+ existingDelegate->OnBleScanStopped();
}
}
@@ -693,11 +642,9 @@
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);
- });
+ ChipBLEDeviceIdentificationInfo info;
+ memcpy(&info, [serviceData bytes], sizeof(info));
+ delegate->OnBleScanAdd(BleConnObjectFromCBPeripheral(cachedPeripheral), info);
}
_scannerDelegate = delegate;
}
@@ -768,12 +715,10 @@
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);
- });
+ 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);
@@ -820,9 +765,7 @@
auto delegate = _scannerDelegate;
if (delegate) {
- dispatch_async(_chipWorkQueue, ^{
- delegate->OnBleScanRemove((__bridge void *) peripheral);
- });
+ delegate->OnBleScanRemove(BleConnObjectFromCBPeripheral(peripheral));
}
}
}
@@ -834,55 +777,6 @@
}
}
-/**
- * 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;
diff --git a/src/platform/Darwin/BlePlatformConfig.h b/src/platform/Darwin/BlePlatformConfig.h
index a27bfa2..096af04 100644
--- a/src/platform/Darwin/BlePlatformConfig.h
+++ b/src/platform/Darwin/BlePlatformConfig.h
@@ -26,6 +26,10 @@
// ==================== Platform Adaptations ====================
+#define BLE_CONNECTION_OBJECT void * // actually __unsafe_unretained CBPeripheral *
+#define BLE_CONNECTION_UNINITIALIZED nullptr
+#define BLE_USES_DEVICE_EVENTS 0
+
// ========== Platform-specific Configuration Overrides =========
/* none so far */
diff --git a/src/platform/Darwin/BlePlatformDelegate.h b/src/platform/Darwin/BlePlatformDelegateImpl.h
similarity index 70%
rename from src/platform/Darwin/BlePlatformDelegate.h
rename to src/platform/Darwin/BlePlatformDelegateImpl.h
index d53f900..256f36f 100644
--- a/src/platform/Darwin/BlePlatformDelegate.h
+++ b/src/platform/Darwin/BlePlatformDelegateImpl.h
@@ -20,9 +20,6 @@
#include <ble/Ble.h>
#include <system/SystemPacketBuffer.h>
-using ::chip::Ble::ChipBleUUID;
-using ::chip::System::PacketBufferHandle;
-
namespace chip {
namespace DeviceLayer {
namespace Internal {
@@ -30,16 +27,16 @@
class BlePlatformDelegateImpl : public Ble::BlePlatformDelegate
{
public:
- CHIP_ERROR SubscribeCharacteristic(BLE_CONNECTION_OBJECT connObj, const ChipBleUUID * svcId,
- const ChipBleUUID * charId) override;
- CHIP_ERROR UnsubscribeCharacteristic(BLE_CONNECTION_OBJECT connObj, const ChipBleUUID * svcId,
- const ChipBleUUID * charId) override;
+ CHIP_ERROR SubscribeCharacteristic(BLE_CONNECTION_OBJECT connObj, const Ble::ChipBleUUID * svcId,
+ const Ble::ChipBleUUID * charId) override;
+ CHIP_ERROR UnsubscribeCharacteristic(BLE_CONNECTION_OBJECT connObj, const Ble::ChipBleUUID * svcId,
+ const Ble::ChipBleUUID * charId) override;
CHIP_ERROR CloseConnection(BLE_CONNECTION_OBJECT connObj) override;
uint16_t GetMTU(BLE_CONNECTION_OBJECT connObj) const override;
- CHIP_ERROR SendIndication(BLE_CONNECTION_OBJECT connObj, const ChipBleUUID * svcId, const ChipBleUUID * charId,
- PacketBufferHandle pBuf) override;
- CHIP_ERROR SendWriteRequest(BLE_CONNECTION_OBJECT connObj, const ChipBleUUID * svcId, const ChipBleUUID * charId,
- PacketBufferHandle pBuf) override;
+ CHIP_ERROR SendIndication(BLE_CONNECTION_OBJECT connObj, const Ble::ChipBleUUID * svcId, const Ble::ChipBleUUID * charId,
+ System::PacketBufferHandle pBuf) override;
+ CHIP_ERROR SendWriteRequest(BLE_CONNECTION_OBJECT connObj, const Ble::ChipBleUUID * svcId, const Ble::ChipBleUUID * charId,
+ System::PacketBufferHandle pBuf) override;
};
} // namespace Internal
diff --git a/src/platform/Darwin/BlePlatformDelegateImpl.mm b/src/platform/Darwin/BlePlatformDelegateImpl.mm
index d967ea4..1b5fe31 100644
--- a/src/platform/Darwin/BlePlatformDelegateImpl.mm
+++ b/src/platform/Darwin/BlePlatformDelegateImpl.mm
@@ -27,76 +27,61 @@
#include <ble/Ble.h>
#include <lib/support/logging/CHIPLogging.h>
-#include <platform/Darwin/BlePlatformDelegate.h>
+#include <platform/Darwin/BlePlatformDelegateImpl.h>
+#include <platform/Darwin/BleUtils.h>
-#import "MTRUUIDHelper.h"
+#import <CoreBluetooth/CoreBluetooth.h>
-using namespace ::chip;
-using namespace ::chip::Ble;
-using ::chip::System::PacketBufferHandle;
+using namespace chip::Ble;
+using chip::System::PacketBufferHandle;
namespace chip {
namespace DeviceLayer {
namespace Internal {
+
+ namespace {
+ CBCharacteristic * FindCharacteristic(CBPeripheral * peripheral, const ChipBleUUID * svcId, const ChipBleUUID * charId)
+ {
+ VerifyOrReturnValue(svcId != nullptr && charId != nullptr, nil);
+ CBUUID * cbSvcId = CBUUIDFromBleUUID(*svcId);
+ for (CBService * service in peripheral.services) {
+ if ([service.UUID isEqual:cbSvcId]) {
+ CBUUID * cbCharId = CBUUIDFromBleUUID(*charId);
+ for (CBCharacteristic * characteristic in service.characteristics) {
+ if ([characteristic.UUID isEqual:cbCharId]) {
+ return characteristic;
+ }
+ }
+ break;
+ }
+ }
+ return nil;
+ }
+ }
+
CHIP_ERROR BlePlatformDelegateImpl::SubscribeCharacteristic(
BLE_CONNECTION_OBJECT connObj, const ChipBleUUID * svcId, const ChipBleUUID * charId)
{
- CHIP_ERROR err = BLE_ERROR_GATT_SUBSCRIBE_FAILED;
-
- if (nullptr == svcId || nullptr == charId) {
- return err;
- }
-
- CBUUID * serviceId = [MTRUUIDHelper GetShortestServiceUUID:svcId];
- CBUUID * characteristicId = [CBUUID UUIDWithData:[NSData dataWithBytes:charId->bytes length:sizeof(charId->bytes)]];
- CBPeripheral * peripheral = (__bridge CBPeripheral *) connObj;
-
- for (CBService * service in peripheral.services) {
- if ([service.UUID.data isEqualToData:serviceId.data]) {
- for (CBCharacteristic * characteristic in service.characteristics) {
- if ([characteristic.UUID.data isEqualToData:characteristicId.data]) {
- err = CHIP_NO_ERROR;
- [peripheral setNotifyValue:true forCharacteristic:characteristic];
- break;
- }
- }
- }
- }
-
- return err;
+ CBPeripheral * peripheral = CBPeripheralFromBleConnObject(connObj);
+ CBCharacteristic * characteristic = FindCharacteristic(peripheral, svcId, charId);
+ VerifyOrReturnError(characteristic != nil, BLE_ERROR_GATT_SUBSCRIBE_FAILED);
+ [peripheral setNotifyValue:YES forCharacteristic:characteristic];
+ return CHIP_NO_ERROR;
}
CHIP_ERROR BlePlatformDelegateImpl::UnsubscribeCharacteristic(
BLE_CONNECTION_OBJECT connObj, const ChipBleUUID * svcId, const ChipBleUUID * charId)
{
- CHIP_ERROR err = BLE_ERROR_GATT_UNSUBSCRIBE_FAILED;
- if (nullptr == svcId || nullptr == charId) {
- return err;
- }
-
- CBUUID * serviceId = [MTRUUIDHelper GetShortestServiceUUID:svcId];
- CBUUID * characteristicId = characteristicId = [CBUUID UUIDWithData:[NSData dataWithBytes:charId->bytes
- length:sizeof(charId->bytes)]];
- CBPeripheral * peripheral = (__bridge CBPeripheral *) connObj;
-
- for (CBService * service in peripheral.services) {
- if ([service.UUID.data isEqualToData:serviceId.data]) {
- for (CBCharacteristic * characteristic in service.characteristics) {
- if ([characteristic.UUID.data isEqualToData:characteristicId.data]) {
- err = CHIP_NO_ERROR;
- [peripheral setNotifyValue:false forCharacteristic:characteristic];
- break;
- }
- }
- }
- }
-
- return err;
+ CBPeripheral * peripheral = CBPeripheralFromBleConnObject(connObj);
+ CBCharacteristic * characteristic = FindCharacteristic(peripheral, svcId, charId);
+ VerifyOrReturnError(characteristic != nil, BLE_ERROR_GATT_UNSUBSCRIBE_FAILED);
+ [peripheral setNotifyValue:NO forCharacteristic:characteristic];
+ return CHIP_NO_ERROR;
}
CHIP_ERROR BlePlatformDelegateImpl::CloseConnection(BLE_CONNECTION_OBJECT connObj)
{
- CBPeripheral * peripheral = (__bridge CBPeripheral *) connObj;
+ CBPeripheral * peripheral = CBPeripheralFromBleConnObject(connObj);
// CoreBluetooth API requires a CBCentralManager to close a connection which is a property of the peripheral.
CBCentralManager * manager = (CBCentralManager *) [peripheral valueForKey:@"manager"];
@@ -108,7 +93,7 @@
uint16_t BlePlatformDelegateImpl::GetMTU(BLE_CONNECTION_OBJECT connObj) const
{
- CBPeripheral * peripheral = (__bridge CBPeripheral *) connObj;
+ CBPeripheral * peripheral = CBPeripheralFromBleConnObject(connObj);
// The negotiated mtu length is a property of the peripheral.
uint16_t mtuLength = [[peripheral valueForKey:@"mtuLength"] unsignedShortValue];
@@ -126,32 +111,12 @@
CHIP_ERROR BlePlatformDelegateImpl::SendWriteRequest(
BLE_CONNECTION_OBJECT connObj, const ChipBleUUID * svcId, const ChipBleUUID * charId, PacketBufferHandle pBuf)
{
- CHIP_ERROR err = BLE_ERROR_GATT_WRITE_FAILED;
- if (nullptr == svcId || nullptr == charId || pBuf.IsNull()) {
- return err;
- }
-
- CBUUID * serviceId = [MTRUUIDHelper GetShortestServiceUUID:svcId];
- CBUUID * characteristicId = [CBUUID UUIDWithData:[NSData dataWithBytes:charId->bytes length:sizeof(charId->bytes)]];
- NSData * data = [NSData dataWithBytes:pBuf->Start() length:pBuf->DataLength()];
- CBPeripheral * peripheral = (__bridge CBPeripheral *) connObj;
-
- for (CBService * service in peripheral.services) {
- if ([service.UUID.data isEqualToData:serviceId.data]) {
- for (CBCharacteristic * characteristic in service.characteristics) {
- if ([characteristic.UUID.data isEqualToData:characteristicId.data]) {
- err = CHIP_NO_ERROR;
- [peripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
- break;
- }
- }
- }
- }
-
- // Going out of scope releases delegate's reference to pBuf. pBuf will be freed when both platform delegate and Chip
- // stack free their references to it. We release pBuf's reference here since its payload bytes were copied into a new
- // NSData object
- return err;
+ CBPeripheral * peripheral = CBPeripheralFromBleConnObject(connObj);
+ CBCharacteristic * characteristic = FindCharacteristic(peripheral, svcId, charId);
+ VerifyOrReturnError(characteristic != nil && !pBuf.IsNull(), BLE_ERROR_GATT_WRITE_FAILED);
+ NSData * data = [NSData dataWithBytes:pBuf->Start() length:pBuf->DataLength()]; // copies data, pBuf can be freed
+ [peripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
+ return CHIP_NO_ERROR;
}
} // namespace Internal
diff --git a/src/platform/Darwin/BleUtils.h b/src/platform/Darwin/BleUtils.h
new file mode 100644
index 0000000..13d7b4e
--- /dev/null
+++ b/src/platform/Darwin/BleUtils.h
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2025 Project CHIP Authors
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <ble/Ble.h>
+
+@class CBPeripheral;
+@class CBUUID;
+
+namespace chip {
+namespace DeviceLayer {
+ namespace Internal {
+
+ inline CBPeripheral * CBPeripheralFromBleConnObject(BLE_CONNECTION_OBJECT connObj)
+ {
+ return (__bridge CBPeripheral *) connObj;
+ }
+
+ inline BLE_CONNECTION_OBJECT BleConnObjectFromCBPeripheral(CBPeripheral * peripheral)
+ {
+ return (__bridge void *) peripheral;
+ }
+
+ // Creates a CBUUID from a ChipBleUUID
+ CBUUID * CBUUIDFromBleUUID(Ble::ChipBleUUID const & uuid);
+
+ // Creates a ChipBleUUID from a CBUUID, expanding 16 or 32 bit UUIDs if necessary.
+ Ble::ChipBleUUID BleUUIDFromCBUUD(CBUUID * uuid);
+
+ }
+}
+}
diff --git a/src/platform/Darwin/BleUtils.mm b/src/platform/Darwin/BleUtils.mm
new file mode 100644
index 0000000..5ca9f4a
--- /dev/null
+++ b/src/platform/Darwin/BleUtils.mm
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2025 Project CHIP Authors
+ *
+ * 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.
+ */
+
+#include "BleUtils.h"
+
+#import <CoreBluetooth/CoreBluetooth.h>
+#import <Foundation/Foundation.h>
+
+using namespace chip::Ble;
+
+namespace chip {
+namespace DeviceLayer {
+ namespace Internal {
+
+ CBUUID * CBUUIDFromBleUUID(ChipBleUUID const & uuid)
+ {
+ return [CBUUID UUIDWithData:[NSData dataWithBytes:uuid.bytes length:sizeof(uuid.bytes)]];
+ }
+
+ ChipBleUUID BleUUIDFromCBUUD(CBUUID * uuid)
+ {
+ // CBUUID handles the expansion to 128 bit automatically internally,
+ // but doesn't expose the expanded UUID in the `data` property, so
+ // we have to re-implement the expansion here.
+ // The Base UUID 00000000-0000-1000-8000-00805F9B34FB is defined in the BLE spec.
+ constexpr ChipBleUUID baseUuid = { { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB } };
+ NSData * uuidData = uuid.data;
+ switch (uuidData.length) {
+ case 2: {
+ ChipBleUUID outUuid = baseUuid;
+ memcpy(outUuid.bytes + 2, uuidData.bytes, 2);
+ return outUuid;
+ }
+ case 4: {
+ ChipBleUUID outUuid = baseUuid;
+ memcpy(outUuid.bytes, uuidData.bytes, 4);
+ return outUuid;
+ }
+ case 16: {
+ ChipBleUUID outUuid;
+ memcpy(outUuid.bytes, uuidData.bytes, 16);
+ return outUuid;
+ }
+ default: {
+ NSCAssert(NO, @"Invalid CBUUID.data: %@", uuidData);
+ return ChipBleUUID {};
+ }
+ }
+ }
+
+ }
+}
+}
diff --git a/src/platform/Darwin/MTRUUIDHelper.h b/src/platform/Darwin/MTRUUIDHelper.h
deleted file mode 100644
index faa0af9..0000000
--- a/src/platform/Darwin/MTRUUIDHelper.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- *
- * Copyright (c) 2020 Project CHIP Authors
- *
- * 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.
- */
-
-#pragma once
-
-#include <ble/Ble.h>
-
-#import <CoreBluetooth/CoreBluetooth.h>
-
-@interface MTRUUIDHelper : NSObject
-+ (CBUUID *)GetShortestServiceUUID:(const chip::Ble::ChipBleUUID *)svcId;
-@end
diff --git a/src/platform/Darwin/MTRUUIDHelperImpl.mm b/src/platform/Darwin/MTRUUIDHelperImpl.mm
deleted file mode 100644
index 8f1b863..0000000
--- a/src/platform/Darwin/MTRUUIDHelperImpl.mm
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- *
- * Copyright (c) 2020 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.
- */
-
-#if !__has_feature(objc_arc)
-#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
-#endif
-
-#import "MTRUUIDHelper.h"
-
-@implementation MTRUUIDHelper
-
-+ (CBUUID *)GetShortestServiceUUID:(const chip::Ble::ChipBleUUID *)svcId
-{
- // shorten the 16-byte UUID reported by BLE Layer to shortest possible, 2 or 4 bytes
- // this is the BLE Service UUID Base. If a 16-byte service UUID partially matches with these 12 bytes,
- // it can be shortened to 2 or 4 bytes.
- static const uint8_t bleBaseUUID[12] = { 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB };
- if (0 == memcmp(svcId->bytes + 4, bleBaseUUID, sizeof(bleBaseUUID))) {
- // okay, let's try to shorten it
- if ((0 == svcId->bytes[0]) && (0 == svcId->bytes[1])) {
- // the highest 2 bytes are both 0, so we just need 2 bytes
- return [CBUUID UUIDWithData:[NSData dataWithBytes:(svcId->bytes + 2) length:2]];
- } // we need to use 4 bytes
- return [CBUUID UUIDWithData:[NSData dataWithBytes:svcId->bytes length:4]];
- }
- // it cannot be shortened as it doesn't match with the BLE Service UUID Base
- return [CBUUID UUIDWithData:[NSData dataWithBytes:svcId->bytes length:16]];
-}
-@end