Skip to content

Commit

Permalink
Enable chunking on writes via MTRBaseDevice's writeAttributeWithEndpo…
Browse files Browse the repository at this point in the history
…intID:.

We used to just encode the list a single TLV item, which did not allow the write
to chunk it.  To fix that, create a DataModel::List with the individual list
items, and encode that.
  • Loading branch information
bzbarsky-apple committed Apr 16, 2024
1 parent 4aadee7 commit dbb38c1
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 3 deletions.
51 changes: 48 additions & 3 deletions src/darwin/Framework/CHIP/MTRBaseDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
#include <app/ClusterStateCache.h>
#include <app/InteractionModelEngine.h>
#include <app/ReadClient.h>
#include <app/data-model/List.h>
#include <controller/CommissioningWindowOpener.h>
#include <controller/ReadInteraction.h>
#include <controller/WriteInteraction.h>
Expand Down Expand Up @@ -633,10 +634,11 @@ static CHIP_ERROR MTREncodeTLVFromDataValueDictionary(id object, chip::TLV::TLVW
}
NSString * typeName = ((NSDictionary *) object)[MTRTypeKey];
id value = ((NSDictionary *) object)[MTRValueKey];
if (!typeName) {
if (![typeName isKindOfClass:[NSString class]]) {
MTR_LOG_ERROR("Error: Object to encode is corrupt");
return CHIP_ERROR_INVALID_ARGUMENT;
}

if ([typeName isEqualToString:MTRSignedIntegerValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt signed integer type: %@", [value class]);
Expand Down Expand Up @@ -1219,10 +1221,53 @@ - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
auto onFailureCb = [failureCb, bridge](
const app::ConcreteAttributePath * attribPath, CHIP_ERROR aError) { failureCb(bridge, aError); };

return chip::Controller::WriteAttribute<MTRDataValueDictionaryDecodableType>(session,
// To handle list chunking properly, we have to convert lists into
// DataModel::List here, because that's special-cased in
// WriteClient.
if (![value isKindOfClass:NSDictionary.class]) {
MTR_LOG_ERROR("Error: Unsupported object to write as attribute value: %@", value);
return CHIP_ERROR_INVALID_ARGUMENT;
}

NSDictionary<NSString *, id> * dataValue = value;
NSString * typeName = dataValue[MTRTypeKey];
if (![typeName isKindOfClass:NSString.class]) {
MTR_LOG_ERROR("Error: Object to encode is corrupt: %@", dataValue);
return CHIP_ERROR_INVALID_ARGUMENT;
}

if (![typeName isEqualToString:MTRArrayValueType]) {
return chip::Controller::WriteAttribute<MTRDataValueDictionaryDecodableType>(session,
static_cast<chip::EndpointId>([endpointID unsignedShortValue]),
static_cast<chip::ClusterId>([clusterID unsignedLongValue]),
static_cast<chip::AttributeId>([attributeID unsignedLongValue]), MTRDataValueDictionaryDecodableType(value),
onSuccessCb, onFailureCb, (timeoutMs == nil) ? NullOptional : Optional<uint16_t>([timeoutMs unsignedShortValue]));
}

// Now we are dealing with a list.
NSArray<NSDictionary<NSString *, id> *> * arrayValue = value[MTRValueKey];
if (![arrayValue isKindOfClass:NSArray.class]) {
MTR_LOG_ERROR("Error: Object to encode claims to be a list but isn't: %@", arrayValue);
return CHIP_ERROR_INVALID_ARGUMENT;
}

std::vector<MTRDataValueDictionaryDecodableType> encodableVector;
encodableVector.reserve(arrayValue.count);

for (NSDictionary<NSString *, id> * arrayItem in arrayValue) {
if (![arrayItem isKindOfClass:NSDictionary.class]) {
MTR_LOG_ERROR("Error: Can't encode corrupt list: %@", arrayValue);
return CHIP_ERROR_INVALID_ARGUMENT;
}

encodableVector.push_back(MTRDataValueDictionaryDecodableType(arrayItem[MTRDataKey]));
}

DataModel::List<MTRDataValueDictionaryDecodableType> encodableList(encodableVector.data(), encodableVector.size());
return chip::Controller::WriteAttribute<DataModel::List<MTRDataValueDictionaryDecodableType>>(session,
static_cast<chip::EndpointId>([endpointID unsignedShortValue]),
static_cast<chip::ClusterId>([clusterID unsignedLongValue]),
static_cast<chip::AttributeId>([attributeID unsignedLongValue]), MTRDataValueDictionaryDecodableType(value),
static_cast<chip::AttributeId>([attributeID unsignedLongValue]), encodableList,
onSuccessCb, onFailureCb, (timeoutMs == nil) ? NullOptional : Optional<uint16_t>([timeoutMs unsignedShortValue]));
});
std::move(*bridge).DispatchAction(self);
Expand Down
167 changes: 167 additions & 0 deletions src/darwin/Framework/CHIPTests/MTROTAProviderTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// module headers
#import <Matter/Matter.h>

#import "MTRDeviceTestDelegate.h"
#import "MTRErrorTestUtils.h"
#import "MTRTestKeys.h"
#import "MTRTestResetCommissioneeHelper.h"
Expand Down Expand Up @@ -1564,6 +1565,172 @@ - (void)test007_DoBDXTransferIncrementalOtaUpdate
}
#endif // ENABLE_REAL_OTA_UPDATE_TESTS

- (void)test008_TestWriteDefaultOTAProviders
{
__auto_type * runner = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload1 testcase:self];
MTRDevice * device = [runner commissionWithNodeID:@(kDeviceId1)];

dispatch_queue_t queue = dispatch_get_main_queue();

__auto_type dataValue = ^(uint16_t endpoint) {
return @{
MTRTypeKey : MTRArrayValueType,
MTRValueKey : @[
@{
MTRDataKey : @ {
MTRTypeKey : MTRStructureValueType,
MTRValueKey : @[
@{
MTRContextTagKey : @(1),
MTRDataKey : @ {
MTRTypeKey : MTRUnsignedIntegerValueType,
MTRValueKey : @(kDeviceId1),
},
},
@{
MTRContextTagKey : @(2),
MTRDataKey : @ {
MTRTypeKey : MTRUnsignedIntegerValueType,
MTRValueKey : @(endpoint),
},
},
],
},
},
],
};
};

{
// Test with MTRBaseDevice first.
MTRBaseDevice * baseDevice = [MTRBaseDevice deviceWithNodeID:device.nodeID
controller:device.deviceController];

__auto_type * cluster = [[MTRBaseClusterOTASoftwareUpdateRequestor alloc] initWithDevice:baseDevice
endpointID:@(0)
queue:queue];
__auto_type * providerLocation = [[MTROTASoftwareUpdateRequestorClusterProviderLocation alloc] init];
providerLocation.providerNodeID = @(kDeviceId1);
providerLocation.endpoint = @(0);
__auto_type * value = @[ providerLocation ];

__auto_type * writeBaseClusterExpectation = [self expectationWithDescription:@"Write succeeded via MTRBaseCluster"];
[cluster writeAttributeDefaultOTAProvidersWithValue:value
completion:^(NSError * _Nullable error) {
XCTAssertNil(error);
[writeBaseClusterExpectation fulfill];
}];
[self waitForExpectations:@[ writeBaseClusterExpectation ] timeout:kTimeoutInSeconds];

__auto_type * writeBaseDeviceExpectation = [self expectationWithDescription:@"Write succeeded via MTRBaseDevice"];
[baseDevice writeAttributeWithEndpointID:@(0)
clusterID:@(MTRClusterIDTypeOTASoftwareUpdateRequestorID)
attributeID:@(MTRAttributeIDTypeClusterOTASoftwareUpdateRequestorAttributeDefaultOTAProvidersID)
value:dataValue(0)
timedWriteTimeout:nil
queue:queue
completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
XCTAssertNil(error);
XCTAssertNotNil(values);
XCTAssertEqual(values.count, 1);

for (NSDictionary<NSString *, id> * value in values) {
XCTAssertNil(value[MTRErrorKey]);
}
[writeBaseDeviceExpectation fulfill];
}];
[self waitForExpectations:@[ writeBaseDeviceExpectation ] timeout:kTimeoutInSeconds];
}

{
// Now test with MTRDevice
__auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
// Make sure we don't have expected value notifications confusing our
// attribute reports.
delegate.skipExpectedValuesForWrite = YES;

XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Subscription established"];
delegate.onReportEnd = ^() {
[gotReportsExpectation fulfill];
};

[device setDelegate:delegate queue:queue];

[self waitForExpectations:@[ gotReportsExpectation ] timeout:60];

delegate.onReportEnd = nil;

__auto_type * expectedAttributePath = [MTRAttributePath attributePathWithEndpointID:@(0)
clusterID:@(MTRClusterIDTypeOTASoftwareUpdateRequestorID)
attributeID:@(MTRAttributeIDTypeClusterOTASoftwareUpdateRequestorAttributeDefaultOTAProvidersID)];

__block __auto_type * expectedValue = dataValue(1);

__block __auto_type * writeExpectation = [self expectationWithDescription:@"Write succeeded via MTRCluster"];
delegate.onAttributeDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * data) {
XCTAssertNotNil(data);
XCTAssertEqual(data.count, 1);
NSDictionary<NSString *, id> * item = data[0];

XCTAssertNil(item[MTRErrorKey]);

MTRAttributePath * path = item[MTRAttributePathKey];
XCTAssertNotNil(path);

XCTAssertEqualObjects(path, expectedAttributePath);

NSDictionary<NSString *, id> * receivedValue = item[MTRDataKey];

// We can't use XCTAssertEqualObjects to compare receivedValue to
// expectedValue here, because receivedValue has a DataVersion
// that's missing from expectedValue, and the struct in it has an
// extra FabricIndex field.
XCTAssertEqualObjects(receivedValue[MTRTypeKey], MTRArrayValueType);

NSArray * receivedArray = receivedValue[MTRValueKey];
NSArray * expectedArray = expectedValue[MTRValueKey];

XCTAssertEqual(receivedArray.count, expectedArray.count);

for (NSUInteger i = 0; i < receivedArray.count; ++i) {
NSDictionary * receivedItem = receivedArray[i][MTRDataKey];
NSDictionary * expectedItem = expectedArray[i][MTRDataKey];

XCTAssertEqual(receivedItem[MTRTypeKey], MTRStructureValueType);
XCTAssertEqual(expectedItem[MTRTypeKey], MTRStructureValueType);

NSArray * receivedFields = receivedItem[MTRValueKey];
NSArray * expectedFields = expectedItem[MTRValueKey];

// Account for the extra FabricIndex.
XCTAssertEqual(receivedFields.count, expectedFields.count + 1);
for (NSUInteger j = 0; j < expectedFields.count; ++j) {
XCTAssertEqualObjects(receivedFields[j], expectedFields[j]);
}
}

[writeExpectation fulfill];
};

__auto_type * cluster = [[MTRClusterOTASoftwareUpdateRequestor alloc] initWithDevice:device
endpointID:@(0)
queue:queue];
[cluster writeAttributeDefaultOTAProvidersWithValue:expectedValue
expectedValueInterval:@(0)];
[self waitForExpectations:@[ writeExpectation ] timeout:kTimeoutInSeconds];

expectedValue = dataValue(2);
writeExpectation = [self expectationWithDescription:@"Write succeeded via MTRDevice"];
[device writeAttributeWithEndpointID:@(0)
clusterID:@(MTRClusterIDTypeOTASoftwareUpdateRequestorID)
attributeID:@(MTRAttributeIDTypeClusterOTASoftwareUpdateRequestorAttributeDefaultOTAProvidersID)
value:expectedValue
expectedValueInterval:@(0)
timedWriteTimeout:nil];
[self waitForExpectations:@[ writeExpectation ] timeout:kTimeoutInSeconds];
}
}

- (void)test999_TearDown
{
[[self class] shutdownStack];
Expand Down

0 comments on commit dbb38c1

Please sign in to comment.