blob: fa213ac06998321f73dbc6ea2f9a21321be19a9d [file] [log] [blame]
/*
*
* Copyright (c) 2020 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.
*/
/**
* @file
* This file defines objects for a CHIP IM Invoke Command Sender
*
*/
#pragma once
#include <type_traits>
#include "CommandSenderLegacyCallback.h"
#include <app/CommandPathParams.h>
#include <app/MessageDef/InvokeRequestMessage.h>
#include <app/MessageDef/InvokeResponseMessage.h>
#include <app/MessageDef/StatusIB.h>
#include <app/data-model/Encode.h>
#include <lib/core/CHIPCore.h>
#include <lib/core/Optional.h>
#include <lib/core/TLVDebug.h>
#include <lib/support/BitFlags.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/DLLUtil.h>
#include <lib/support/logging/CHIPLogging.h>
#include <messaging/ExchangeHolder.h>
#include <messaging/ExchangeMgr.h>
#include <messaging/Flags.h>
#include <protocols/Protocols.h>
#include <system/SystemPacketBuffer.h>
#include <system/TLVPacketBufferBackingStore.h>
#define COMMON_STATUS_SUCCESS 0
namespace chip {
namespace app {
class CommandSender final : public Messaging::ExchangeDelegate
{
public:
// CommandSender::ExtendableCallback::OnResponse is public SDK API, so we cannot break
// source compatibility for it. To allow for additional values to be added at a future
// time without constantly changing the function's declaration parameter list, we are
// defining the struct ResponseData and adding that to the parameter list to allow for
// future extendability.
struct ResponseData
{
// The command path field in invoke command response.
const ConcreteCommandPath & path;
// The status of the command. It can be any success status, including possibly a cluster-specific one.
// If `data` is not null, statusIB will always be a generic SUCCESS status with no-cluster specific
// information.
const StatusIB & statusIB;
// The command data, will be nullptr if the server returns a StatusIB.
TLV::TLVReader * data;
// Reference for the command. This should be associated with the reference value sent out in the initial
// invoke request.
Optional<uint16_t> commandRef;
};
// CommandSender::ExtendableCallback::OnError is public SDK API, so we cannot break source
// compatibility for it. To allow for additional values to be added at a future time
// without constantly changing the function's declaration parameter list, we are
// defining the struct ErrorData and adding that to the parameter list
// to allow for future extendability.
struct ErrorData
{
/**
* The following errors will be delivered through `error`
*
* - CHIP_ERROR_TIMEOUT: A response was not received within the expected response timeout.
* - CHIP_ERROR_*TLV*: A malformed, non-compliant response was received from the server.
* - CHIP_ERROR encapsulating a StatusIB: If we got a non-path-specific
* status response from the server. In that case,
* StatusIB::InitFromChipError can be used to extract the status.
* - CHIP_ERROR*: All other cases.
*/
CHIP_ERROR error;
};
/**
* @brief Callback that is extendable for future features, starting with batch commands
*
* The two major differences between ExtendableCallback and Callback are:
* 1. Path-specific errors go to OnResponse instead of OnError
* - Note: Non-path-specific errors still go to OnError.
* 2. Instead of having new parameters at the end of the arguments list, with defaults,
* as functionality expands, a parameter whose type is defined in this header is used
* as the argument to the callbacks
*
* To support batch commands client must use ExtendableCallback.
*/
class ExtendableCallback
{
public:
virtual ~ExtendableCallback() = default;
/**
* OnResponse will be called for all path specific responses from the server that have been received
* and processed. Specifically:
* - When a status code is received and it is IM::Success, aData will be nullptr.
* - When a status code is received and it is IM and/or cluster error, aData will be nullptr.
* - These kinds of errors are referred to as path-specific errors.
* - When a data response is received, aData will point to a valid TLVReader initialized to point at the struct container
* that contains the data payload (callee will still need to open and process the container).
*
* The CommandSender object MUST continue to exist after this call is completed. The application shall wait until it
* receives an OnDone call to destroy the object.
*
* @param[in] apCommandSender The command sender object that initiated the command transaction.
* @param[in] aResponseData Information pertaining to the response.
*/
;
virtual void OnResponse(CommandSender * commandSender, const ResponseData & aResponseData) {}
/**
* OnError will be called when a non-path-specific error occurs *after* a successful call to SendCommandRequest().
*
* The CommandSender object MUST continue to exist after this call is completed. The application shall wait until it
* receives an OnDone call to destroy and free the object.
*
* NOTE: Path specific errors do NOT come to OnError, but instead go to OnResponse.
*
* @param[in] apCommandSender The command sender object that initiated the command transaction.
* @param[in] aErrorData A error data regarding error that occurred.
*/
virtual void OnError(const CommandSender * apCommandSender, const ErrorData & aErrorData) {}
/**
* OnDone will be called when CommandSender has finished all work and is safe to destroy and free the
* allocated CommandSender object.
*
* This function will:
* - Always be called exactly *once* for a given CommandSender instance.
* - Be called even in error circumstances.
* - Only be called after a successful call to SendCommandRequest returns, if SendCommandRequest is used.
* - Always be called before a successful return from SendGroupCommandRequest, if SendGroupCommandRequest is used.
*
* This function must be implemented to destroy the CommandSender object.
*
* @param[in] apCommandSender The command sender object of the terminated invoke command transaction.
*/
virtual void OnDone(CommandSender * apCommandSender) = 0;
};
// `Callback` exists for legacy purposes. If you are developing a new callback implementation,
// please use `ExtendableCallback`.
using Callback = CommandSenderLegacyCallback;
// SetCommandSenderConfig is a public SDK API, so we cannot break source compatibility
// for it. By having parameters to that API use this struct instead of individual
// function arguments, we centralize required changes to one file when adding new
// funtionality.
struct ConfigParameters
{
ConfigParameters & SetRemoteMaxPathsPerInvoke(uint16_t aRemoteMaxPathsPerInvoke)
{
remoteMaxPathsPerInvoke = aRemoteMaxPathsPerInvoke;
return *this;
}
// If remoteMaxPathsPerInvoke is 1, this will allow the CommandSender client to contain only one command and
// doesn't enforce other batch commands requirements.
uint16_t remoteMaxPathsPerInvoke = 1;
};
// AddRequestData is a public SDK API, so we must maintain source compatibility.
// Using this struct for API parameters instead of individual parameters allows us
// to make necessary changes for new functionality in a single location.
struct AddRequestDataParameters
{
// gcc bug requires us to have the constructor below
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96645
AddRequestDataParameters() {}
AddRequestDataParameters(const Optional<uint16_t> & aTimedInvokeTimeoutMs) : timedInvokeTimeoutMs(aTimedInvokeTimeoutMs) {}
// When a value is provided for timedInvokeTimeoutMs, this invoke becomes a timed
// invoke. CommandSender will use the minimum of all provided timeouts for execution.
const Optional<uint16_t> timedInvokeTimeoutMs;
// The command reference is required when sending multiple commands. It allows the caller
// to associate this request with its corresponding response.
Optional<uint16_t> commandRef;
};
// PrepareCommand is a public SDK API, so we must maintain source compatibility.
// Using this struct for API parameters instead of individual parameters allows us
// to make necessary changes for new functionality in a single location.
struct PrepareCommandParameters
{
// gcc bug requires us to have the constructor below
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96645
PrepareCommandParameters() {}
PrepareCommandParameters(const AddRequestDataParameters & aAddRequestDataParam) :
commandRef(aAddRequestDataParam.commandRef)
{}
PrepareCommandParameters & SetStartDataStruct(bool aStartDataStruct)
{
startDataStruct = aStartDataStruct;
return *this;
}
PrepareCommandParameters & SetCommandRef(uint16_t aCommandRef)
{
commandRef.SetValue(aCommandRef);
return *this;
}
// The command reference is required when sending multiple commands. It allows the caller
// to associate this request with its corresponding response. We validate the reference
// early in PrepareCommand, even though it's not used until FinishCommand. This proactive
// validation helps prevent unnecessary writing an InvokeRequest into the packet that later
// needs to be undone.
//
// Currently, provided commandRefs for the first request must start at 0 and increment by one
// for each subsequent request. This requirement can be relaxed in the future if a compelling
// need arises.
// TODO(#30453): After introducing Request/Response tracking, remove statement above about
// this currently enforced requirement on commandRefs.
Optional<uint16_t> commandRef;
// If the InvokeRequest needs to be in a state with a started data TLV struct container
bool startDataStruct = false;
};
// FinishCommand is a public SDK API, so we must maintain source compatibility.
// Using this struct for API parameters instead of individual parameters allows us
// to make necessary changes for new functionality in a single location.
struct FinishCommandParameters
{
// gcc bug requires us to have the constructor below
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96645
FinishCommandParameters() {}
FinishCommandParameters(const Optional<uint16_t> & aTimedInvokeTimeoutMs) : timedInvokeTimeoutMs(aTimedInvokeTimeoutMs) {}
FinishCommandParameters(const AddRequestDataParameters & aAddRequestDataParam) :
timedInvokeTimeoutMs(aAddRequestDataParam.timedInvokeTimeoutMs), commandRef(aAddRequestDataParam.commandRef)
{}
FinishCommandParameters & SetEndDataStruct(bool aEndDataStruct)
{
endDataStruct = aEndDataStruct;
return *this;
}
FinishCommandParameters & SetCommandRef(uint16_t aCommandRef)
{
commandRef.SetValue(aCommandRef);
return *this;
}
// When a value is provided for timedInvokeTimeoutMs, this invoke becomes a timed
// invoke. CommandSender will use the minimum of all provided timeouts for execution.
const Optional<uint16_t> timedInvokeTimeoutMs;
// The command reference is required when sending multiple commands. It allows the caller
// to associate this request with its corresponding response. This value must be
// the same as the one provided in PrepareCommandParameters when calling PrepareCommand.
Optional<uint16_t> commandRef;
// If InvokeRequest is in a state where the data TLV struct container is currently open
// and FinishCommand should close it.
bool endDataStruct = false;
};
/*
* Constructor.
*
* The callback passed in has to outlive this CommandSender object.
* If used in a groups setting, callbacks do not need to be passed.
* If callbacks are passed the only one that will be called in a group sesttings is the onDone
*/
CommandSender(Callback * apCallback, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest = false,
bool aSuppressResponse = false);
CommandSender(ExtendableCallback * apCallback, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest = false,
bool aSuppressResponse = false);
CommandSender(std::nullptr_t, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest = false,
bool aSuppressResponse = false) :
CommandSender(static_cast<Callback *>(nullptr), apExchangeMgr, aIsTimedRequest, aSuppressResponse)
{}
~CommandSender();
/**
* Enables additional features of CommandSender, for example sending batch commands.
*
* In the case of enabling batch commands, once set it ensures that commands contain all
* required data elements while building the InvokeRequestMessage. This must be called
* before PrepareCommand.
*
* @param [in] aConfigParams contains information to configure CommandSender behavior,
* such as such as allowing a max number of paths per invoke greater than one,
* based on how many paths the remote peer claims to support.
*
* @return CHIP_ERROR_INCORRECT_STATE
* If device has previously called `PrepareCommand`.
* @return CHIP_ERROR_INVALID_ARGUMENT
* Invalid argument value.
* @return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE
* Device has not enabled CHIP_CONFIG_SENDING_BATCH_COMMANDS_ENABLED.
*/
CHIP_ERROR SetCommandSenderConfig(ConfigParameters & aConfigParams);
CHIP_ERROR PrepareCommand(const CommandPathParams & aCommandPathParams, PrepareCommandParameters & aPrepareCommandParams);
[[deprecated("PrepareCommand should migrate to calling PrepareCommand with PrepareCommandParameters")]] CHIP_ERROR
PrepareCommand(const CommandPathParams & aCommandPathParams, bool aStartDataStruct = true)
{
PrepareCommandParameters prepareCommandParams;
prepareCommandParams.SetStartDataStruct(aStartDataStruct);
return PrepareCommand(aCommandPathParams, prepareCommandParams);
}
CHIP_ERROR FinishCommand(FinishCommandParameters & aFinishCommandParams);
[[deprecated("FinishCommand should migrate to calling FinishCommand with FinishCommandParameters")]] CHIP_ERROR
FinishCommand(bool aEndDataStruct = true)
{
FinishCommandParameters finishCommandParams;
finishCommandParams.SetEndDataStruct(aEndDataStruct);
return FinishCommand(finishCommandParams);
}
[[deprecated("FinishCommand should migrate to calling FinishCommand with FinishCommandParameters")]] CHIP_ERROR
FinishCommand(const Optional<uint16_t> & aTimedInvokeTimeoutMs)
{
FinishCommandParameters finishCommandParams(aTimedInvokeTimeoutMs);
return FinishCommand(finishCommandParams);
}
TLV::TLVWriter * GetCommandDataIBTLVWriter();
/**
* API for adding a data request. The template parameter T is generally
* expected to be a ClusterName::Commands::CommandName::Type struct, but any
* object that can be encoded using the DataModel::Encode machinery and
* exposes the right command id will work.
*
* @param [in] aCommandPath The path of the command being requested.
* @param [in] aData The data for the request.
*/
template <typename CommandDataT, typename std::enable_if_t<!CommandDataT::MustUseTimedInvoke(), int> = 0>
CHIP_ERROR AddRequestData(const CommandPathParams & aCommandPath, const CommandDataT & aData)
{
AddRequestDataParameters addRequestDataParams;
return AddRequestData(aCommandPath, aData, addRequestDataParams);
}
template <typename CommandDataT>
CHIP_ERROR AddRequestData(const CommandPathParams & aCommandPath, const CommandDataT & aData,
AddRequestDataParameters & aAddRequestDataParams)
{
VerifyOrReturnError(!CommandDataT::MustUseTimedInvoke() || aAddRequestDataParams.timedInvokeTimeoutMs.HasValue(),
CHIP_ERROR_INVALID_ARGUMENT);
return AddRequestDataInternal(aCommandPath, aData, aAddRequestDataParams);
}
template <typename CommandDataT>
CHIP_ERROR AddRequestData(const CommandPathParams & aCommandPath, const CommandDataT & aData,
const Optional<uint16_t> & aTimedInvokeTimeoutMs)
{
AddRequestDataParameters addRequestDataParams(aTimedInvokeTimeoutMs);
return AddRequestData(aCommandPath, aData, addRequestDataParams);
}
/**
* @brief Returns the number of InvokeResponseMessages received.
*
* Responses to multiple requests might be split across several InvokeResponseMessages.
* This function helps track the total count. Primarily for test validation purposes.
*/
size_t GetInvokeResponseMessageCount();
#if CONFIG_BUILD_FOR_HOST_UNIT_TEST
/**
* Version of AddRequestData that allows sending a message that is
* guaranteed to fail due to requiring a timed invoke but not providing a
* timeout parameter. For use in tests only.
*/
template <typename CommandDataT>
CHIP_ERROR TestOnlyAddRequestDataNoTimedCheck(const CommandPathParams & aCommandPath, const CommandDataT & aData,
AddRequestDataParameters & aAddRequestDataParams)
{
return AddRequestDataInternal(aCommandPath, aData, aAddRequestDataParams);
}
CHIP_ERROR TestOnlyFinishCommand(FinishCommandParameters & aFinishCommandParams)
{
if (mBatchCommandsEnabled)
{
VerifyOrReturnError(aFinishCommandParams.commandRef.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
}
return FinishCommandInternal(aFinishCommandParams);
}
/**
* Version of SendCommandRequest that sets the TimedRequest flag but does not send the TimedInvoke
* action. For use in tests only.
*/
CHIP_ERROR TestOnlyCommandSenderTimedRequestFlagWithNoTimedInvoke(const SessionHandle & session,
Optional<System::Clock::Timeout> timeout = NullOptional);
#endif // CONFIG_BUILD_FOR_HOST_UNIT_TEST
private:
template <typename CommandDataT>
CHIP_ERROR AddRequestDataInternal(const CommandPathParams & aCommandPath, const CommandDataT & aData,
AddRequestDataParameters & aAddRequestDataParams)
{
PrepareCommandParameters prepareCommandParams(aAddRequestDataParams);
ReturnErrorOnFailure(PrepareCommand(aCommandPath, prepareCommandParams));
TLV::TLVWriter * writer = GetCommandDataIBTLVWriter();
VerifyOrReturnError(writer != nullptr, CHIP_ERROR_INCORRECT_STATE);
ReturnErrorOnFailure(DataModel::Encode(*writer, TLV::ContextTag(CommandDataIB::Tag::kFields), aData));
FinishCommandParameters finishCommandParams(aAddRequestDataParams);
return FinishCommand(finishCommandParams);
}
CHIP_ERROR FinishCommandInternal(FinishCommandParameters & aFinishCommandParams);
public:
// Sends a queued up command request to the target encapsulated by the secureSession handle.
//
// Upon successful return from this call, all subsequent errors that occur during this interaction
// will be conveyed through the OnError callback above. In addition, upon completion of work regardless of
// whether it was successful or not, the OnDone callback will be invoked to indicate completion of work on this
// object and to indicate to the application that it can destroy and free this object.
//
// Applications can, however, destroy this object at any time after this call, except while handling
// an OnResponse or OnError callback, and it will safely clean-up.
//
// If this call returns failure, the callback's OnDone will never be called; the client is responsible
// for destroying this object on failure.
//
// Client can specify the maximum time to wait for response (in milliseconds) via timeout parameter.
// Default timeout value will be used otherwise.
//
CHIP_ERROR SendCommandRequest(const SessionHandle & session, Optional<System::Clock::Timeout> timeout = NullOptional);
// Sends a queued up group command request to the target encapsulated by the secureSession handle.
//
// If this function is successful, it will invoke the OnDone callback before returning to indicate
// to the application that it can destroy and free this object.
//
CHIP_ERROR SendGroupCommandRequest(const SessionHandle & session);
private:
friend class TestCommandInteraction;
enum class State : uint8_t
{
Idle, ///< Default state that the object starts out in, where no work has commenced
AddingCommand, ///< In the process of adding a command.
AddedCommand, ///< A command has been completely encoded and is awaiting transmission.
AwaitingTimedStatus, ///< Sent a Timed Request and waiting for response.
AwaitingResponse, ///< The command has been sent successfully, and we are awaiting invoke response.
ResponseReceived, ///< Received a response to our invoke and request and processing the response.
AwaitingDestruction, ///< The object has completed its work and is awaiting destruction by the application.
};
union CallbackHandle
{
CallbackHandle(Callback * apCallback) : legacyCallback(apCallback) {}
CallbackHandle(ExtendableCallback * apExtendableCallback) : extendableCallback(apExtendableCallback) {}
Callback * legacyCallback;
ExtendableCallback * extendableCallback;
};
void MoveToState(const State aTargetState);
const char * GetStateStr() const;
/*
* Allocates a packet buffer used for encoding an invoke request payload.
*
* This can be called multiple times safely, as it will only allocate the buffer once for the lifetime
* of this object.
*/
CHIP_ERROR AllocateBuffer();
// ExchangeDelegate interface implementation. Private so people won't
// accidentally call it on us when we're not being treated as an actual
// ExchangeDelegate.
CHIP_ERROR OnMessageReceived(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader,
System::PacketBufferHandle && aPayload) override;
void OnResponseTimeout(Messaging::ExchangeContext * apExchangeContext) override;
//
// Called internally to signal the completion of all work on this object, gracefully close the
// exchange (by calling into the base class) and finally, signal to the application that it's
// safe to release this object.
//
void Close();
/*
* This forcibly closes the exchange context if a valid one is pointed to. Such a situation does
* not arise during normal message processing flows that all normally call Close() above. This can only
* arise due to application-initiated destruction of the object when this object is handling receiving/sending
* message payloads.
*/
void Abort();
CHIP_ERROR ProcessInvokeResponse(System::PacketBufferHandle && payload, bool & moreChunkedMessages);
CHIP_ERROR ProcessInvokeResponseIB(InvokeResponseIB::Parser & aInvokeResponse);
void SetTimedInvokeTimeoutMs(const Optional<uint16_t> & aTimedInvokeTimeoutMs);
// Send our queued-up Invoke Request message. Assumes the exchange is ready
// and mPendingInvokeData is populated.
CHIP_ERROR SendInvokeRequest();
CHIP_ERROR Finalize(System::PacketBufferHandle & commandPacket);
CHIP_ERROR SendCommandRequestInternal(const SessionHandle & session, Optional<System::Clock::Timeout> timeout);
void OnResponseCallback(const ResponseData & aResponseData)
{
// mpExtendableCallback and mpCallback are mutually exclusive.
if (mUseExtendableCallback && mCallbackHandle.extendableCallback)
{
mCallbackHandle.extendableCallback->OnResponse(this, aResponseData);
}
else if (mCallbackHandle.legacyCallback)
{
mCallbackHandle.legacyCallback->OnResponse(this, aResponseData.path, aResponseData.statusIB, aResponseData.data);
}
}
void OnErrorCallback(CHIP_ERROR aError)
{
// mpExtendableCallback and mpCallback are mutually exclusive.
if (mUseExtendableCallback && mCallbackHandle.extendableCallback)
{
ErrorData errorData = { aError };
mCallbackHandle.extendableCallback->OnError(this, errorData);
}
else if (mCallbackHandle.legacyCallback)
{
mCallbackHandle.legacyCallback->OnError(this, aError);
}
}
void OnDoneCallback()
{
// mpExtendableCallback and mpCallback are mutually exclusive.
if (mUseExtendableCallback && mCallbackHandle.extendableCallback)
{
mCallbackHandle.extendableCallback->OnDone(this);
}
else if (mCallbackHandle.legacyCallback)
{
mCallbackHandle.legacyCallback->OnDone(this);
}
}
Messaging::ExchangeHolder mExchangeCtx;
CallbackHandle mCallbackHandle;
Messaging::ExchangeManager * mpExchangeMgr = nullptr;
InvokeRequestMessage::Builder mInvokeRequestBuilder;
// TODO Maybe we should change PacketBufferTLVWriter so we can finalize it
// but have it hold on to the buffer, and get the buffer from it later.
// Then we could avoid this extra pointer-sized member.
System::PacketBufferHandle mPendingInvokeData;
// If mTimedInvokeTimeoutMs has a value, we are expected to do a timed
// invoke.
Optional<uint16_t> mTimedInvokeTimeoutMs;
TLV::TLVType mDataElementContainerType = TLV::kTLVType_NotSpecified;
chip::System::PacketBufferTLVWriter mCommandMessageWriter;
uint16_t mInvokeResponseMessageCount = 0;
uint16_t mFinishedCommandCount = 0;
uint16_t mRemoteMaxPathsPerInvoke = 1;
State mState = State::Idle;
bool mSuppressResponse = false;
bool mTimedRequest = false;
bool mBufferAllocated = false;
bool mBatchCommandsEnabled = false;
bool mUseExtendableCallback = false;
};
} // namespace app
} // namespace chip