blob: f8530c219fb5665645535ce74751ab916d874135 [file] [log] [blame]
/*
* 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.
*/
#pragma once
#import <Matter/Matter.h>
#import "../common/DeviceDelegate.h"
#import "MTRError_Utils.h"
#include <commands/common/Command.h>
NS_ASSUME_NONNULL_BEGIN
class PrintDeviceCommand : public CHIPCommandBridge {
public:
PrintDeviceCommand()
: CHIPCommandBridge("print", "Prints the endpoints, clusters, commands and attributes that are "
"available on a device instance.")
{
AddArgument("node-id", 0, UINT64_MAX, &mNodeId, "The Node ID of the device whose details will be printed.");
AddArgument("endpoint-id", 0, UINT16_MAX, &mEndPointId, "If provided, restrict the output to this single endpoint.");
}
protected:
/////////// CHIPCommandBridge Interface /////////
CHIP_ERROR RunCommand() override
{
auto * controller = CurrentCommissioner();
VerifyOrReturnError(nil != controller, CHIP_ERROR_INCORRECT_STATE);
auto * device = [MTRDevice deviceWithNodeID:@(mNodeId) controller:controller];
VerifyOrReturnError(nil != device, CHIP_ERROR_INCORRECT_STATE);
DeviceWithNodeId(mNodeId);
if (device.deviceCachePrimed) {
ReturnErrorOnFailure(PrintDevice(device));
SetCommandExitStatus(CHIP_NO_ERROR);
} else {
auto onDeviceCachePrimedBlock = ^{
CHIP_ERROR error = PrintDevice(device);
[device removeDelegate:mDeviceDelegate];
mDeviceDelegate = nil;
SetCommandExitStatus(error);
};
mDeviceDelegate = [[DeviceDelegate alloc] init];
mDeviceDelegate.onDeviceCachePrimed = onDeviceCachePrimedBlock;
auto deviceDelegateDispatchQueue = dispatch_queue_create("com.chip.devicedelegate", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
[device addDelegate:mDeviceDelegate queue:deviceDelegateDispatchQueue];
}
return CHIP_NO_ERROR;
}
chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(30); }
private:
// Error Descriptions
NSString * const kErrorDomain = @"Error";
NSInteger const kErrorCode = 0;
NSString * const kErrorNotAnArray = @"Expected an array, but received a different type.";
NSString * const kErrorNotANumber = @"Expected a numeric value, but received a different type.";
NSString * const kErrorNotAStructure = @"Expected a structure, but received a different type.";
NSString * const kErrorNilArray = @"Expected an array, but received a nil response.";
NSString * const kErrorNilNumber = @"Expected a numeric value, but received a nil response.";
NSString * const kErrorNilStructure = @"Expected a structure, but received a nil response.";
CHIP_ERROR PrintDevice(MTRDevice * device)
{
CHIP_ERROR error = CHIP_NO_ERROR;
NSError * internalError = nil;
AddDeviceToBuffer(device, &internalError);
if (nil != internalError) {
error = MTRErrorToCHIPErrorCode(internalError);
ChipLogError(chipTool, "Error while reading device configuration: %s", internalError.localizedDescription.UTF8String);
}
return error;
}
void AddDeviceToBuffer(MTRDevice * device, NSError * _Nonnull __autoreleasing * error)
{
mLogBuffer = [NSMutableString string];
auto * rootEndPointID = @(0);
auto * endpoints = ReadAsNumbers(
device,
rootEndPointID,
@(MTRClusterIDTypeDescriptorID),
@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID),
error);
VerifyOrReturn(nil == *error);
AddEndPointToBuffer(device, rootEndPointID, error);
VerifyOrReturn(nil == *error);
for (NSNumber * endpoint in endpoints) {
AddEndPointToBuffer(device, endpoint, error);
VerifyOrReturn(nil == *error);
}
// Everything should be buffered now, print it.
NSLog(@"%@", mLogBuffer);
}
void AddEndPointToBuffer(MTRDevice * device, NSNumber * endpoint, NSError * _Nonnull __autoreleasing * error)
{
if (mEndPointId.HasValue() && ![endpoint isEqual:@(mEndPointId.Value())]) {
return;
}
auto * deviceType = GetDeviceType(device, endpoint, error);
VerifyOrReturn(nil == *error);
auto * clusters = ReadAsNumbers(
device,
endpoint,
@(MTRClusterIDTypeDescriptorID),
@(MTRAttributeIDTypeClusterDescriptorAttributeServerListID),
error);
VerifyOrReturn(nil == *error);
AddToBuffer(@"\n");
AddToBuffer(@"Endpoint %@ [%@]", endpoint, deviceType);
AddToBuffer(@"\t┃");
for (NSNumber * cluster in clusters) {
AddClusterToBuffer(device, endpoint, cluster, error);
VerifyOrReturn(nil == *error);
}
}
void AddClusterToBuffer(MTRDevice * device, NSNumber * endpoint, NSNumber * cluster, NSError * _Nonnull __autoreleasing * error)
{
auto * clusterName = MTRClusterNameForID(static_cast<MTRClusterIDType>(cluster.unsignedLongLongValue));
auto * clusterRevision = ReadAsNumber(
device,
endpoint,
cluster,
@(MTRAttributeIDTypeGlobalAttributeClusterRevisionID),
error);
VerifyOrReturn(nil == *error);
auto * features = GetFeatureMap(device, endpoint, error);
VerifyOrReturn(nil == *error);
AddToBuffer(@"\t┃");
AddToBuffer(@"\t┣━━ %@ [Revision: %@ | Feature Map: %@ | ID: %@ (%@)]", clusterName, clusterRevision, features, cluster, FormatID(cluster));
AddCommandsToBuffer(device, endpoint, cluster, error);
VerifyOrReturn(nil == *error);
AddAttributesToBuffer(device, endpoint, cluster, error);
VerifyOrReturn(nil == *error);
}
void AddCommandsToBuffer(MTRDevice * device, NSNumber * endpoint, NSNumber * cluster, NSError * _Nonnull __autoreleasing * error)
{
auto * commands = ReadAsNumbers(
device,
endpoint,
cluster,
@(MTRAttributeIDTypeGlobalAttributeAcceptedCommandListID),
error);
VerifyOrReturn(nil == *error);
if (commands.count == 0) {
return;
}
AddClusterToBufferMemberHeader(@"Commands");
NSNumber * longestCommand = @(0);
for (NSNumber * command in commands) {
NSString * name = MTRRequestCommandNameForID(
static_cast<MTRClusterIDType>(cluster.unsignedLongLongValue),
static_cast<MTRCommandIDType>(command.unsignedLongLongValue));
longestCommand = @(MAX(longestCommand.unsignedLongLongValue, name.length));
}
for (NSNumber * command in commands) {
NSString * name = MTRRequestCommandNameForID(
static_cast<MTRClusterIDType>(cluster.unsignedLongLongValue),
static_cast<MTRCommandIDType>(command.unsignedLongLongValue));
AddClusterToBufferMemberContent(name, command, longestCommand, command == commands.lastObject);
}
}
void AddAttributesToBuffer(MTRDevice * device, NSNumber * endpoint, NSNumber * cluster, NSError * _Nonnull __autoreleasing * error)
{
auto * attributes = ReadAsNumbers(
device,
endpoint,
cluster,
@(MTRAttributeIDTypeGlobalAttributeAttributeListID),
error);
VerifyOrReturn(nil == *error);
if (attributes.count == 0) {
return;
}
AddClusterToBufferMemberHeader(@"Attributes");
NSNumber * longestAttribute = @(0);
for (NSNumber * attribute in attributes) {
NSString * name = MTRAttributeNameForID(
static_cast<MTRClusterIDType>(cluster.unsignedLongLongValue),
static_cast<MTRAttributeIDType>(attribute.unsignedLongLongValue));
longestAttribute = @(MAX(longestAttribute.unsignedLongLongValue, name.length));
}
for (NSNumber * attribute in attributes) {
NSString * name = MTRAttributeNameForID(
static_cast<MTRClusterIDType>(cluster.unsignedLongLongValue),
static_cast<MTRAttributeIDType>(attribute.unsignedLongLongValue));
AddClusterToBufferMemberContent(name, attribute, longestAttribute, attribute == attributes.lastObject);
}
}
void AddClusterToBufferMemberHeader(NSString * header)
{
AddToBuffer(@"\t┃ ┃");
AddToBuffer(@"\t┃ ┣━ %@", header);
}
void AddClusterToBufferMemberContent(NSString * name, NSNumber * value, NSNumber * longest, BOOL isLast)
{
NSUInteger padCount = static_cast<NSUInteger>(MAX(0, static_cast<NSInteger>(longest.unsignedLongLongValue) - static_cast<NSInteger>(name.length)));
NSString * padding = [@"" stringByPaddingToLength:padCount withString:@"╶" startingAtIndex:0];
if (isLast) {
AddToBuffer(@"\t┃ ┃ ┗━━ %@%@ [ID: %@ (%@)]", name, padding, value, FormatID(value));
} else {
AddToBuffer(@"\t┃ ┃ ┣━━ %@%@ [ID: %@ (%@)]", name, padding, value, FormatID(value));
}
}
NSString * _Nullable GetDeviceType(MTRDevice * device, NSNumber * endpoint, NSError * _Nonnull __autoreleasing * error)
{
auto * deviceTypes = ReadAsStructures(
device,
endpoint,
@(MTRClusterIDTypeDescriptorID),
@(MTRAttributeIDTypeClusterDescriptorAttributeDeviceTypeListID),
error);
VerifyOrReturnValue(nil == *error, nil);
NSString * deviceTypeString = @"";
for (NSDictionary * value in deviceTypes) {
NSNumber * deviceTypeID = value[@(0)];
NSNumber * deviceTypeRevision = value[@(1)];
auto * deviceType = [MTRDeviceType deviceTypeForID:deviceTypeID];
deviceTypeString = [deviceTypeString stringByAppendingFormat:@" (%@, Revision: %@) ", deviceType.name, deviceTypeRevision];
}
return deviceTypeString;
}
NSString * _Nullable GetFeatureMap(MTRDevice * device, NSNumber * endpoint, NSError * _Nonnull __autoreleasing * error)
{
auto * featureMap = ReadAsNumber(
device,
endpoint,
@(MTRClusterIDTypeDescriptorID),
@(MTRAttributeIDTypeGlobalAttributeFeatureMapID),
error);
VerifyOrReturnValue(nil == *error, nil);
// Ideally, we would like to return the features as strings, but there are no such methods to get the feature names.
return [NSString stringWithFormat:@"%@", featureMap];
}
NSNumber * _Nullable ReadResponseAsNumber(NSDictionary * response, NSError * _Nonnull __autoreleasing * error)
{
NSString * responseType = response[MTRTypeKey];
if (![responseType isEqual:MTRSignedIntegerValueType] && ![responseType isEqual:MTRUnsignedIntegerValueType]) {
*error = [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:@ { NSLocalizedDescriptionKey : kErrorNotANumber }];
return nil;
}
return response[MTRValueKey];
}
NSArray<NSNumber *> * _Nullable ReadResponseAsNumbers(NSDictionary * response, NSError * _Nonnull __autoreleasing * error)
{
NSMutableArray<NSNumber *> * values = [NSMutableArray array];
NSString * responseType = response[MTRTypeKey];
if (![responseType isEqual:MTRArrayValueType]) {
*error = [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:@ { NSLocalizedDescriptionKey : kErrorNotAnArray }];
return nil;
}
NSArray * responseValues = response[MTRValueKey];
for (NSDictionary * responseValue in responseValues) {
auto * value = ReadResponseAsNumber(responseValue[MTRDataKey], error);
if (*error) {
// Bubble the underlying error up to the caller unchanged
return nil;
}
[values addObject:value];
}
return values;
}
NSDictionary * _Nullable ReadResponseAsStructure(NSDictionary * response, NSError * _Nonnull __autoreleasing * error)
{
NSString * responseType = response[MTRTypeKey];
if (![responseType isEqual:MTRStructureValueType]) {
*error = [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:@ { NSLocalizedDescriptionKey : kErrorNotAStructure }];
return nil;
}
NSMutableDictionary * value = [NSMutableDictionary dictionary];
NSArray * responseValues = response[MTRValueKey];
for (NSDictionary * responseValue in responseValues) {
auto * numberValue = ReadResponseAsNumber(responseValue[MTRDataKey], error);
if (*error) {
// Bubble the underlying error up to the caller unchanged
return nil;
}
value[responseValue[MTRContextTagKey]] = numberValue;
}
return value;
}
NSArray<NSDictionary *> * _Nullable ReadResponseAsStructures(NSDictionary * response, NSError * _Nonnull __autoreleasing * error)
{
NSMutableArray<NSDictionary *> * values = [NSMutableArray array];
NSString * responseType = response[MTRTypeKey];
if (![responseType isEqual:MTRArrayValueType]) {
*error = [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:@ { NSLocalizedDescriptionKey : kErrorNotAnArray }];
return nil;
}
NSArray * responseValues = response[MTRValueKey];
for (NSDictionary * responseValue in responseValues) {
auto * value = ReadResponseAsStructure(responseValue[MTRDataKey], error);
if (*error) {
// Bubble the underlying error up to the caller unchanged
return nil;
}
[values addObject:value];
}
return values;
}
NSNumber * _Nullable ReadAsNumber(MTRDevice * device, NSNumber * endpoint, NSNumber * cluster, NSNumber * attribute, NSError * _Nonnull __autoreleasing * error)
{
auto * response = [device readAttributeWithEndpointID:endpoint clusterID:cluster attributeID:attribute params:nil];
if (nil == response) {
*error = [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:@ { NSLocalizedDescriptionKey : kErrorNilNumber }];
return nil;
}
NSError * innerError = nil;
auto * value = ReadResponseAsNumber(response, &innerError);
VerifyOrReturnValue(nil == innerError, nil, WrapError(innerError, endpoint, cluster, attribute, error));
return value;
}
NSArray<NSNumber *> * _Nullable ReadAsNumbers(MTRDevice * device, NSNumber * endpoint, NSNumber * cluster, NSNumber * attribute, NSError * _Nonnull __autoreleasing * error)
{
auto * response = [device readAttributeWithEndpointID:endpoint clusterID:cluster attributeID:attribute params:nil];
if (nil == response) {
*error = [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:@ { NSLocalizedDescriptionKey : kErrorNilArray }];
return nil;
}
NSError * innerError = nil;
auto * value = ReadResponseAsNumbers(response, &innerError);
VerifyOrReturnValue(nil == innerError, nil, WrapError(innerError, endpoint, cluster, attribute, error));
return value;
}
NSArray<NSDictionary *> * _Nullable ReadAsStructures(MTRDevice * device, NSNumber * endpoint, NSNumber * cluster, NSNumber * attribute, NSError * _Nonnull __autoreleasing * error)
{
auto * response = [device readAttributeWithEndpointID:endpoint clusterID:cluster attributeID:attribute params:nil];
if (nil == response) {
*error = [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:@ { NSLocalizedDescriptionKey : kErrorNilStructure }];
return nil;
}
NSError * innerError = nil;
auto * value = ReadResponseAsStructures(response, &innerError);
VerifyOrReturnValue(nil == innerError, nil, WrapError(innerError, endpoint, cluster, attribute, error));
return value;
}
void WrapError(NSError * innerError, NSNumber * endpoint, NSNumber * cluster, NSNumber * attribute, NSError * _Nonnull __autoreleasing * error)
{
auto * clusterName = MTRClusterNameForID(static_cast<MTRClusterIDType>(cluster.unsignedLongLongValue));
auto * attributeName = MTRAttributeNameForID(
static_cast<MTRClusterIDType>(cluster.unsignedLongLongValue),
static_cast<MTRAttributeIDType>(attribute.unsignedLongLongValue));
auto * userInfo = @ { NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Endpoint: %@ - Cluster: %@ - Attribute: %@ : %@", endpoint, clusterName, attributeName, innerError.localizedDescription] };
*error = [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:userInfo];
}
static inline NSString * FormatID(NSNumber * value)
{
unsigned long long v = value.unsignedLongLongValue;
if (v <= 0xFFFFULL) {
return [NSString stringWithFormat:@"0x%04llX", v];
} else if (v <= 0xFFFFFFFFULL) {
return [NSString stringWithFormat:@"0x%08llX", v];
} else {
return [NSString stringWithFormat:@"0x%016llX", v];
}
}
void AddToBuffer(NSString * format, ...) NS_FORMAT_FUNCTION(2, 3)
{
va_list args;
va_start(args, format);
auto * line = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
[mLogBuffer appendString:line];
[mLogBuffer appendString:@"\n"];
}
chip::NodeId mNodeId;
chip::Optional<chip::EndpointId> mEndPointId;
NSMutableString * mLogBuffer;
DeviceDelegate * mDeviceDelegate = nil;
};
NS_ASSUME_NONNULL_END