| /* |
| * |
| * Copyright (c) 2021 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 |
| |
| #include <app/AttributePathParams.h> |
| #include <app/ConcreteAttributePath.h> |
| #include <app/InteractionModelTimeout.h> |
| #include <app/MessageDef/AttributeDataIBs.h> |
| #include <app/MessageDef/AttributeStatusIB.h> |
| #include <app/MessageDef/StatusIB.h> |
| #include <app/MessageDef/WriteRequestMessage.h> |
| #include <app/data-model/Encode.h> |
| #include <app/data-model/FabricScoped.h> |
| #include <app/data-model/List.h> |
| #include <lib/core/CHIPCore.h> |
| #include <lib/core/TLVDebug.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> |
| |
| namespace chip { |
| namespace app { |
| |
| class InteractionModelEngine; |
| |
| /** |
| * @brief The read client represents the initiator side of a Write Interaction, and is responsible |
| * for generating one Write Request for a particular set of attributes, and handling the Write response. |
| * Consumer can allocate one write client, then call PrepareAttribute, insert attribute value, followed by FinishAttribute for |
| * every attribute it wants to insert in write request, then call SendWriteRequest |
| * |
| * Note: When writing lists, you may receive multiple write status responses for a single list. |
| * Please see ChunkedWriteCallback.h for a high level API which will merge status codes for |
| * chunked write requests. |
| * |
| */ |
| class WriteClient : public Messaging::ExchangeDelegate |
| { |
| public: |
| class Callback |
| { |
| public: |
| virtual ~Callback() = default; |
| |
| /** |
| * OnResponse will be called when a write response has been received |
| * and processed for the given path. |
| * |
| * The WriteClient object MUST continue to exist after this call is completed. The application shall wait until it |
| * receives an OnDone call before it shuts down the object. |
| * |
| * @param[in] apWriteClient The write client object that initiated the write transaction. |
| * @param[in] aPath The attribute path field in write response. |
| * @param[in] attributeStatus Attribute-specific status, containing an InteractionModel::Status code as well as |
| * an optional cluster-specific status code. |
| */ |
| virtual void OnResponse(const WriteClient * apWriteClient, const ConcreteDataAttributePath & aPath, |
| StatusIB attributeStatus) |
| {} |
| |
| /** |
| * OnError will be called when an error occurs *after* a successful call to SendWriteRequest(). The following |
| * errors will be delivered through this call in the aError field: |
| * |
| * - 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. |
| * |
| * The WriteClient object MUST continue to exist after this call is completed. The application shall wait until it |
| * receives an OnDone call before it shuts down the object. |
| * |
| * @param[in] apWriteClient The write client object that initiated the attribute write transaction. |
| * @param[in] aError A system error code that conveys the overall error code. |
| */ |
| virtual void OnError(const WriteClient * apWriteClient, CHIP_ERROR aError) {} |
| |
| /** |
| * OnDone will be called when WriteClient has finished all work and is reserved for future WriteClient ownership change. |
| * (#10366) Users may use this function to release their own objects related to this write interaction. |
| * |
| * This function will: |
| * - Always be called exactly *once* for a given WriteClient instance. |
| * - Be called even in error circumstances. |
| * - Only be called after a successful call to SendWriteRequest has been made. |
| * |
| * @param[in] apWriteClient The write client object of the terminated write transaction. |
| */ |
| virtual void OnDone(WriteClient * apWriteClient) = 0; |
| }; |
| |
| /** |
| * Construct the client object. Within the lifetime |
| * of this instance. |
| * |
| * @param[in] apExchangeMgr A pointer to the ExchangeManager object. |
| * @param[in] apCallback Callback set by application. |
| * @param[in] aTimedWriteTimeoutMs If provided, do a timed write using this timeout. |
| * @param[in] aSuppressResponse If provided, set SuppressResponse field to the provided value |
| */ |
| WriteClient(Messaging::ExchangeManager * apExchangeMgr, Callback * apCallback, const Optional<uint16_t> & aTimedWriteTimeoutMs, |
| bool aSuppressResponse = false) : |
| mpExchangeMgr(apExchangeMgr), |
| mExchangeCtx(*this), mpCallback(apCallback), mTimedWriteTimeoutMs(aTimedWriteTimeoutMs), |
| mSuppressResponse(aSuppressResponse) |
| {} |
| |
| #if CONFIG_BUILD_FOR_HOST_UNIT_TEST |
| WriteClient(Messaging::ExchangeManager * apExchangeMgr, Callback * apCallback, const Optional<uint16_t> & aTimedWriteTimeoutMs, |
| uint16_t aReservedSize) : |
| mpExchangeMgr(apExchangeMgr), |
| mExchangeCtx(*this), mpCallback(apCallback), mTimedWriteTimeoutMs(aTimedWriteTimeoutMs), mReservedSize(aReservedSize) |
| {} |
| #endif |
| |
| /** |
| * Encode an attribute value that can be directly encoded using DataModel::Encode. Will create a new chunk when necessary. |
| */ |
| template <class T> |
| CHIP_ERROR EncodeAttribute(const AttributePathParams & attributePath, const T & value, |
| const Optional<DataVersion> & aDataVersion = NullOptional) |
| { |
| ReturnErrorOnFailure(EnsureMessage()); |
| |
| // Here, we are using kInvalidEndpointId for missing endpoint id, which is used when sending group write requests. |
| return EncodeSingleAttributeDataIB( |
| ConcreteDataAttributePath(attributePath.HasWildcardEndpointId() ? kInvalidEndpointId : attributePath.mEndpointId, |
| attributePath.mClusterId, attributePath.mAttributeId, aDataVersion), |
| value); |
| } |
| |
| /** |
| * Encode a possibly-chunked list attribute value. Will create a new chunk when necessary. |
| */ |
| template <class T> |
| CHIP_ERROR EncodeAttribute(const AttributePathParams & attributePath, const DataModel::List<T> & value, |
| const Optional<DataVersion> & aDataVersion = NullOptional) |
| { |
| // Here, we are using kInvalidEndpointId for missing endpoint id, which is used when sending group write requests. |
| ConcreteDataAttributePath path = |
| ConcreteDataAttributePath(attributePath.HasWildcardEndpointId() ? kInvalidEndpointId : attributePath.mEndpointId, |
| attributePath.mClusterId, attributePath.mAttributeId, aDataVersion); |
| |
| ReturnErrorOnFailure(EnsureMessage()); |
| |
| // Encode an empty list for the chunking protocol. |
| ReturnErrorOnFailure(EncodeSingleAttributeDataIB(path, DataModel::List<uint8_t>())); |
| |
| path.mListOp = ConcreteDataAttributePath::ListOperation::AppendItem; |
| for (ListIndex i = 0; i < value.size(); i++) |
| { |
| ReturnErrorOnFailure(EncodeSingleAttributeDataIB(path, value.data()[i])); |
| } |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| /** |
| * Encode a Nullable attribute value. This needs a separate overload so it can dispatch to the right |
| * EncodeAttribute when writing a nullable list. |
| */ |
| template <class T> |
| CHIP_ERROR EncodeAttribute(const AttributePathParams & attributePath, const DataModel::Nullable<T> & value, |
| const Optional<DataVersion> & aDataVersion = NullOptional) |
| { |
| ReturnErrorOnFailure(EnsureMessage()); |
| |
| if (value.IsNull()) |
| { |
| // Here, we are using kInvalidEndpointId to for missing endpoint id, which is used when sending group write requests. |
| return EncodeSingleAttributeDataIB( |
| ConcreteDataAttributePath(attributePath.HasWildcardEndpointId() ? kInvalidEndpointId : attributePath.mEndpointId, |
| attributePath.mClusterId, attributePath.mAttributeId, aDataVersion), |
| value); |
| } |
| |
| return EncodeAttribute(attributePath, value.Value(), aDataVersion); |
| } |
| |
| /** |
| * Encode an attribute value which is already encoded into a TLV. The TLVReader is expected to be initialized and the read head |
| * is expected to point to the element to be encoded. |
| * |
| * Note: When encoding lists with this function, you may receive more than one write status for a single list. You can refer |
| * to ChunkedWriteCallback.h for a high level API which will merge status codes for chunked write requests. |
| */ |
| CHIP_ERROR PutPreencodedAttribute(const ConcreteDataAttributePath & attributePath, const TLV::TLVReader & data); |
| |
| /** |
| * Once SendWriteRequest returns successfully, the WriteClient will |
| * handle calling Shutdown on itself once it decides it's done with waiting |
| * for a response (i.e. times out or gets a response). Client can specify |
| * the maximum time to wait for response (in milliseconds) via timeout parameter. |
| * If the timeout is missing or is set to System::Clock::kZero, a value based on the MRP timeouts of the session will be used. |
| * If SendWriteRequest is never called, or the call fails, the API |
| * consumer is responsible for calling Shutdown on the WriteClient. |
| */ |
| CHIP_ERROR SendWriteRequest(const SessionHandle & session, System::Clock::Timeout timeout = System::Clock::kZero); |
| |
| /** |
| * Shutdown the WriteClient. This terminates this instance |
| * of the object and releases all held resources. |
| */ |
| void Shutdown(); |
| |
| private: |
| friend class TestWriteInteraction; |
| friend class InteractionModelEngine; |
| |
| enum class State |
| { |
| Initialized = 0, // The client has been initialized |
| AddAttribute, // The client has added attribute and ready for a SendWriteRequest |
| AwaitingTimedStatus, // Sent a Tiemd Request, waiting for response. |
| AwaitingResponse, // The client has sent out the write request message |
| ResponseReceived, // We have gotten a response after sending write request |
| AwaitingDestruction, // The object has completed its work and is awaiting destruction by the application. |
| }; |
| |
| CHIP_ERROR OnMessageReceived(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, |
| System::PacketBufferHandle && aPayload) override; |
| void OnResponseTimeout(Messaging::ExchangeContext * apExchangeContext) override; |
| |
| void MoveToState(const State aTargetState); |
| CHIP_ERROR ProcessWriteResponseMessage(System::PacketBufferHandle && payload); |
| CHIP_ERROR ProcessAttributeStatusIB(AttributeStatusIB::Parser & aAttributeStatusIB); |
| const char * GetStateStr() const; |
| |
| /** |
| * Encode an attribute value that can be directly encoded using DataModel::Encode. |
| */ |
| template <class T, std::enable_if_t<!DataModel::IsFabricScoped<T>::value, int> = 0> |
| CHIP_ERROR TryEncodeSingleAttributeDataIB(const ConcreteDataAttributePath & attributePath, const T & value) |
| { |
| chip::TLV::TLVWriter * writer = nullptr; |
| |
| ReturnErrorOnFailure(PrepareAttributeIB(attributePath)); |
| VerifyOrReturnError((writer = GetAttributeDataIBTLVWriter()) != nullptr, CHIP_ERROR_INCORRECT_STATE); |
| ReturnErrorOnFailure(DataModel::Encode(*writer, chip::TLV::ContextTag(chip::app::AttributeDataIB::Tag::kData), value)); |
| ReturnErrorOnFailure(FinishAttributeIB()); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| template <class T, std::enable_if_t<DataModel::IsFabricScoped<T>::value, int> = 0> |
| CHIP_ERROR TryEncodeSingleAttributeDataIB(const ConcreteDataAttributePath & attributePath, const T & value) |
| { |
| chip::TLV::TLVWriter * writer = nullptr; |
| |
| ReturnErrorOnFailure(PrepareAttributeIB(attributePath)); |
| VerifyOrReturnError((writer = GetAttributeDataIBTLVWriter()) != nullptr, CHIP_ERROR_INCORRECT_STATE); |
| ReturnErrorOnFailure( |
| DataModel::EncodeForWrite(*writer, chip::TLV::ContextTag(chip::app::AttributeDataIB::Tag::kData), value)); |
| ReturnErrorOnFailure(FinishAttributeIB()); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| /** |
| * A wrapper for TryEncodeSingleAttributeDataIB which will start a new chunk when failed with CHIP_ERROR_NO_MEMORY or |
| * CHIP_ERROR_BUFFER_TOO_SMALL. |
| */ |
| template <class T> |
| CHIP_ERROR EncodeSingleAttributeDataIB(const ConcreteDataAttributePath & attributePath, const T & value) |
| { |
| TLV::TLVWriter backupWriter; |
| |
| mWriteRequestBuilder.GetWriteRequests().Checkpoint(backupWriter); |
| |
| // First attempt to write this attribute. |
| CHIP_ERROR err = TryEncodeSingleAttributeDataIB(attributePath, value); |
| if (err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL) |
| { |
| // If it failed with no memory, then we create a new chunk for it. |
| mWriteRequestBuilder.GetWriteRequests().Rollback(backupWriter); |
| ReturnErrorOnFailure(StartNewMessage()); |
| ReturnErrorOnFailure(TryEncodeSingleAttributeDataIB(attributePath, value)); |
| } |
| else |
| { |
| ReturnErrorOnFailure(err); |
| } |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| /** |
| * Encode a preencoded attribute data, returns TLV encode error if the ramaining space of current chunk is too small for the |
| * AttributeDataIB. |
| */ |
| CHIP_ERROR TryPutSinglePreencodedAttributeWritePayload(const ConcreteDataAttributePath & attributePath, |
| const TLV::TLVReader & data); |
| |
| /** |
| * Encode a preencoded attribute data, will try to create a new chunk when necessary. |
| */ |
| CHIP_ERROR PutSinglePreencodedAttributeWritePayload(const ConcreteDataAttributePath & attributePath, |
| const TLV::TLVReader & data); |
| |
| CHIP_ERROR EnsureMessage(); |
| |
| /** |
| * 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(); |
| |
| // Send our queued-up Write Request message. Assumes the exchange is ready |
| // and mPendingWriteData is populated. |
| CHIP_ERROR SendWriteRequest(); |
| |
| // Encodes the header of an AttributeDataIB, a special case for attributePath is its EndpointId can be kInvalidEndpointId, this |
| // is used when sending group write requests. |
| // TODO(#14935) Update AttributePathParams to support more list operations. |
| CHIP_ERROR PrepareAttributeIB(const ConcreteDataAttributePath & attributePath); |
| CHIP_ERROR FinishAttributeIB(); |
| TLV::TLVWriter * GetAttributeDataIBTLVWriter(); |
| |
| /** |
| * Create a new message (or a new chunk) for the write request. |
| */ |
| CHIP_ERROR StartNewMessage(); |
| |
| /** |
| * Finalize Write Request Message TLV Builder and retrieve final data from tlv builder for later sending |
| */ |
| CHIP_ERROR FinalizeMessage(bool aHasMoreChunks); |
| |
| Messaging::ExchangeManager * mpExchangeMgr = nullptr; |
| Messaging::ExchangeHolder mExchangeCtx; |
| Callback * mpCallback = nullptr; |
| State mState = State::Initialized; |
| System::PacketBufferTLVWriter mMessageWriter; |
| WriteRequestMessage::Builder mWriteRequestBuilder; |
| // 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 mPendingWriteData; |
| // If mTimedWriteTimeoutMs has a value, we are expected to do a timed |
| // write. |
| Optional<uint16_t> mTimedWriteTimeoutMs; |
| bool mSuppressResponse = false; |
| |
| // A list of buffers, one buffer for each chunk. |
| System::PacketBufferHandle mChunks; |
| |
| // TODO: This file might be compiled with different build flags on Darwin platform (when building WriteClient.cpp and |
| // CHIPClustersObjc.mm), which will cause undefined behavior when building write requests. Uncomment the #if and #endif after |
| // resolving it. |
| // #if CONFIG_BUILD_FOR_HOST_UNIT_TEST |
| uint16_t mReservedSize = 0; |
| // #endif |
| |
| /** |
| * Below we define several const variables for encoding overheads. |
| * WriteRequestMessage = |
| * { |
| * timedRequest = false, |
| * AttributeDataIBs = |
| * [ |
| * AttributeDataIB = \ |
| * { | |
| * DataVersion = 0x0, | |
| * AttributePathIB = | |
| * { | |
| * Endpoint = 0x2, | "atomically" encoded via |
| * Cluster = 0x50f, > EncodeAttribute or |
| * Attribute = 0x0000_0006, | PutPreencodedAttribute |
| * ListIndex = Null, | |
| * } | |
| * Data = ... | |
| * }, / |
| * (...) |
| * ], <-- 1 byte "end of AttributeDataIB" (end of container) |
| * moreChunkedMessages = false, <-- 2 bytes "kReservedSizeForMoreChunksFlag" |
| * InteractionModelRevision = 1,<-- 3 bytes "kReservedSizeForIMRevision" |
| * } <-- 1 byte "end of WriteRequestMessage" (end of container) |
| */ |
| |
| // Reserved size for the MoreChunks boolean flag, which takes up 1 byte for the control tag and 1 byte for the context tag. |
| static constexpr uint16_t kReservedSizeForMoreChunksFlag = 1 + 1; |
| // End Of Container (0x18) uses one byte. |
| static constexpr uint16_t kReservedSizeForEndOfContainer = 1; |
| // Reserved size for the uint8_t InteractionModelRevision flag, which takes up 1 byte for the control tag and 1 byte for the |
| // context tag, 1 byte for value |
| static constexpr uint16_t kReservedSizeForIMRevision = 1 + 1 + 1; |
| // Reserved buffer for TLV level overhead (the overhead for end of AttributeDataIBs (end of container), more chunks flag, end |
| // of WriteRequestMessage (another end of container)). |
| static constexpr uint16_t kReservedSizeForTLVEncodingOverhead = kReservedSizeForIMRevision + kReservedSizeForMoreChunksFlag + |
| kReservedSizeForEndOfContainer + kReservedSizeForEndOfContainer; |
| bool mHasDataVersion = false; |
| }; |
| |
| } // namespace app |
| } // namespace chip |