diff --git a/src/BUILD.gn b/src/BUILD.gn index 60ba11c0d28f61..731890affa0120 100644 --- a/src/BUILD.gn +++ b/src/BUILD.gn @@ -55,6 +55,7 @@ if (chip_build_tests) { chip_test_group("tests") { deps = [] tests = [ + "${chip_root}/src/app/interaction-model/tests", "${chip_root}/src/access/tests", "${chip_root}/src/crypto/tests", "${chip_root}/src/inet/tests", diff --git a/src/app/interaction-model/Actions.h b/src/app/interaction-model/Actions.h new file mode 100644 index 00000000000000..62021fbf7ccd32 --- /dev/null +++ b/src/app/interaction-model/Actions.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 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 +#include +#include + +namespace chip { +namespace app { +namespace InteractionModel { + +/// Data provided to data models in order to interface with the interaction model environment. +struct InteractionModelActions +{ + Events * events; + Paths * paths; + RequestContext * requestContext; +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/BUILD.gn b/src/app/interaction-model/BUILD.gn new file mode 100644 index 00000000000000..c91c2aedac633e --- /dev/null +++ b/src/app/interaction-model/BUILD.gn @@ -0,0 +1,41 @@ +# Copyright (c) 2024 Project CHIP Authors +# +# 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. +import("//build_overrides/chip.gni") + +source_set("interaction-model") { + sources = [ + "Actions.h", + "Events.h", + "InvokeResponder.h", + "IterationTypes.h", + "Model.h", + "OperationTypes.h", + "Paths.h", + "RequestContext.h", + ] + + public_deps = [ + "${chip_root}/src/access:types", + "${chip_root}/src/app:attribute-access", + "${chip_root}/src/app:events", + "${chip_root}/src/app:paths", + "${chip_root}/src/app/MessageDef", + "${chip_root}/src/app/data-model", + "${chip_root}/src/lib/core", + "${chip_root}/src/lib/core:error", + "${chip_root}/src/lib/core:types", + "${chip_root}/src/lib/support", + "${chip_root}/src/messaging", + ] +} diff --git a/src/app/interaction-model/Events.h b/src/app/interaction-model/Events.h new file mode 100644 index 00000000000000..255a55e97ee465 --- /dev/null +++ b/src/app/interaction-model/Events.h @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2024 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 +#include +#include +#include +#include +#include +#include + +#include + +namespace chip { +namespace app { +namespace InteractionModel { + +namespace internal { +template +class SimpleEventLoggingDelegate : public EventLoggingDelegate +{ +public: + SimpleEventLoggingDelegate(const T & aEventData) : mEventData(aEventData){}; + CHIP_ERROR WriteEvent(chip::TLV::TLVWriter & aWriter) final override + { + return DataModel::Encode(aWriter, TLV::ContextTag(EventDataIB::Tag::kData), mEventData); + } + +private: + const T & mEventData; +}; + +template ::value, bool> = true> +EventNumber GenerateEvent(E & emittor, const T & aEventData, EndpointId aEndpoint) +{ + internal::SimpleEventLoggingDelegate eventData(aEventData); + ConcreteEventPath path(aEndpoint, aEventData.GetClusterId(), aEventData.GetEventId()); + EventOptions eventOptions; + eventOptions.mPath = path; + eventOptions.mPriority = aEventData.GetPriorityLevel(); + eventOptions.mFabricIndex = aEventData.GetFabricIndex(); + + // this skips logging the event if it's fabric-scoped but no fabric association exists yet. + + if (eventOptions.mFabricIndex == kUndefinedFabricIndex) + { + ChipLogError(EventLogging, "Event encode failure: no fabric index for fabric scoped event"); + return kInvalidEventId; + } + + // + // Unlike attributes which have a different 'EncodeForRead' for fabric-scoped structs, + // fabric-sensitive events don't require that since the actual omission of the event in its entirety + // happens within the event management framework itself at the time of access. + // + // The 'mFabricIndex' field in the event options above is encoded out-of-band alongside the event payload + // and used to match against the accessing fabric. + // + EventNumber eventNumber; + CHIP_ERROR err = emittor.GenerateEvent(&eventData, eventOptions, eventNumber); + if (err != CHIP_NO_ERROR) + { + ChipLogError(EventLogging, "Failed to log event: %" CHIP_ERROR_FORMAT, err.Format()); + return kInvalidEventId; + } + + return eventNumber; +} + +template ::value, bool> = true> +EventNumber GenerateEvent(E & emittor, const T & aEventData, EndpointId endpointId) +{ + internal::SimpleEventLoggingDelegate eventData(aEventData); + ConcreteEventPath path(endpointId, aEventData.GetClusterId(), aEventData.GetEventId()); + EventOptions eventOptions; + eventOptions.mPath = path; + eventOptions.mPriority = aEventData.GetPriorityLevel(); + EventNumber eventNumber; + CHIP_ERROR err = emittor.GenerateEvent(&eventData, eventOptions, eventNumber); + if (err != CHIP_NO_ERROR) + { + ChipLogError(EventLogging, "Failed to log event: %" CHIP_ERROR_FORMAT, err.Format()); + return kInvalidEventId; + } + + return eventNumber; +} + +} // namespace internal + +class Events +{ +public: + virtual ~Events() = default; + + /// Generates the given event. + /// + /// Events are generally expected to be sent to subscribed clients and also + /// be available for read later until they get overwritten by new events + /// that are being generated. + virtual CHIP_ERROR GenerateEvent(EventLoggingDelegate * eventContentWriter, const EventOptions & options, + EventNumber & generatedEventNumber) = 0; + + // Convenience methods for event logging using cluster-object structures + // On error, these log and return kInvalidEventId + template + EventNumber GenerateEvent(const T & eventData, EndpointId endpointId) + { + return internal::GenerateEvent(*this, eventData, endpointId); + } +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/InvokeResponder.h b/src/app/interaction-model/InvokeResponder.h new file mode 100644 index 00000000000000..0a399b24564e96 --- /dev/null +++ b/src/app/interaction-model/InvokeResponder.h @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2024 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 +#include +#include + +namespace chip { +namespace app { +namespace InteractionModel { + +/// Handles encoding of an invoke response for a specific invoke request. +/// +/// This class handles a single response (i.e. a CommandDataIB within the +/// matter protocol) and is responsible for constructing its corresponding +/// response (i.e. a InvokeResponseIB within the matter protocol) +/// +/// Invoke responses MUST contain exactly ONE of: +/// - response data (accessed via `ResponseEncoder`) +/// - A status, which may be success or failure, both of which may +/// contain a cluster-specific error code. +/// +/// To encode a response, `Complete` MUST be called. +/// +/// `Complete` requirements +/// - Complete with InteractionModel::Status::Success will respond with data +/// some response data was written. +/// - Any other case (including success with cluster specific codes) implies +/// no response data and a status will be encoded instead +/// - this includes the case when some response data was written already. +/// In that case, the response data will be rolled back and only the status +/// will be encoded. +/// +/// Creating a response MAY be retried at most once, if and only if `Complete` +/// returns CHIP_ERROR_BUFFER_TOO_SMALL. Retry attempts MUST not exceed 1: +/// - FlushPendingResponses MUST be called to make as much buffer space as possible +/// available for encoding +/// - The response encoding (including `ResponseEncoder` usage and calling Complete) +/// MUST be retried once more. If the final Complete returns an error, the result +/// of the invoke will be an error status. +/// +class InvokeResponder +{ +public: + virtual ~InvokeResponder() = default; + + // Copying not allowed since underlying requirement is that on deletion of this + // object, a reply will be sent. + InvokeResponder(const InvokeResponder &) = delete; + InvokeResponder & operator=(const InvokeResponder &) = delete; + + /// Flush any pending replies before encoding the current reply. + /// + /// MAY be called at most once. + /// + /// This function is intended to provided the ability to retry sending a reply + /// if a reply encoding fails due to insufficient buffer. + /// + /// Call this if `Complete(...)` returns CHIP_ERROR_BUFFER_TOO_SMALL and try + /// again. If reply data is needed, the complete ResponseEncoder + Complete + /// call chain MUST be re-run. + virtual CHIP_ERROR FlushPendingResponses() = 0; + + /// Reply with a data payload. + /// + /// MUST be called at most once per reply. + /// Can be called a 2nd time after a `FlushPendingResponses()` call + /// + /// - responseCommandId must correspond with the data encoded in the returned encoder + /// - Complete(CHIP_NO_ERROR) MUST be called to flush the reply + /// + /// If encoder returns CHIP_ERROR_BUFFER_TOO_SMALL, FlushPendingResponses should be + /// used to attempt to free up buffer space then encoding should be tried again. + virtual DataModel::WrappedStructEncoder & ResponseEncoder(CommandId responseCommandId) = 0; + + /// Signal completing of the reply. + /// + /// MUST be called exactly once to signal a response is to be recorded to be sent. + /// The error code (and the data encoded by ResponseEncoder) may be buffered for + /// sending among other batched responses. + /// + /// If this returns CHIP_ERROR_BUFFER_TOO_SMALL, this can be called a 2nd time after + /// a FlushPendingResponses. + /// + /// Argument behavior: + /// - Commands can only be replied with ONE of the following (spec 8.9.4.4): + /// - command data (i.e. ResponseEncoder contents) + /// - A status (including success/error/cluster-specific-success-or-error ) + /// - As a result there are two possible paths: + /// - IF a Status::Success is given (WITHOUT cluster specific status), then + /// the data in ResponseEncoder is sent as a reply. If no data was sent, + /// a invoke `Status::Success` with no cluster specific data is sent + /// - OTHERWISE any previously encoded data via ResponseEncoder is discarded + /// and the given reply (success with cluster status or failure) is sent + /// as a reply to the invoke. + /// + /// + /// Returns success/failure state. One error code MUST be handled in particular: + /// + /// - CHIP_ERROR_BUFFER_TOO_SMALL will return IF AND ONLY IF the responder was unable + /// to fully serialize the given reply/error data. + /// + /// If such an error is returned, the caller MUST retry by calling FlushPendingResponses + /// first and then re-encoding the reply content (use ResponseEncoder if applicable and + /// call Complete again) + /// + /// - Any other error (i.e. different from CHIP_NO_ERROR) mean that the invoke response + /// will contain an error and such an error is considered permanent. + /// + virtual CHIP_ERROR Complete(StatusIB error) = 0; +}; + +/// Enforces that once acquired, Complete will be called on the underlying writer +class AutoCompleteInvokeResponder +{ +public: + // non-copyable: once you have a handle, keep it + AutoCompleteInvokeResponder(const AutoCompleteInvokeResponder &) = delete; + AutoCompleteInvokeResponder & operator=(const AutoCompleteInvokeResponder &) = delete; + + AutoCompleteInvokeResponder(InvokeResponder * writer) : mWriter(writer) {} + ~AutoCompleteInvokeResponder() + { + if (mCompleteState != CompleteState::kComplete) + { + mWriter->Complete(Protocols::InteractionModel::Status::Failure); + } + } + + /// Direct access to reply encoding. + /// + /// Use this only in conjunction with the other Raw* calls + DataModel::WrappedStructEncoder & RawResponseEncoder(CommandId replyCommandId) + { + return mWriter->ResponseEncoder(replyCommandId); + } + + /// Direct access to flushing replies + /// + /// Use this only in conjunction with the other Raw* calls + CHIP_ERROR RawFlushPendingReplies() + { + // allow a flush if we never called it (this may not be reasonable, however + // we accept an early flush) or if flush is expected + VerifyOrReturnError((mCompleteState == CompleteState::kNeverCalled) || (mCompleteState == CompleteState::kFlushExpected), + CHIP_ERROR_INCORRECT_STATE); + mCompleteState = CompleteState::kFlushed; + return mWriter->FlushPendingResponses(); + } + + /// Call "Complete" without the automatic retries. + /// + /// Use this in conjunction with the other Raw* calls + CHIP_ERROR RawComplete(StatusIB status) + { + VerifyOrReturnError((mCompleteState == CompleteState::kNeverCalled) || (mCompleteState == CompleteState::kFlushed), + CHIP_ERROR_INCORRECT_STATE); + CHIP_ERROR err = mWriter->Complete(status); + if ((err == CHIP_ERROR_BUFFER_TOO_SMALL) && (mCompleteState == CompleteState::kNeverCalled)) + { + mCompleteState = CompleteState::kFlushExpected; + } + else + { + mCompleteState = CompleteState::kComplete; + } + return err; + } + + /// Complete the given command. + /// + /// Automatically handles retries for sending. + /// Cannot be called after Raw* methods are used. + /// + /// Any error returned by this are final and not retriable + /// as a retry for CHIP_ERROR_BUFFER_TOO_SMALL is already built in. + CHIP_ERROR Complete(StatusIB status) + { + VerifyOrReturnError(mCompleteState == CompleteState::kNeverCalled, CHIP_ERROR_INCORRECT_STATE); + // this is a final complete, including retry handling + mCompleteState = CompleteState::kComplete; + CHIP_ERROR err = mWriter->Complete(status); + + if (err != CHIP_ERROR_BUFFER_TOO_SMALL) + { + return err; + } + + // retry once. Failure to flush is permanent. + ReturnErrorOnFailure(mWriter->FlushPendingResponses()); + return mWriter->Complete(status); + } + + /// Sends the specified data structure as a response + /// + /// This version of the send has built-in RETRY and handles + /// Flush/Complete automatically. + /// Cannot be called after Raw* methods are used. + /// + /// Any error returned by this are final and not retriable + /// as a retry for CHIP_ERROR_BUFFER_TOO_SMALL is already built in. + template + CHIP_ERROR Send(const ReplyData & data) + { + VerifyOrReturnError(mCompleteState == CompleteState::kNeverCalled, CHIP_ERROR_INCORRECT_STATE); + // this is a final complete, including retry handling + mCompleteState = CompleteState::kComplete; + CHIP_ERROR err = data.Encode(ResponseEncoder(ReplyData::GetCommandId())); + if (err != CHIP_ERROR_BUFFER_TOO_SMALL) + { + LogErrorOnFailure(err); + err = mWriter->Complete(StatusIB(err)); + } + if (err != CHIP_ERROR_BUFFER_TOO_SMALL) + { + return err; + } + + // retry once. Failure to flush is permanent. + ReturnErrorOnFailure(mWriter->FlushPendingResponses()); + err = data.Encode(ResponseEncoder(ReplyData::GetCommandId())); + + // If encoding fails, we will end up sending an error back to the other side + // the caller + LogErrorOnFailure(err); + if (err == CHIP_NO_ERROR) + { + err = mWriter->Complete(StatusIB(err)); + } + else + { + // Error in "complete" is not something we can really forward anymore since + // we already got an error in Encode ... just log this. + LogErrorOnFailure(mWriter->Complete(StatusIB(err))); + } + + return err; + } + +private: + // Contract says that complete may only be called twice: + // - initial complete + // - again after a `Flush` + // The states here expect we are in: + // + // +----------------------------Flush---------| + // | v + // NEVER --Complete--> F_EXPECTED --Flush--> FLUSHED --Complete--> COMPLETE + // | ^ + // +-------------(success or permanent error)-----------| + enum class CompleteState + { + kNeverCalled, + kFlushExpected, + kFlushed, + kComplete, + }; + + InvokeResponder * mWriter; + CompleteState mCompleteState = CompleteState::kNeverCalled; +}; + +enum ReplyAsyncFlags +{ + // Some commands that are expensive to process (e.g. crypto). + // Implementations may choose to send an ack on the message right away to + // avoid MRP retransmits. + kSlowCommandHandling = 0x0001, +}; + +class InvokeReply +{ +public: + virtual ~InvokeReply() = default; + + // reply with no data + CHIP_ERROR Reply(StatusIB status) { return this->Reply().Complete(status); } + + // Enqueue the content of the reply at this point in time (rather than Async sending it). + // + // Implementations will often batch several replies into one packet for batch commands, + // so it will be implementation-specific on when the actual reply packet is + // sent. + virtual AutoCompleteInvokeResponder Reply() = 0; + + // Reply "later" to the command. This allows async processing. A reply will be forced + // when the returned InvokeReply is destroyed. + // + // NOTE: Each InvokeReply is associated with a separate `CommandDataIB` within batch + // commands. When replying asynchronously, each InvokeReply will set the response + // data for the given commandpath/ref only. + // + // IF empty pointer is returned, insufficient memory to reply async is available and + // this should be handled (e.g. by returning an error to the handler/replying with + // an errorcode synchronously). + virtual std::unique_ptr ReplyAsync(BitFlags flags) = 0; +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/IterationTypes.h b/src/app/interaction-model/IterationTypes.h new file mode 100644 index 00000000000000..32ad8bc42b73be --- /dev/null +++ b/src/app/interaction-model/IterationTypes.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024 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 +#include + +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace InteractionModel { + +enum class ClusterQualityFlags : uint32_t +{ + kDiagnosticsData = 0x0001, // `K` quality, may be filtered out in subscriptions +}; + +struct ClusterInfo +{ + DataVersion dataVersion; // current version of this cluster + BitFlags flags; +}; + +struct ClusterEntry +{ + ConcreteClusterPath path; + ClusterInfo info; +}; + +enum class AttributeQualityFlags : uint32_t +{ + kListAttribute = 0x0001, // This attribute is a list attribute + kChangesOmitted = 0x0002, // `C` quality on attributes +}; + +struct AttributeInfo +{ + BitFlags flags; +}; + +struct AttributeEntry +{ + ConcreteAttributePath path; + AttributeInfo info; +}; + +/// Provides metadata information for a data model +/// +/// The data model can be viewed as a tree of endpoint/cluster/attribute +/// where each element can be iterated through independently +/// +/// Iteration rules: +/// - kInvalidEndpointId will be returned when iteration ends (or generally kInvalid* for paths) +/// - Any internal iteration errors are just logged (callers do not handle iteration CHIP_ERROR) +/// - Iteration order is NOT guaranteed, however uniqueness and completeness is (must iterate +/// over all possible distinct values as long as no internal structural changes occur) +class AttributeTreeIterator +{ +public: + virtual ~AttributeTreeIterator() = default; + + virtual EndpointId FirstEndpoint() = 0; + virtual EndpointId NextEndpoint(EndpointId before) = 0; + + virtual ClusterEntry FirstCluster(EndpointId endpoint) = 0; + virtual ClusterEntry NextCluster(const ConcreteClusterPath & before) = 0; + virtual std::optional GetClusterInfo(const ConcreteClusterPath & path) = 0; + + virtual AttributeEntry FirstAttribute(const ConcreteClusterPath & cluster) = 0; + virtual AttributeEntry NextAttribute(const ConcreteAttributePath & before) = 0; + virtual std::optional GetAttributeInfo(const ConcreteAttributePath & path) = 0; +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/Model.h b/src/app/interaction-model/Model.h new file mode 100644 index 00000000000000..b3a127c074aaa1 --- /dev/null +++ b/src/app/interaction-model/Model.h @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2024 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 +#include + +#include +#include + +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace InteractionModel { + +/// Represents operations against a matter-defined data model. +/// +/// Class is SINGLE-THREADED: +/// - operations are assumed to only be ever run in a single event-loop +/// thread or equivalent +/// - class is allowed to attempt to cache indexes/locations for faster +/// lookups of things (e.g during iterations) +class Model : public AttributeTreeIterator +{ +public: + virtual ~Model() = default; + + // `actions` pointers will be guaranteed valid until Shutdown is called() + virtual CHIP_ERROR Startup(InteractionModelActions actions) + { + mActions = actions; + return CHIP_NO_ERROR; + } + virtual CHIP_ERROR Shutdown() = 0; + + // During the transition phase, we expect a large subset of code to require access to + // event emitting, path marking and other operations + virtual InteractionModelActions CurrentActions() { return mActions; } + + /// List reading has specific handling logic: + /// `state` contains in/out data about the current list reading. MUST start with kInvalidListIndex on first call + /// + /// Return codes: + /// CHIP_ERROR_MORE_LIST_DATA_AVAILABLE (NOTE: new error defined for this purpose) + /// - partial data written to the destination + /// - destination will contain AT LEAST one valid list entry fully serialized + /// - destination will be fully valid (it will be rolled back on partial list writes) + /// CHIP_IM_GLOBAL_STATUS(code): + /// - error codes that are translatable in IM status codes (otherwise we expect Failure to be reported) + /// - In particular, some handlers rely on special handling for: + /// - `UnsupportedAccess` - for ACL checks (e.g. wildcard expansion may choose to skip these) + /// - to check for this, CHIP_ERROR provides: + /// - ::IsPart(ChipError::SdkPart::kIMGlobalStatus) -> bool + /// - ::GetSdkCode() -> uint8_t to translate to the actual code + virtual CHIP_ERROR ReadAttribute(const ReadAttributeRequest & request, ReadState & state, AttributeValueEncoder & encoder) = 0; + + /// Requests a write of an attribute. + /// + /// When this is invoked, caller is expected to have already done some validations: + /// - cluster `data version` has been checked for the incoming request if applicable + /// + /// List operation support: + /// - the first list write will have `request.writeFlags.Has(WriteFlags::kListBegin)` + /// - the last list write will have `request.writeFlags.Has(WriteFlags::kListEnd)` + /// - the last list write MAY have empty data (no list items) + /// + /// When `request.writeFlags.Has(WriteFlags::kForceInternal)` the request is from an internal app update + /// and SHOULD bypass some internal checks (like timed enforcement, potentially read-only restrictions) + /// + /// Return codes + /// CHIP_IM_GLOBAL_STATUS(code): + /// - error codes that are translatable to specific IM codes + /// - in particular, the following codes are interesting/expected + /// - `UnsupportedWrite` for attempts to write read-only data + /// - `UnsupportedAccess` for ACL failures + /// - `NeedsTimedInteraction` for writes that are not timed however are required to be so + virtual CHIP_ERROR WriteAttribute(const WriteAttributeRequest & request, AttributeValueDecoder & decoder) = 0; + + /// `responder` is used to send back the reply. + /// - calling Reply() or ReplyAsync() will let the application control the reply + /// - returning a CHIP_NO_ERROR without reply/reply_async implies a Status::Success reply without data + /// - returning a CHIP_*_ERROR implies an error reply (error and data are mutually exclusive) + /// + /// See InvokeReply/AutoCompleteInvokeResponder for details on how to send back replies and expected + /// error handling. If you require knowledge if a response was successfully sent, use the underlying + /// `reply` object instead of returning an error codes from Invoke. + /// + /// Return codes + /// CHIP_IM_GLOBAL_STATUS(code): + /// - error codes that are translatable to specific IM codes + /// - in particular, the following codes are interesting/expected + /// - `UnsupportedEndpoint` for invalid endpoint + /// - `UnsupportedCluster` for no such cluster on the endpoint + /// - `UnsupportedCommand` for no such command in the cluster + /// - `UnsupportedAccess` for permission errors (ACL or fabric scoped with invalid fabric) + /// - `NeedsTimedInteraction` if the invoke requires timed interaction support + virtual CHIP_ERROR Invoke(const InvokeRequest & request, chip::TLV::TLVReader & input_arguments, InvokeReply & reply) = 0; + +private: + InteractionModelActions mActions = { nullptr }; +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/OperationTypes.h b/src/app/interaction-model/OperationTypes.h new file mode 100644 index 00000000000000..115dc11a1ff3bc --- /dev/null +++ b/src/app/interaction-model/OperationTypes.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024 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 +#include +#include +#include + +#include +#include + +namespace chip { +namespace app { +namespace InteractionModel { + +/// Contains common flags among all interaction model operations: read/write/invoke +enum class OperationFlags : uint32_t +{ + kInternal = 0x0001, // Internal request for data changes (can bypass checks/ACL etc.) +}; + +/// This information is available for ALL interactions: read/write/invoke +struct OperationRequest +{ + OperationFlags operationFlags; + + /// Current authentication data EXCEPT for internal requests. + /// - Non-internal requests MUST have this set. + /// - operationFlags.Has(OperationFlags::kInternal) MUST NOT have this set + std::optional subjectDescriptor; +}; + +enum class ReadFlags : uint32_t +{ + kFabricFiltered = 0x0001, // reading is performed fabric-filtered +}; + +struct ReadAttributeRequest : OperationRequest +{ + ConcreteAttributePath path; + std::optional dataVersion; + BitFlags readFlags; +}; + +struct ReadState +{ + // When reading lists, reading will start at this index. + // As list data is read, this index is incremented + ListIndex listEncodeStart = kInvalidListIndex; +}; + +enum class WriteFlags : uint32_t +{ + kTimed = 0x0001, // Received as a 2nd command after a timed invoke + kListBegin = 0x0002, // This is the FIRST list data element in a series of data + kListEnd = 0x0004, // This is the LAST list element to write +}; + +struct WriteAttributeRequest : OperationRequest +{ + ConcreteDataAttributePath path; // NOTE: this also contains LIST operation options (i.e. "data" path type) + BitFlags writeFlags; +}; + +enum class InvokeFlags : uint32_t +{ + kTimed = 0x0001, // Received as a 2nd command after a timed invoke +}; + +struct InvokeRequest : OperationRequest +{ + ConcreteCommandPath path; + std::optional groupRequestId; // set if and only if this was a group request + BitFlags invokeFlags; +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/Paths.h b/src/app/interaction-model/Paths.h new file mode 100644 index 00000000000000..2bf9f0c4158011 --- /dev/null +++ b/src/app/interaction-model/Paths.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 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 + +namespace chip { +namespace app { +namespace InteractionModel { + +/// Handles path attributes for interaction models. +/// +/// It allows a user of the class to mark specific paths +/// as having changed. The intended use is for some listener to +/// perform operations as a result of something having changed, +/// usually by forwarding updates (e.g. in case of subscriptions +/// that cover that path). +/// +/// Methods on this class MUCH be called from within the matter +/// main loop as they will likely trigger interaction model +/// internal updates and subscription event updates. +class Paths +{ +public: + virtual ~Paths() = 0; + + /// Mark some specific attributes dirty. + /// Wildcards are supported. + virtual void MarkDirty(const AttributePathParams & path) = 0; +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/RequestContext.h b/src/app/interaction-model/RequestContext.h new file mode 100644 index 00000000000000..74fa9af9da0fc8 --- /dev/null +++ b/src/app/interaction-model/RequestContext.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 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 + +namespace chip { +namespace app { +namespace InteractionModel { + +// Context for a currently executing request +class RequestContext +{ +public: + virtual ~RequestContext() = default; + + /// Valid ONLY during synchronous handling of a Read/Write/Invoke + /// + /// Used sparingly, however some operations will require these. An example + /// usage is "Operational Credentials aborting communications on removed fabrics" + /// + /// Callers MUST check for null here (e.g. unit tests mocks may set this to + /// nullptr due to object complexity) + virtual Messaging::ExchangeContext * CurrentExchange() = 0; +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/tests/BUILD.gn b/src/app/interaction-model/tests/BUILD.gn new file mode 100644 index 00000000000000..c7d36b4f1dcdca --- /dev/null +++ b/src/app/interaction-model/tests/BUILD.gn @@ -0,0 +1,23 @@ +# Copyright (c) 2024 Project CHIP Authors +# +# 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. +import("//build_overrides/chip.gni") +import("${chip_root}/build/chip/chip_test_suite.gni") + +chip_test_suite("tests") { + test_sources = [ "TestEventEmitting.cpp" ] + + cflags = [ "-Wconversion" ] + + public_deps = [ "${chip_root}/src/app/interaction-model" ] +} diff --git a/src/app/interaction-model/tests/TestEventEmitting.cpp b/src/app/interaction-model/tests/TestEventEmitting.cpp new file mode 100644 index 00000000000000..cb49dc25caa068 --- /dev/null +++ b/src/app/interaction-model/tests/TestEventEmitting.cpp @@ -0,0 +1,164 @@ +/* + * + * Copyright (c) 2024 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. + */ + +#include +#include +#include +#include + +#include + +namespace { + +using namespace chip; +using namespace chip::app; +using namespace chip::app::InteractionModel; + +using StartUpEventType = chip::app::Clusters::BasicInformation::Events::StartUp::Type; +using AccessControlEntryChangedType = chip::app::Clusters::AccessControl::Events::AccessControlEntryChanged::Type; + +constexpr uint32_t kFakeSoftwareVersion = 0x1234abcd; + +/// Keeps the "last event" in-memory to allow tests to validate +/// that event writing and encoding worked. +class LogOnlyEvents : public Events +{ +public: + CHIP_ERROR GenerateEvent(EventLoggingDelegate * eventContentWriter, const EventOptions & options, + EventNumber & generatedEventNumber) override + { + TLV::TLVWriter writer; + TLV::TLVType outerType; + writer.Init(mLastEventEncodeBuffer); + + ReturnErrorOnFailure(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, outerType)); + ReturnErrorOnFailure(eventContentWriter->WriteEvent(writer)); + ReturnErrorOnFailure(writer.EndContainer(outerType)); + ReturnErrorOnFailure(writer.Finalize()); + mLastEncodedSpan = ByteSpan(mLastEventEncodeBuffer, writer.GetLengthWritten()); + + mLastOptions = options; + generatedEventNumber = ++mCurrentEventNumber; + + return CHIP_NO_ERROR; + } + + EventNumber CurrentEventNumber() const { return mCurrentEventNumber; } + const EventOptions & LastOptions() const { return mLastOptions; } + ByteSpan LastWrittenEvent() const { return mLastEncodedSpan; } + + // This relies on the default encoding of events which uses + // DataModel::Encode on a EventDataIB::Tag::kData + template + CHIP_ERROR DecodeLastEvent(T & dest) + { + // attempt to decode the last encoded event + TLV::TLVReader reader; + TLV::TLVType outerType; + + reader.Init(LastWrittenEvent()); + + ReturnErrorOnFailure(reader.Next()); + ReturnErrorOnFailure(reader.EnterContainer(outerType)); + + ReturnErrorOnFailure(reader.Next()); // MUST be positioned on the first element + ReturnErrorOnFailure(DataModel::Decode(reader, dest)); + + ReturnErrorOnFailure(reader.ExitContainer(outerType)); + + return CHIP_NO_ERROR; + } + +private: + EventNumber mCurrentEventNumber = 0; + EventOptions mLastOptions; + uint8_t mLastEventEncodeBuffer[128]; + ByteSpan mLastEncodedSpan; +}; + +} // namespace + +TEST(TestInteractionModelEventEmitting, TestBasicType) +{ + LogOnlyEvents logOnlyEvents; + Events * events = &logOnlyEvents; + + StartUpEventType event{ kFakeSoftwareVersion }; + + EventNumber n1 = events->GenerateEvent(event, 0 /* EndpointId */); + ASSERT_EQ(n1, logOnlyEvents.CurrentEventNumber()); + ASSERT_EQ(logOnlyEvents.LastOptions().mPath, + ConcreteEventPath(0 /* endpointId */, StartUpEventType::GetClusterId(), StartUpEventType::GetEventId())); + + chip::app::Clusters::BasicInformation::Events::StartUp::DecodableType decoded_event; + CHIP_ERROR err = logOnlyEvents.DecodeLastEvent(decoded_event); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(EventLogging, "Decoding failed: %" CHIP_ERROR_FORMAT, err.Format()); + } + ASSERT_EQ(err, CHIP_NO_ERROR); + ASSERT_EQ(decoded_event.softwareVersion, kFakeSoftwareVersion); + + EventNumber n2 = events->GenerateEvent(event, /* endpointId = */ 1); + ASSERT_EQ(n2, logOnlyEvents.CurrentEventNumber()); + ASSERT_NE(n1, logOnlyEvents.CurrentEventNumber()); + + ASSERT_EQ(logOnlyEvents.LastOptions().mPath, + ConcreteEventPath(1 /* endpointId */, StartUpEventType::GetClusterId(), StartUpEventType::GetEventId())); +} + +TEST(TestInteractionModelEventEmitting, TestFabricScoped) +{ + constexpr NodeId kTestNodeId = 0x12ab; + constexpr uint16_t kTestPasscode = 12345; + constexpr FabricIndex kTestFabricIndex = kMinValidFabricIndex + 10; + static_assert(kTestFabricIndex != kUndefinedFabricIndex); + + LogOnlyEvents logOnlyEvents; + Events * events = &logOnlyEvents; + + AccessControlEntryChangedType event; + event.adminNodeID = chip::app::DataModel::MakeNullable(kTestNodeId); + event.adminPasscodeID = chip::app::DataModel::MakeNullable(kTestPasscode); + + EventNumber n1 = events->GenerateEvent(event, 0 /* EndpointId */); + // encoding without a fabric ID MUST fail for fabric events + ASSERT_EQ(n1, kInvalidEventId); + + event.fabricIndex = kTestFabricIndex; + n1 = events->GenerateEvent(event, /* endpointId = */ 0); + + ASSERT_NE(n1, kInvalidEventId); + ASSERT_EQ(n1, logOnlyEvents.CurrentEventNumber()); + ASSERT_EQ(logOnlyEvents.LastOptions().mPath, + ConcreteEventPath(0 /* endpointId */, AccessControlEntryChangedType::GetClusterId(), + AccessControlEntryChangedType::GetEventId())); + + chip::app::Clusters::AccessControl::Events::AccessControlEntryChanged::DecodableType decoded_event; + CHIP_ERROR err = logOnlyEvents.DecodeLastEvent(decoded_event); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(EventLogging, "Decoding failed: %" CHIP_ERROR_FORMAT, err.Format()); + } + ASSERT_EQ(err, CHIP_NO_ERROR); + ASSERT_EQ(decoded_event.adminNodeID.ValueOr(0), kTestNodeId); + ASSERT_EQ(decoded_event.adminPasscodeID.ValueOr(0), kTestPasscode); + ASSERT_EQ(decoded_event.fabricIndex, kTestFabricIndex); +}