Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[darwin-framework-tool] Add XPC connectivity support for HomeKit on iOS #37794

Merged
merged 5 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@

#import <lib/support/logging/CHIPLogging.h>

#if TARGET_OS_MACCATALYST || TARGET_OS_IOS
#import "HomeKitConnector.h"
#endif

@interface DeviceControllerXPCServerImpl : NSObject <MTRDeviceControllerServerProtocol>
@property (nonatomic, strong) id<MTRDeviceControllerClientProtocol> clientProxy;
@property (nonatomic, strong) NSArray<MTRDeviceController *> * controllers;
Expand Down Expand Up @@ -331,19 +335,25 @@ - (void)start

- (void)stop
{
#if TARGET_OS_MACCATALYST || TARGET_OS_IOS
[[HomeKitConnector sharedInstance] stop];
#endif
}

- (MTRDeviceController *)createController:(NSString *)controllerID serviceName:(NSString *)serviceName error:(NSError * __autoreleasing *)error
{
__auto_type connectBlock = ^NSXPCConnection *
{
NSXPCConnection * (^connectBlock)(void) = nil;
#if TARGET_OS_OSX
connectBlock = ^NSXPCConnection *
{
return [[NSXPCConnection alloc] initWithMachServiceName:serviceName options:0];
#else
ChipLogError(chipTool, "NSXPCConnection::initWithMachServiceName is not supported on this platform.");
return nil;
#endif // TARGET_OS_OSX
};
#elif TARGET_OS_MACCATALYST || TARGET_OS_IOS
connectBlock = [[HomeKitConnector sharedInstance] connectBlockFor:controllerID];
controllerID = [[HomeKitConnector sharedInstance] homeControllerIDFor:controllerID];
#endif

VerifyOrReturnValue(nil != connectBlock, nil, ChipLogError(chipTool, "DeviceControllerXPCServerWithServiceName is not supported on this platform."));
return [MTRDeviceController sharedControllerWithID:controllerID xpcConnectBlock:connectBlock];
}
@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Project CHIP Authors
* All rights reserved.
*
* 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 <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface HomeKitConnector : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
+ (instancetype)sharedInstance;

- (void)start;
- (void)stop;
- (NSString *)homeControllerIDFor:(NSString *)controllerID;
- (NSXPCConnection * (^)(void) )connectBlockFor:(NSString *)controllerID;
@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* Copyright (c) 2025 Project CHIP Authors
* All rights reserved.
*
* 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 "HomeKitConnector.h"
#import "../CHIPCommandBridge.h"
#import <lib/support/logging/CHIPLogging.h>

#import <HomeKit/HomeKit.h>

const int64_t kHomeManagerSetupTimeout = 10LL * NSEC_PER_SEC;
NSString * kControllerIdPrefixStr = @(kControllerIdPrefix);

@interface HomeKitConnector () <HMHomeManagerDelegate>
@property (nonatomic, assign) BOOL connectorStarted;
@property (nonatomic, strong) HMHomeManager * homeManager;
@property (nonatomic, assign) BOOL homeManagerReady;
@end

@implementation HomeKitConnector {
dispatch_group_t _homeManagerReadyGroup;
}

+ (instancetype)sharedInstance
{
static HomeKitConnector * sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[HomeKitConnector alloc] init];
});
return sharedInstance;
}

- (void)start
{
VerifyOrReturn(!_connectorStarted);
_connectorStarted = YES;

_homeManagerReady = NO;
_homeManagerReadyGroup = dispatch_group_create();
dispatch_group_enter(_homeManagerReadyGroup);

_homeManager = [[HMHomeManager alloc] init];
_homeManager.delegate = self;

// Wait until homeManagerDidUpdateHomes is called or timeout
dispatch_group_wait(_homeManagerReadyGroup, dispatch_time(DISPATCH_TIME_NOW, kHomeManagerSetupTimeout));

[self printHomes];
}

- (void)stop
{
VerifyOrReturn(_connectorStarted);

_homeManager.delegate = nil;
_homeManager = nil;
}

- (void)homeManagerDidUpdateHomes:(HMHomeManager *)manager
{
VerifyOrReturn(!_homeManagerReady);
dispatch_group_leave(_homeManagerReadyGroup);
}

- (HMHome *)homeFor:(NSString *)controllerID
{
[[HomeKitConnector sharedInstance] start];

__auto_type * homes = _homeManager.homes;
VerifyOrReturnValue(0 != homes.count, nil, ChipLogError(chipTool, "HomeKit is not configured with any homes."));

NSNumber * fabricId = nil;
if ([controllerID hasPrefix:kControllerIdPrefixStr]) {
__auto_type * fabricIdString = [controllerID substringFromIndex:kControllerIdPrefixStr.length];
fabricId = @([fabricIdString integerValue]);
} else {
fabricId = CHIPCommandBridge::GetCommissionerFabricId([controllerID UTF8String]);
}

// When multiple homes exist, the first controller corresponds to the first home, the second controller to the second home, etc.
// If there are fewer homes than controllers, controllers beyond the last home will be associated with the final home in the list.
NSUInteger index = [fabricId unsignedShortValue] - 1;
if (index >= homes.count) {
index = homes.count - 1;
}

return homes[index];
}

- (NSString *)paddedString:(NSString *)string width:(NSUInteger)width
{
// Using length might not account for all unicode width details, but it's a simple approximation.
NSUInteger length = string.length;
if (length >= width) {
return string;
}
NSMutableString * result = [NSMutableString stringWithString:string];
for (NSUInteger i = 0; i < (width - length); i++) {
[result appendString:@" "];
}
return result;
}

- (NSString *)repeatString:(NSString *)string count:(NSUInteger)count
{
NSMutableString * result = [NSMutableString string];
for (NSUInteger i = 0; i < count; i++) {
[result appendString:string];
}
return result;
}

- (void)printHomes
{
for (HMHome * home in _homeManager.homes) {
NSUInteger maxNameLength = 0;
NSUInteger maxNodeIDLength = 0;
NSUInteger maxManufacturerLength = 0;
NSUInteger maxModelLength = 0;

__auto_type * sortedAccessories = [home.accessories sortedArrayUsingComparator:^NSComparisonResult(HMAccessory * a, HMAccessory * b) {
return [a.name localizedCaseInsensitiveCompare:b.name];
}];

for (HMAccessory * accessory in sortedAccessories) {
maxNameLength = MAX(maxNameLength, accessory.name.length);
maxManufacturerLength = MAX(maxManufacturerLength, accessory.manufacturer.length);
maxModelLength = MAX(maxModelLength, accessory.model.length);
maxNodeIDLength = MAX(maxNodeIDLength, [accessory.matterNodeID stringValue].length);
}

__auto_type * rows = [NSMutableArray arrayWithCapacity:sortedAccessories.count];
[sortedAccessories enumerateObjectsUsingBlock:^(HMAccessory * accessory, NSUInteger idx, BOOL * stop) {
if (accessory.matterNodeID == nil || [accessory.matterNodeID integerValue] == 0) {
return;
}

__auto_type * name = [self paddedString:accessory.name width:maxNameLength];
__auto_type * manufacturer = [self paddedString:accessory.manufacturer width:maxManufacturerLength];
__auto_type * model = [self paddedString:accessory.model width:maxModelLength];
__auto_type * nodeID = [self paddedString:[accessory.matterNodeID stringValue] width:maxNodeIDLength];
__auto_type * formattedString = [NSString stringWithFormat:@" %@ │ %@ │ %@ │ %@ ", name, manufacturer, model, nodeID];
[rows addObject:formattedString];
}];

NSUInteger tableWidth = 1 + maxNameLength + 3 + maxManufacturerLength + 3 + maxModelLength + 3 + maxNodeIDLength + 1;
NSLog(@"╔%@╗", [self repeatString:@"═" count:tableWidth]);
NSLog(@"║%@║", [self paddedString:[NSString stringWithFormat:@" %@ [%@] ", home.name, home.matterControllerID] width:tableWidth]);
NSLog(@"╠%@╣", [self repeatString:@"═" count:tableWidth]);
for (NSString * row in rows) {
NSLog(@"║%@║", row);
}
NSLog(@"╚%@╝", [self repeatString:@"═" count:tableWidth]);
}
}

- (NSString *)homeControllerIDFor:(NSString *)controllerID
{
__auto_type * home = [self homeFor:controllerID];
return home.matterControllerID;
}

- (NSXPCConnection * (^)(void) )connectBlockFor:(NSString *)controllerID;
{
__auto_type * home = [self homeFor:controllerID];
ChipLogProgress(chipTool, "Controller '%s' will be associated with home '%s'.", [controllerID UTF8String], [home.matterControllerID UTF8String]);

if ([controllerID hasPrefix:kControllerIdPrefixStr]) {
if ([home respondsToSelector:NSSelectorFromString(@"matterStartupParametersXPCConnectBlock")]) {
return [home valueForKey:@"matterStartupParametersXPCConnectBlock"];
}

ChipLogError(chipTool, "Error: 'matterStartupParametersXPCConnectBlock' not available for controller '%s'.", [controllerID UTF8String]);
return nil;
} else {
return home.matterControllerXPCConnectBlock;
}
}
@end
23 changes: 17 additions & 6 deletions examples/darwin-framework-tool/commands/common/xpc/XPCServer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@

#import <lib/support/logging/CHIPLogging.h>

#if TARGET_OS_MACCATALYST || TARGET_OS_IOS
#import "HomeKitConnector.h"
#endif

@interface XPCServerImpl : NSObject <MTRXPCServerProtocol_MTRDevice>
@property (nonatomic, strong) id<MTRXPCClientProtocol> clientProxy;
@property (nonatomic, strong) NSArray<MTRDeviceController *> * controllers;
Expand Down Expand Up @@ -239,19 +243,26 @@ - (void)start

- (void)stop
{
#if TARGET_OS_MACCATALYST || TARGET_OS_IOS
[[HomeKitConnector sharedInstance] stop];
#endif
}

- (MTRDeviceController *)createController:(NSString *)controllerID serviceName:(NSString *)serviceName error:(NSError * __autoreleasing *)error
{
__auto_type connectBlock = ^NSXPCConnection *
{
NSXPCConnection * (^connectBlock)(void) = nil;
#if TARGET_OS_OSX
connectBlock = ^NSXPCConnection *
{
return [[NSXPCConnection alloc] initWithMachServiceName:serviceName options:0];
#else
ChipLogError(chipTool, "NSXPCConnection::initWithMachServiceName is not supported on this platform.");
return nil;
#endif // TARGET_OS_OSX
};
#elif TARGET_OS_MACCATALYST || TARGET_OS_IOS
connectBlock = [[HomeKitConnector sharedInstance] connectBlockFor:controllerID];
controllerID = [[HomeKitConnector sharedInstance] homeControllerIDFor:controllerID];
#endif

VerifyOrReturnValue(nil != connectBlock, nil, ChipLogError(chipTool, "XPCServerWithServiceName is not supported on this platform."));

__auto_type * uniqueIdentifier = [[NSUUID alloc] initWithUUIDString:controllerID];
__auto_type * xpcParams = [[MTRXPCDeviceControllerParameters alloc] initWithXPConnectionBlock:connectBlock uniqueIdentifier:uniqueIdentifier];
return [[MTRDeviceController alloc] initWithParameters:xpcParams error:error];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
<array>
<string>group.com.appleinternal.chip-tool</string>
</array>
<key>com.apple.developer.homekit</key>
<true/>
<key>com.apple.developer.homekit.background-mode</key>
<true/>
</dict>
</plist>
44 changes: 25 additions & 19 deletions examples/darwin-framework-tool/main.mm
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,30 @@

int main(int argc, const char * argv[])
{
int exitCode = EXIT_SUCCESS;
@autoreleasepool {
dft::logging::Setup();
__auto_type * runQueue = dispatch_queue_create("com.chip.main.dft", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
dispatch_async(runQueue, ^{
int exitCode = EXIT_SUCCESS;

Commands commands;
registerCommandsBdx(commands);
registerCommandsPairing(commands);
registerCommandsDCL(commands);
registerCommandsDelay(commands);
registerCommandsDiscover(commands);
registerCommandsInteractive(commands);
registerCommandsMemory(commands);
registerCommandsPayload(commands);
registerClusterOtaSoftwareUpdateProviderInteractive(commands);
registerCommandsStorage(commands);
registerCommandsConfiguration(commands);
registerClusters(commands);
exitCode = commands.Run(argc, (char **) argv);
}
return ConditionalLeaksCheck(exitCode);
@autoreleasepool {
dft::logging::Setup();
Commands commands;
registerCommandsBdx(commands);
registerCommandsPairing(commands);
registerCommandsDCL(commands);
registerCommandsDelay(commands);
registerCommandsDiscover(commands);
registerCommandsInteractive(commands);
registerCommandsMemory(commands);
registerCommandsPayload(commands);
registerClusterOtaSoftwareUpdateProviderInteractive(commands);
registerCommandsStorage(commands);
registerCommandsConfiguration(commands);
registerClusters(commands);
exitCode = commands.Run(argc, (char **) argv);
}
exit(ConditionalLeaksCheck(exitCode));
});

dispatch_main();
return EXIT_SUCCESS;
}
Loading
Loading