/* * * 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 read handler for a CHIP Interaction Data model * */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/61a9d19e6af12fdfb0872bcff26d19de6c680a1a/src/Ch02_Architecture.adoc#1122-subscribe-interaction-limits inline constexpr uint16_t kSubscriptionMaxIntervalPublisherLimit = 3600; // seconds (60 minutes) namespace chip { namespace app { // // Forward declare the Engine (which is in a different namespace) to be able to use // it as a friend class below. // namespace reporting { class Engine; class TestReportingEngine; class ReportScheduler; class TestReportScheduler; } // namespace reporting class InteractionModelEngine; class TestInteractionModelEngine; /** * @class ReadHandler * * @brief The read handler is responsible for processing a read request, asking the attribute/event store * for the relevant data, and sending a reply. * */ class ReadHandler : public Messaging::ExchangeDelegate { public: using SubjectDescriptor = Access::SubjectDescriptor; enum class InteractionType : uint8_t { Read, Subscribe, }; /* * A callback used to interact with the application. */ class ApplicationCallback { public: virtual ~ApplicationCallback() = default; /* * Called right after a SubscribeRequest has been parsed and processed. This notifies an interested application * of a subscription that is about to be established. It also provides an avenue for altering the parameters of the * subscription (specifically, the min/max negotiated intervals) or even outright rejecting the subscription for * application-specific reasons. * * TODO: Need a new IM status code to convey application-rejected subscribes. Currently, a Failure IM status code is sent * back to the subscriber, which isn't sufficient. * * To reject the subscription, a CHIP_ERROR code that is not equivalent to CHIP_NO_ERROR should be returned. * * More information about the set of paths associated with this subscription can be retrieved by calling the appropriate * Get* methods below. * * aReadHandler: Reference to the ReadHandler associated with the subscription. * aSecureSession: A reference to the underlying secure session associated with the subscription. * */ virtual CHIP_ERROR OnSubscriptionRequested(ReadHandler & aReadHandler, Transport::SecureSession & aSecureSession) { return CHIP_NO_ERROR; } /* * Called after a subscription has been fully established. */ virtual void OnSubscriptionEstablished(ReadHandler & aReadHandler){}; /* * Called right before a subscription is about to get terminated. This is only called on subscriptions that were terminated * after they had been fully established (and therefore had called OnSubscriptionEstablished). * OnSubscriptionEstablishment(). */ virtual void OnSubscriptionTerminated(ReadHandler & aReadHandler){}; }; /* * A callback used to manage the lifetime of the ReadHandler object. */ class ManagementCallback { public: virtual ~ManagementCallback() = default; /* * Method that signals to a registered callback that this object * has completed doing useful work and is now safe for release/destruction. */ virtual void OnDone(ReadHandler & apReadHandlerObj) = 0; /* * Retrieve the ApplicationCallback (if a valid one exists) from our management entity. This avoids * storing multiple references to the application provided callback and having to subsequently manage lifetime * issues w.r.t the ReadHandler itself. */ virtual ApplicationCallback * GetAppCallback() = 0; /* * Retrieve the InteractionalModelEngine that holds this ReadHandler. */ virtual InteractionModelEngine * GetInteractionModelEngine() = 0; }; // TODO (#27675) : Merge existing callback and observer into one class and have an observer pool in the Readhandler to notify // every /* * Observer class for ReadHandler, meant to allow multiple objects to observe the ReadHandler. Currently only one observer is * supported but all above callbacks should be merged into observer type and an observer pool should be added to allow multiple * objects to observe ReadHandler */ class Observer { public: virtual ~Observer() = default; /// @brief Callback invoked to notify a subscription was successfully established for the ReadHandler /// @param[in] apReadHandler ReadHandler that completed its subscription virtual void OnSubscriptionEstablished(ReadHandler * apReadHandler) = 0; /// @brief Callback invoked when a ReadHandler went from a non reportable state to a reportable state. Indicates to the /// observer that a report should be emitted when the min interval allows it. /// /// This will only be invoked for subscribe-type ReadHandler objects, and only after /// OnSubscriptionEstablished has been called. /// /// @param[in] apReadHandler ReadHandler that became dirty and in HandlerState::CanStartReporting state virtual void OnBecameReportable(ReadHandler * apReadHandler) = 0; /// @brief Callback invoked when the read handler needs to make sure to send a message to the subscriber within the next /// maxInterval time period. /// @param[in] apReadHandler ReadHandler that has generated a report virtual void OnSubscriptionReportSent(ReadHandler * apReadHandler) = 0; /// @brief Callback invoked when a ReadHandler is getting removed so it can be unregistered /// @param[in] apReadHandler ReadHandler getting destroyed virtual void OnReadHandlerDestroyed(ReadHandler * apReadHandler) = 0; }; /* * Destructor - as part of destruction, it will abort the exchange context * if a valid one still exists. * * See Abort() for details on when that might occur. */ ~ReadHandler() override; /** * * Constructor. * * The callback passed in has to outlive this handler object. * */ ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeContext * apExchangeContext, InteractionType aInteractionType, Observer * observer, DataModel::Provider * apDataModel); #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS /** * * Constructor in preparation for resuming a persisted subscription * * The callback passed in has to outlive this handler object. * */ ReadHandler(ManagementCallback & apCallback, Observer * observer, DataModel::Provider * apDataModel); #endif const SingleLinkedListNode * GetAttributePathList() const { return mpAttributePathList; } const SingleLinkedListNode * GetEventPathList() const { return mpEventPathList; } const SingleLinkedListNode * GetDataVersionFilterList() const { return mpDataVersionFilterList; } /** * @brief Returns the reporting intervals that will used by the ReadHandler for the subscription being requested. * After the subscription is established, these will be the set reporting intervals and cannot be changed. * * @param[out] aMinInterval minimum time delta between two reports for the subscription * @param[in] aMaxInterval maximum time delta between two reports for the subscription */ void GetReportingIntervals(uint16_t & aMinInterval, uint16_t & aMaxInterval) const { aMinInterval = mMinIntervalFloorSeconds; aMaxInterval = mMaxInterval; } /** * @brief Returns the maximum reporting interval that was initially requested by the subscriber * This is the same value as the mMaxInterval member if the max interval is not changed by the publisher. * * @note If the device is an ICD, the MaxInterval of a subscription is automatically set to a multiple of the IdleModeDuration. * This function is the only way to get the requested max interval once the OnSubscriptionRequested application callback * is called. * * @return uint16_t subscriber requested maximum reporting interval */ inline uint16_t GetSubscriberRequestedMaxInterval() const { return mSubscriberRequestedMaxInterval; } CHIP_ERROR SetMinReportingIntervalForTests(uint16_t aMinInterval) { VerifyOrReturnError(IsIdle(), CHIP_ERROR_INCORRECT_STATE); VerifyOrReturnError(aMinInterval <= mMaxInterval, CHIP_ERROR_INVALID_ARGUMENT); // Ensures the new min interval is higher than the subscriber established one. mMinIntervalFloorSeconds = std::max(mMinIntervalFloorSeconds, aMinInterval); return CHIP_NO_ERROR; } /* * Set the maximum reporting interval for the subscription. This SHALL only be called * from the OnSubscriptionRequested callback above. The restriction is as below * MinIntervalFloor ≤ MaxInterval ≤ MAX(SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT, MaxIntervalCeiling) * Where SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT is set to 60m in the spec. */ CHIP_ERROR SetMaxReportingInterval(uint16_t aMaxInterval) { VerifyOrReturnError(IsIdle(), CHIP_ERROR_INCORRECT_STATE); VerifyOrReturnError(mMinIntervalFloorSeconds <= aMaxInterval, CHIP_ERROR_INVALID_ARGUMENT); VerifyOrReturnError(aMaxInterval <= std::max(GetPublisherSelectedIntervalLimit(), mSubscriberRequestedMaxInterval), CHIP_ERROR_INVALID_ARGUMENT); mMaxInterval = aMaxInterval; return CHIP_NO_ERROR; } #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS /** * * @brief Initialize a ReadHandler for a resumed subsciption * * Used after the SubscriptionResumptionSessionEstablisher establishs the CASE session */ void OnSubscriptionResumed(const SessionHandle & sessionHandle, SubscriptionResumptionSessionEstablisher & sessionEstablisher); #endif private: PriorityLevel GetCurrentPriority() const { return mCurrentPriority; } EventNumber & GetEventMin() { return mEventMin; } /** * Returns SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT * For an ICD publisher, this SHALL be set to the idle mode duration. * Otherwise, this SHALL be set to 60 minutes. */ uint16_t GetPublisherSelectedIntervalLimit(); enum class ReadHandlerFlags : uint8_t { // The flag indicating we are in the middle of a series of chunked report messages, this flag will be cleared during // sending last chunked message. ChunkedReport = (1 << 0), // Tracks whether we're in the initial phase of receiving priming // reports, which is always true for reads and true for subscriptions // prior to receiving a subscribe response. PrimingReports = (1 << 1), ActiveSubscription = (1 << 2), FabricFiltered = (1 << 3), // For subscriptions, we record the dirty set generation when we started to generate the last report. // The mCurrentReportsBeginGeneration records the generation at the start of the current report. This only/ // has a meaningful value while IsReporting() is true. // // mPreviousReportsBeginGeneration will be set to mCurrentReportsBeginGeneration after we send the last // chunk of the current report. Anything that was dirty with a generation earlier than // mPreviousReportsBeginGeneration has had its value sent to the client. // when receiving initial request, it needs mark current handler as dirty. // when there is urgent event, it needs mark current handler as dirty. ForceDirty = (1 << 4), // Don't need the response for report data if true SuppressResponse = (1 << 5), }; /** * Process a read/subscribe request. Parts of the processing may end up being asynchronous, but the ReadHandler * guarantees that it will call Shutdown on itself when processing is done (including if OnReadInitialRequest * returns an error). * * @retval #Others If fails to process read request * @retval #CHIP_NO_ERROR On success. * */ void OnInitialRequest(System::PacketBufferHandle && aPayload); /** * Send ReportData to initiator * * @param[in] aPayload A payload that has read request data * @param[in] aMoreChunks A flags indicating there will be more chunks expected to be sent for this read request * * @retval #Others If fails to send report data * @retval #CHIP_NO_ERROR On success. * * If an error is returned, the ReadHandler guarantees that it is not in * a state where it's waiting for a response. */ CHIP_ERROR SendReportData(System::PacketBufferHandle && aPayload, bool aMoreChunks); /* * Get the appropriate size of a packet buffer to allocate for encoding a Report message. * This size might depend on the underlying session used by the ReadHandler. * * The size returned here is the size not including the various prepended headers * (what System::PacketBuffer calls the "available size"). */ size_t GetReportBufferMaxSize(); /** * Returns whether this ReadHandler represents a subscription that was created by the other side of the provided exchange. */ bool IsFromSubscriber(Messaging::ExchangeContext & apExchangeContext) const; bool IsIdle() const { return mState == HandlerState::Idle; } /// @brief Returns whether the ReadHandler is in a state where it can send a report and there is data to report. bool ShouldStartReporting() const { // Important: Anything that changes ShouldStartReporting() from false to true // (which can only happen for subscriptions) must call // mObserver->OnBecameReportable(this). return CanStartReporting() && (ShouldReportUnscheduled() || IsDirty()); } /// @brief CanStartReporting() is true if the ReadHandler is in a state where it could generate /// a (possibly empty) report if someone asked it to. bool CanStartReporting() const { return mState == HandlerState::CanStartReporting; } /// @brief ShouldReportUnscheduled() is true if the ReadHandler should be asked to generate reports /// without consulting the report scheduler. bool ShouldReportUnscheduled() const { return CanStartReporting() && (IsType(ReadHandler::InteractionType::Read) || IsPriming()); } bool IsAwaitingReportResponse() const { return mState == HandlerState::AwaitingReportResponse; } // Resets the path iterator to the beginning of the whole report for generating a series of new reports. void ResetPathIterator(); CHIP_ERROR ProcessDataVersionFilterList(DataVersionFilterIBs::Parser & aDataVersionFilterListParser); // if current priority is in the middle, it has valid snapshoted last event number, it check cleaness via comparing // with snapshotted last event number. if current priority is in the end, no valid // sanpshotted last event, check with latest last event number, re-setup snapshoted checkpoint, and compare again. bool CheckEventClean(EventManagement & aEventManager); bool IsType(InteractionType type) const { return (mInteractionType == type); } bool IsChunkedReport() const { return mFlags.Has(ReadHandlerFlags::ChunkedReport); } // Is reporting indicates whether we are in the middle of a series chunks. As we will set mIsChunkedReport on the first chunk // and clear that flag on the last chunk, we can use mIsChunkedReport to indicate this state. bool IsReporting() const { return mFlags.Has(ReadHandlerFlags::ChunkedReport); } bool IsPriming() const { return mFlags.Has(ReadHandlerFlags::PrimingReports); } bool IsActiveSubscription() const { return mFlags.Has(ReadHandlerFlags::ActiveSubscription); } bool IsFabricFiltered() const { return mFlags.Has(ReadHandlerFlags::FabricFiltered); } CHIP_ERROR OnSubscribeRequest(Messaging::ExchangeContext * apExchangeContext, System::PacketBufferHandle && aPayload); void GetSubscriptionId(SubscriptionId & aSubscriptionId) const { aSubscriptionId = mSubscriptionId; } AttributePathExpandIterator * GetAttributePathExpandIterator() { return &mAttributePathExpandIterator; } /// @brief Notifies the read handler that a set of attribute paths has been marked dirty. This will schedule a reporting engine /// run if the change to the attribute path makes the ReadHandler reportable. /// @param aAttributeChanged Path to the attribute that was changed. void AttributePathIsDirty(const AttributePathParams & aAttributeChanged); bool IsDirty() const { return (mDirtyGeneration > mPreviousReportsBeginGeneration) || mFlags.Has(ReadHandlerFlags::ForceDirty); } void ClearForceDirtyFlag() { ClearStateFlag(ReadHandlerFlags::ForceDirty); } NodeId GetInitiatorNodeId() const { auto session = GetSession(); return session == nullptr ? kUndefinedNodeId : session->GetPeerNodeId(); } FabricIndex GetAccessingFabricIndex() const { auto session = GetSession(); return session == nullptr ? kUndefinedFabricIndex : session->GetFabricIndex(); } Transport::SecureSession * GetSession() const; SubjectDescriptor GetSubjectDescriptor() const { return GetSession()->GetSubjectDescriptor(); } auto GetTransactionStartGeneration() const { return mTransactionStartGeneration; } /// @brief Forces the read handler into a dirty state, regardless of what's going on with attributes. /// This can lead to scheduling of a reporting run immediately, if the min interval has been reached, /// or after the min interval is reached if it has not yet been reached. void ForceDirtyState(); const AttributeEncodeState & GetAttributeEncodeState() const { return mAttributeEncoderState; } void SetAttributeEncodeState(const AttributeEncodeState & aState) { mAttributeEncoderState = aState; } uint32_t GetLastWrittenEventsBytes() const { return mLastWrittenEventsBytes; } // Returns the number of interested paths, including wildcard and concrete paths. size_t GetAttributePathCount() const { return mpAttributePathList == nullptr ? 0 : mpAttributePathList->Count(); }; size_t GetEventPathCount() const { return mpEventPathList == nullptr ? 0 : mpEventPathList->Count(); }; size_t GetDataVersionFilterCount() const { return mpDataVersionFilterList == nullptr ? 0 : mpDataVersionFilterList->Count(); }; CHIP_ERROR SendStatusReport(Protocols::InteractionModel::Status aStatus); friend class TestReadInteraction; friend class chip::app::reporting::TestReportingEngine; friend class chip::app::reporting::TestReportScheduler; // // The engine needs to be able to Abort/Close a ReadHandler instance upon completion of work for a given read/subscribe // interaction. We do not want to make these methods public just to give an adjacent class in the IM access, since public // should really be taking application usage considerations as well. Hence, make it a friend. // friend class chip::app::reporting::Engine; friend class chip::app::InteractionModelEngine; friend class TestInteractionModelEngine; // The report scheduler needs to be able to access StateFlag private functions ShouldStartReporting(), CanStartReporting(), // ForceDirtyState() and IsDirty() to know when to schedule a run so it is declared as a friend class. friend class chip::app::reporting::ReportScheduler; enum class HandlerState : uint8_t { Idle, ///< The handler has been initialized and is ready CanStartReporting, ///< The handler has is now capable of generating reports and may generate one immediately ///< or later when other criteria are satisfied (e.g hold-off for min reporting interval). AwaitingReportResponse, ///< The handler has sent the report to the client and is awaiting a status response. AwaitingDestruction, ///< The object has completed its work and is awaiting destruction by the application. }; enum class CloseOptions { kDropPersistedSubscription, kKeepPersistedSubscription }; /** * Called internally to signal the completion of all work on this objecta and signal to a registered callback that it's * safe to release this object. * * @param options This specifies whether to drop or keep the subscription * */ void Close(CloseOptions options = CloseOptions::kDropPersistedSubscription); CHIP_ERROR SendSubscribeResponse(); CHIP_ERROR ProcessSubscribeRequest(System::PacketBufferHandle && aPayload); CHIP_ERROR ProcessReadRequest(System::PacketBufferHandle && aPayload); CHIP_ERROR ProcessAttributePaths(AttributePathIBs::Parser & aAttributePathListParser); CHIP_ERROR ProcessEventPaths(EventPathIBs::Parser & aEventPathsParser); CHIP_ERROR ProcessEventFilters(EventFilterIBs::Parser & aEventFiltersParser); CHIP_ERROR OnStatusResponse(Messaging::ExchangeContext * apExchangeContext, System::PacketBufferHandle && aPayload, bool & aSendStatusResponse); CHIP_ERROR OnMessageReceived(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload) override; void OnResponseTimeout(Messaging::ExchangeContext * apExchangeContext) override; void MoveToState(const HandlerState aTargetState); const char * GetStateStr() const; void PersistSubscription(); /// @brief Modifies a state flag in the read handler. If the read handler went from a /// non-reportable state to a reportable state, schedules a reporting engine run. /// @param aFlag Flag to set /// @param aValue Flag new value void SetStateFlag(ReadHandlerFlags aFlag, bool aValue = true); /// @brief This function call SetStateFlag with the flag value set to false, thus possibly emitting a report /// generation. /// @param aFlag Flag to clear void ClearStateFlag(ReadHandlerFlags aFlag); AttributePathExpandIterator mAttributePathExpandIterator; // The current generation of the reporting engine dirty set the last time we were notified that a path we're interested in was // marked dirty. // // This allows us to detemine whether any paths we care about might have // been marked dirty after we had already sent reports for them, which would // mean we should report those paths again, by comparing this generation to the // current generation when we started sending the last set reports that we completed. // // This allows us to reset the iterator to the beginning of the current // cluster instead of the beginning of the whole report in AttributePathIsDirty, without // permanently missing dirty any paths. uint64_t mDirtyGeneration = 0; // For subscriptions, we record the timestamp when we started to generate the last report. // The mCurrentReportsBeginGeneration records the timestamp for the current report, which won;t be used for checking if this // ReadHandler is dirty. // mPreviousReportsBeginGeneration will be set to mCurrentReportsBeginGeneration after we sent the last chunk of the current // report. uint64_t mPreviousReportsBeginGeneration = 0; uint64_t mCurrentReportsBeginGeneration = 0; /* * (mDirtyGeneration = b > a, this is a dirty read handler) * +- Start Report -> mCurrentReportsBeginGeneration = c * | +- AttributePathIsDirty (Attribute Y) -> mDirtyGeneration = d * | | +- Last Chunk -> mPreviousReportsBeginGeneration = mCurrentReportsBeginGeneration = c * | | | +- (mDirtyGeneration = d) > (mPreviousReportsBeginGeneration = c), this is a dirty read handler * | | | | Attribute X has a dirty generation less than c, Attribute Y has a dirty generation larger than c * | | | | So Y will be included in the report but X will not be inclued in this report. * -a--b--c------d-----e---f---> Generation * | | * | +- AttributePathIsDirty (Attribute X) (mDirtyGeneration = b) * +- mPreviousReportsBeginGeneration * For read handler, if mDirtyGeneration > mPreviousReportsBeginGeneration, then we regard it as a dirty read handler, and it * should generate report on timeout reached. */ // When we don't have enough resources for a new subscription, the oldest subscription might be evicted by interaction model // engine, the "oldest" subscription is the subscription with the smallest generation. uint64_t mTransactionStartGeneration = 0; SubscriptionId mSubscriptionId = 0; uint16_t mMinIntervalFloorSeconds = 0; uint16_t mMaxInterval = 0; uint16_t mSubscriberRequestedMaxInterval = 0; EventNumber mEventMin = 0; // The last schedule event number snapshoted in the beginning when preparing to fill new events to reports EventNumber mLastScheduledEventNumber = 0; // TODO: We should shutdown the transaction when the session expires. SessionHolder mSessionHandle; Messaging::ExchangeHolder mExchangeCtx; #if CHIP_CONFIG_UNSAFE_SUBSCRIPTION_EXCHANGE_MANAGER_USE // TODO: this should be replaced by a pointer to the InteractionModelEngine that created the ReadHandler // once InteractionModelEngine is no longer a singleton (see issue 23625) Messaging::ExchangeManager * mExchangeMgr = nullptr; #endif // CHIP_CONFIG_UNSAFE_SUBSCRIPTION_EXCHANGE_MANAGER_USE SingleLinkedListNode * mpAttributePathList = nullptr; SingleLinkedListNode * mpEventPathList = nullptr; SingleLinkedListNode * mpDataVersionFilterList = nullptr; ManagementCallback & mManagementCallback; uint32_t mLastWrittenEventsBytes = 0; // The detailed encoding state for a single attribute, used by list chunking feature. // The size of AttributeEncoderState is 2 bytes for now. AttributeEncodeState mAttributeEncoderState; // Current Handler state HandlerState mState = HandlerState::Idle; PriorityLevel mCurrentPriority = PriorityLevel::Invalid; BitFlags mFlags; InteractionType mInteractionType = InteractionType::Read; // TODO (#27675): Merge all observers into one and that one will dispatch the callbacks to the right place. Observer * mObserver = nullptr; }; } // namespace app } // namespace chip