Skip to content

Commit

Permalink
Implement MTRBaseCluster invokes on top of MTRBaseDevice. (#29554)
Browse files Browse the repository at this point in the history
* Implement MTRBaseCluster invokes on top of MTRBaseDevice.

This gets rid of command bridge code and shares code a lot better.

* Address review comment.

* Remove some more code.
  • Loading branch information
bzbarsky-apple authored and pull[bot] committed Dec 13, 2024
1 parent c8ec318 commit 1548454
Show file tree
Hide file tree
Showing 12 changed files with 5,138 additions and 10,331 deletions.
165 changes: 0 additions & 165 deletions src/darwin/Framework/CHIP/MTRBaseClusterUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -263,169 +263,4 @@ void MTRReadAttribute(MTRReadParams * _Nonnull params,
std::move(*callbackBridge).DispatchAction(device);
}

/**
* Utility functions base clusters use for doing commands.
*/
template <typename InvokeBridgeType, typename ResponseType>
class MTRInvokeCallback : public chip::app::CommandSender::Callback {
public:
MTRInvokeCallback(InvokeBridgeType * _Nonnull bridge, typename InvokeBridgeType::SuccessCallbackType _Nonnull onResponse,
MTRErrorCallback _Nonnull onError)
: mBridge(bridge)
, mOnResponse(onResponse)
, mOnError(onError)
{
}

~MTRInvokeCallback() {}

void AdoptCommandSender(chip::Platform::UniquePtr<chip::app::CommandSender> commandSender)
{
mCommandSender = std::move(commandSender);
}

protected:
// We need to have different OnResponse implementations depending on whether
// ResponseType is DataModel::NullObjectType or not. Since template class methods
// can't be partially specialized (either you have to partially specialize
// the class template, or you have to fully specialize the method), use
// enable_if to deal with this.
void OnResponse(chip::app::CommandSender * commandSender, const chip::app::ConcreteCommandPath & commandPath,
const chip::app::StatusIB & status, chip::TLV::TLVReader * reader) override
{
HandleResponse(commandSender, commandPath, status, reader);
}

/**
* Response handler for data responses.
*/
template <typename T = ResponseType, std::enable_if_t<!std::is_same<T, chip::app::DataModel::NullObjectType>::value, int> = 0>
void HandleResponse(chip::app::CommandSender * commandSender, const chip::app::ConcreteCommandPath & commandPath,
const chip::app::StatusIB & status, chip::TLV::TLVReader * reader)
{
if (mCalledCallback) {
return;
}
mCalledCallback = true;

ResponseType response;
CHIP_ERROR err = CHIP_NO_ERROR;

//
// We're expecting response data in this variant of OnResponse. Consequently, reader should always be
// non-null. If it is, it means we received a success status code instead, which is not what was expected.
//
VerifyOrExit(reader != nullptr, err = CHIP_ERROR_SCHEMA_MISMATCH);

//
// Validate that the data response we received matches what we expect in terms of its cluster and command IDs.
//
VerifyOrExit(
commandPath.mClusterId == ResponseType::GetClusterId() && commandPath.mCommandId == ResponseType::GetCommandId(),
err = CHIP_ERROR_SCHEMA_MISMATCH);

err = chip::app::DataModel::Decode(*reader, response);
SuccessOrExit(err);

mOnResponse(mBridge, response);

exit:
if (err != CHIP_NO_ERROR) {
mOnError(mBridge, err);
}
}

/**
* Response handler for status responses.
*/
template <typename T = ResponseType, std::enable_if_t<std::is_same<T, chip::app::DataModel::NullObjectType>::value, int> = 0>
void HandleResponse(chip::app::CommandSender * commandSender, const chip::app::ConcreteCommandPath & commandPath,
const chip::app::StatusIB & status, chip::TLV::TLVReader * reader)
{
if (mCalledCallback) {
return;
}
mCalledCallback = true;

//
// If we got a valid reader, it means we received response data that we were not expecting to receive.
//
if (reader != nullptr) {
mOnError(mBridge, CHIP_ERROR_SCHEMA_MISMATCH);
return;
}

chip::app::DataModel::NullObjectType nullResp;
mOnResponse(mBridge, nullResp);
}

void OnError(const chip::app::CommandSender * commandSender, CHIP_ERROR error) override
{
if (mCalledCallback) {
return;
}
mCalledCallback = true;

mOnError(mBridge, error);
}

void OnDone(chip::app::CommandSender * commandSender) override
{
if (!mCalledCallback) {
// This can happen if the server sends a response with an empty
// InvokeResponses list. Since we are not sending wildcard command
// paths, that's not a valid response and we should treat it as an
// error. Use the error we would have gotten if we in fact expected
// a nonempty list.
OnError(commandSender, CHIP_END_OF_TLV);
}

chip::Platform::Delete(this);
}

InvokeBridgeType * _Nonnull mBridge;

typename InvokeBridgeType::SuccessCallbackType mOnResponse;
MTRErrorCallback mOnError;
chip::Platform::UniquePtr<chip::app::CommandSender> mCommandSender;
// For reads, we ensure that we make only one data/error callback to our consumer.
bool mCalledCallback = false;
};

/**
* timedInvokeTimeoutMs, if provided, is how long the server will wait for us to
* send the invoke after we sent the Timed Request message.
*
* invokeTimeout, if provided, will have possible MRP latency added to it and
* the result is how long we will wait for the server to respond.
*/
template <typename BridgeType, typename RequestDataType>
CHIP_ERROR MTRStartInvokeInteraction(BridgeType * _Nonnull bridge, const RequestDataType & requestData,
chip::Messaging::ExchangeManager & exchangeManager, const chip::SessionHandle & session,
typename BridgeType::SuccessCallbackType successCb, MTRErrorCallback failureCb, chip::EndpointId endpoint,
chip::Optional<uint16_t> timedInvokeTimeoutMs, chip::Optional<chip::System::Clock::Timeout> invokeTimeout)
{
auto callback = chip::Platform::MakeUnique<MTRInvokeCallback<BridgeType, typename RequestDataType::ResponseType>>(
bridge, successCb, failureCb);
VerifyOrReturnError(callback != nullptr, CHIP_ERROR_NO_MEMORY);

auto commandSender
= chip::Platform::MakeUnique<chip::app::CommandSender>(callback.get(), &exchangeManager, timedInvokeTimeoutMs.HasValue());
VerifyOrReturnError(commandSender != nullptr, CHIP_ERROR_NO_MEMORY);

chip::app::CommandPathParams commandPath(endpoint, 0, RequestDataType::GetClusterId(), RequestDataType::GetCommandId(),
chip::app::CommandPathFlags::kEndpointIdValid);
ReturnErrorOnFailure(commandSender->AddRequestData(commandPath, requestData, timedInvokeTimeoutMs));

if (invokeTimeout.HasValue()) {
invokeTimeout.SetValue(session->ComputeRoundTripTimeout(invokeTimeout.Value()));
}
ReturnErrorOnFailure(commandSender->SendCommandRequest(session, invokeTimeout));

callback->AdoptCommandSender(std::move(commandSender));
callback.release();

return CHIP_NO_ERROR;
};

NS_ASSUME_NONNULL_END
42 changes: 42 additions & 0 deletions src/darwin/Framework/CHIP/MTRBaseDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#import "MTRSetupPayload_Internal.h"
#import "NSDataSpanConversion.h"
#import "NSStringSpanConversion.h"
#import "zap-generated/MTRCommandPayloads_Internal.h"

#include "app/ConcreteAttributePath.h"
#include "app/ConcreteCommandPath.h"
Expand Down Expand Up @@ -1308,6 +1309,47 @@ - (void)_invokeCommandWithEndpointID:(NSNumber *)endpointID
std::move(*bridge).DispatchAction(self);
}

- (void)_invokeKnownCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandPayload:(id)commandPayload
timedInvokeTimeout:(NSNumber * _Nullable)timeout
serverSideProcessingTimeout:(NSNumber * _Nullable)serverSideProcessingTimeout
responseClass:(Class _Nullable)responseClass
queue:(dispatch_queue_t)queue
completion:(void (^)(id _Nullable response, NSError * _Nullable error))completion
{
NSError * encodingError;
auto * commandFields = [commandPayload _encodeAsDataValue:&encodingError];
if (commandFields == nil) {
dispatch_async(queue, ^{
completion(nil, encodingError);
});
return;
}

auto responseHandler = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
id _Nullable response = nil;
if (error == nil) {
if (values.count != 1) {
error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeSchemaMismatch userInfo:nil];
} else if (responseClass != nil) {
response = [[responseClass alloc] initWithResponseValue:values[0] error:&error];
}
}
completion(response, error);
};

[self _invokeCommandWithEndpointID:endpointID
clusterID:clusterID
commandID:commandID
commandFields:commandFields
timedInvokeTimeout:timeout
serverSideProcessingTimeout:serverSideProcessingTimeout
queue:queue
completion:responseHandler];
}

- (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
attributeID:(NSNumber * _Nullable)attributeID
Expand Down
18 changes: 18 additions & 0 deletions src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ static inline MTRTransportType MTRMakeTransportType(chip::Transport::Type type)
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion;

/**
* Like the public invokeCommandWithEndpointID but:
*
* 1) Allows passing through a serverSideProcessingTimeout.
* 2) Expects one of the command payload structs as commandPayload
* 3) On success, returns an instance of responseClass via the completion (or
* nil if there is no responseClass, which indicates a status-only command).
*/
- (void)_invokeKnownCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandPayload:(id)commandPayload
timedInvokeTimeout:(NSNumber * _Nullable)timeout
serverSideProcessingTimeout:(NSNumber * _Nullable)serverSideProcessingTimeout
responseClass:(Class _Nullable)responseClass
queue:(dispatch_queue_t)queue
completion:(void (^)(id _Nullable response, NSError * _Nullable error))completion;

@end

@interface MTRClusterPath ()
Expand Down
86 changes: 32 additions & 54 deletions src/darwin/Framework/CHIP/templates/MTRBaseClusters-src.zapt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#import "NSStringSpanConversion.h"
#import "NSDataSpanConversion.h"

#include <app-common/zap-generated/cluster-objects.h>
#include <controller/CHIPCluster.h>
#include <lib/support/CHIPListUtils.h>
#include <platform/CHIPDeviceLayer.h>
Expand Down Expand Up @@ -53,7 +54,6 @@ using chip::System::Clock::Seconds16;
{{! This is used as the implementation for both the new-name and old-name bits, so check for both here. }}
{{#if (or (isSupported cluster command=command)
(isSupported (compatClusterNameRemapping parent.name) command=(compatCommandNameRemapping parent.name name)))}}
{{#*inline "callbackName"}}{{#if hasSpecificResponse}}{{cluster}}Cluster{{asUpperCamelCase responseName preserveAcronyms=true}}{{else}}CommandSuccess{{/if}}{{/inline}}
{{#*inline "paramsType"}}
{{#unless (isSupported cluster command=command)}}
MTR{{compatClusterNameRemapping parent.name}}Cluster{{compatCommandNameRemapping parent.name name}}Params
Expand All @@ -69,61 +69,39 @@ MTR{{cluster}}Cluster{{command}}Params
{{/unless}}
- (void){{asLowerCamelCase name}}WithParams: ({{> paramsType}} * {{#unless commandHasRequiredField}}_Nullable{{/unless}})params completion:({{>command_completion_type command=.}})completion
{
// Make a copy of params before we go async.
params = [params copy];
auto * bridge = new MTR{{>callbackName}}CallbackBridge(self.callbackQueue,
{{#if hasSpecificResponse}}
{{! This treats completion as taking an id for the data. This is
not great from a type-safety perspective, of course. }}
completion,
{{else}}
{{! For now, don't change the bridge API; instead just use an adapter
to invoke our completion handler. This is not great from a
type-safety perspective, of course. }}
^(id _Nullable value, NSError * _Nullable error) {
completion(error);
},
{{/if}}
^(ExchangeManager & exchangeManager, const SessionHandle & session, {{>callbackName}}CallbackType successCb, MTRErrorCallback failureCb, MTRCallbackBridgeBase * bridge) {
auto * typedBridge = static_cast<MTR{{>callbackName}}CallbackBridge *>(bridge);
Optional<uint16_t> timedInvokeTimeoutMs;
Optional<Timeout> invokeTimeout;
ListFreer listFreer;
{{asUpperCamelCase parent.name}}::Commands::{{asUpperCamelCase name}}::Type request;
if (params != nil) {
if (params.timedInvokeTimeoutMs != nil) {
params.timedInvokeTimeoutMs = MTRClampedNumber(params.timedInvokeTimeoutMs, @(1), @(UINT16_MAX));
timedInvokeTimeoutMs.SetValue(params.timedInvokeTimeoutMs.unsignedShortValue);
}
if (params.serverSideProcessingTimeout != nil) {
// Clamp to a number of seconds that will not overflow 32-bit
// int when converted to ms.
auto * serverSideProcessingTimeout = MTRClampedNumber(params.serverSideProcessingTimeout, @(0), @(UINT16_MAX));
invokeTimeout.SetValue(Seconds16(serverSideProcessingTimeout.unsignedShortValue));
}
}
{{#if mustUseTimedInvoke}}
if (!timedInvokeTimeoutMs.HasValue()) {
timedInvokeTimeoutMs.SetValue(10000);
}
if (params == nil) {
params = [[{{> paramsType}} alloc] init];
}

auto responseHandler = ^(id _Nullable response, NSError * _Nullable error) {
{{#if hasSpecificResponse}}
completion(response, error);
{{else}}
completion(error);
{{/if}}
{{#zcl_command_arguments}}
{{#first}}
{{#unless parent.commandHasRequiredField}}
if (params != nil) {
{{/unless}}
{{/first}}
{{>encode_value target=(concat "request." (asLowerCamelCase label)) source=(concat "params." (asStructPropertyName label)) cluster=parent.parent.name errorCode="return CHIP_ERROR_INVALID_ARGUMENT;" depth=0}}
{{#last}}
{{#unless parent.commandHasRequiredField}}
}
{{/unless}}
{{/last}}
{{/zcl_command_arguments}}
};

return MTRStartInvokeInteraction(typedBridge, request, exchangeManager, session, successCb, failureCb, self.endpoint, timedInvokeTimeoutMs, invokeTimeout);
});
std::move(*bridge).DispatchAction(self.device);
auto * timedInvokeTimeoutMs = params.timedInvokeTimeoutMs;
{{#if mustUseTimedInvoke}}
if (timedInvokeTimeoutMs == nil) {
timedInvokeTimeoutMs = @(10000);
}
{{/if}}

using RequestType = {{asUpperCamelCase parent.name}}::Commands::{{asUpperCamelCase name}}::Type;
[self.device _invokeKnownCommandWithEndpointID:@(self.endpoint)
clusterID:@(RequestType::GetClusterId())
commandID:@(RequestType::GetCommandId())
commandPayload:params
timedInvokeTimeout:timedInvokeTimeoutMs
serverSideProcessingTimeout:params.serverSideProcessingTimeout
{{#if hasSpecificResponse}}
responseClass:MTR{{cluster}}Cluster{{asUpperCamelCase responseName preserveAcronyms=true}}Params.class
{{else}}
responseClass:nil
{{/if}}
queue:self.callbackQueue
completion:responseHandler];
}
{{/if}}
{{/inline}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#include <lib/support/TypeTraits.h>

{{#>MTRCallbackBridge partial-type="Status" }}DefaultSuccessCallback{{/MTRCallbackBridge}}
{{#>MTRCallbackBridge partial-type="CommandStatus" }}CommandSuccessCallback{{/MTRCallbackBridge}}
{{#>MTRCallbackBridge type="Octet_String" isNullable=false ns="chip"}}OctetStringAttributeCallback{{/MTRCallbackBridge}}
{{#>MTRCallbackBridge type="Octet_String" isNullable=true ns="chip"}}NullableOctetStringAttributeCallback{{/MTRCallbackBridge}}
{{#>MTRCallbackBridge type="Char_String" isNullable=false ns="chip"}}CharStringAttributeCallback{{/MTRCallbackBridge}}
Expand Down Expand Up @@ -57,14 +56,6 @@
{{/zcl_attributes_server}}
{{/zcl_clusters}}

{{#zcl_clusters}}
{{#zcl_command_responses}}
{{#if (isSupported (asUpperCamelCase ../name preserveAcronyms=true) command=(asUpperCamelCase name preserveAcronyms=true))}}
{{#>MTRCallbackBridge partial-type="Command" }}{{asUpperCamelCase ../../name preserveAcronyms=true}}Cluster{{asUpperCamelCase ../name preserveAcronyms=true}}Callback{{/MTRCallbackBridge}}
{{/if}}
{{/zcl_command_responses}}
{{/zcl_clusters}}

{{#zcl_clusters}}
{{#zcl_enums}}
{{#if (isSupported (asUpperCamelCase parent.name preserveAcronyms=true) enum=(asUpperCamelCase name preserveAcronyms=true))}}
Expand Down
Loading

0 comments on commit 1548454

Please sign in to comment.