Define a IM/DM decoupling interface for interaction model engine to interact with the data model bits (#32914)
* Start adding a interaction model decoupling
* Restyle
* Also add invoke responder bits
* Add the first model definition, for each operation
* Add all files into the build definition
* Start adding the side effect support classes to the interaction model definitions
* Add actions and integrate them to the IM model
* Add iteration methods
* Restyle
* Add all files to the build definition
* Start defining a unit test to at least validate compilation. Will later validate that we get events emitted
* Fix a dependency
* Move EventLoggingDelegate to be part of "events" in app.
- Remove unneeded includes from this header
- add dependency to core (due to TLV)
* Prepare for testing ... does not compile however TODOs are starting to go away
* Fix SFINAE logic on eventing
* test that something is actually emitted for events
* Tests actually do something
* Some tests actually pass
* Restyle
* Minor style updates
* Rename EmitEvent to GenerateEvent
* Add description for paths
* Cleanup some example text ... we do not need an exhaustive usage list
* Restyle
* Fix spelling
* Renames based on code review: use UpperCamelCase
* Updated proposal for retries and paths
* Restyle
* one more comment
* Restyled by clang-format
* Fix typo in parameter type
* Some updates for code review
* Undo submodule update
* Fix logic error in buffer-too-small check for buffer data
* Fix return error code when send encounters an error
* Fix logic for mCompleted handling in the auto-complete handling
* More comments
* Comments
* Use AAI types for interaction model. I think they will need some splitting, however for now they seem to be a solid catch-all
* Update some naming to not use the a... syntax since those are odd
* Code review updates
* Added extra comment
* Enforce lifetime in invoke responses
* More comments
* More comments
* Restyle
* Added comment on replyasync with nullptr
* Update src/app/interaction-model/InvokeResponder.h
Co-authored-by: Terence Hampson <thampson@google.com>
* Update src/app/interaction-model/InvokeResponder.h
Co-authored-by: Terence Hampson <thampson@google.com>
* Update src/app/interaction-model/InvokeResponder.h
Co-authored-by: Terence Hampson <thampson@google.com>
* Update src/app/interaction-model/InvokeResponder.h
Co-authored-by: Terence Hampson <thampson@google.com>
* Some code review updates
* Some code review updates
* Make the subject descriptor optional
* use std::optional instead of chip optiona
* Restyle
---------
Co-authored-by: Restyled.io <commits@restyled.io>
Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Terence Hampson <thampson@google.com>
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 61292e1..d1dfcc7 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -55,6 +55,7 @@
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 0000000..62021fb
--- /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 <app/interaction-model/Events.h>
+#include <app/interaction-model/Paths.h>
+#include <app/interaction-model/RequestContext.h>
+
+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 0000000..c91c2ae
--- /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 0000000..255a55e
--- /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 <app/EventLoggingDelegate.h>
+#include <app/EventLoggingTypes.h>
+#include <app/MessageDef/EventDataIB.h>
+#include <app/data-model/Encode.h>
+#include <app/data-model/FabricScoped.h>
+#include <lib/core/CHIPError.h>
+#include <lib/support/logging/CHIPLogging.h>
+
+#include <type_traits>
+
+namespace chip {
+namespace app {
+namespace InteractionModel {
+
+namespace internal {
+template <typename T>
+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 <typename E, typename T, std::enable_if_t<DataModel::IsFabricScoped<T>::value, bool> = true>
+EventNumber GenerateEvent(E & emittor, const T & aEventData, EndpointId aEndpoint)
+{
+ internal::SimpleEventLoggingDelegate<T> 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 <typename E, typename T, std::enable_if_t<!DataModel::IsFabricScoped<T>::value, bool> = true>
+EventNumber GenerateEvent(E & emittor, const T & aEventData, EndpointId endpointId)
+{
+ internal::SimpleEventLoggingDelegate<T> 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 <typename T>
+ 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 0000000..0a399b2
--- /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 <app/MessageDef/StatusIB.h>
+#include <app/data-model/WrappedStructEncoder.h>
+#include <lib/core/CHIPError.h>
+
+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 <typename ReplyData>
+ 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<InvokeReply> ReplyAsync(BitFlags<ReplyAsyncFlags> 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 0000000..32ad8bc
--- /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 <cstdint>
+#include <optional>
+
+#include <app/ConcreteAttributePath.h>
+#include <app/ConcreteClusterPath.h>
+#include <lib/core/DataModelTypes.h>
+#include <lib/support/BitFlags.h>
+
+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<ClusterQualityFlags> 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<AttributeQualityFlags> 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<ClusterInfo> GetClusterInfo(const ConcreteClusterPath & path) = 0;
+
+ virtual AttributeEntry FirstAttribute(const ConcreteClusterPath & cluster) = 0;
+ virtual AttributeEntry NextAttribute(const ConcreteAttributePath & before) = 0;
+ virtual std::optional<AttributeInfo> 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 0000000..b3a127c
--- /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 <lib/core/TLVReader.h>
+#include <lib/core/TLVWriter.h>
+
+#include <app/AttributeValueDecoder.h>
+#include <app/AttributeValueEncoder.h>
+
+#include <app/interaction-model/Actions.h>
+#include <app/interaction-model/InvokeResponder.h>
+#include <app/interaction-model/IterationTypes.h>
+#include <app/interaction-model/OperationTypes.h>
+
+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 0000000..115dc11
--- /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 <access/SubjectDescriptor.h>
+#include <app/ConcreteAttributePath.h>
+#include <app/ConcreteCommandPath.h>
+#include <lib/support/BitFlags.h>
+
+#include <cstdint>
+#include <optional>
+
+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<chip::Access::SubjectDescriptor> subjectDescriptor;
+};
+
+enum class ReadFlags : uint32_t
+{
+ kFabricFiltered = 0x0001, // reading is performed fabric-filtered
+};
+
+struct ReadAttributeRequest : OperationRequest
+{
+ ConcreteAttributePath path;
+ std::optional<DataVersion> dataVersion;
+ BitFlags<ReadFlags> 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> writeFlags;
+};
+
+enum class InvokeFlags : uint32_t
+{
+ kTimed = 0x0001, // Received as a 2nd command after a timed invoke
+};
+
+struct InvokeRequest : OperationRequest
+{
+ ConcreteCommandPath path;
+ std::optional<GroupId> groupRequestId; // set if and only if this was a group request
+ BitFlags<InvokeFlags> 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 0000000..2bf9f0c
--- /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 <app/AttributePathParams.h>
+
+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 0000000..74fa9af
--- /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 <messaging/ExchangeContext.h>
+
+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 0000000..c7d36b4
--- /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 0000000..cb49dc2
--- /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 <app-common/zap-generated/cluster-objects.h>
+#include <app/data-model/Decode.h>
+#include <app/interaction-model/Events.h>
+#include <lib/support/CodeUtils.h>
+
+#include <gtest/gtest.h>
+
+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 <typename T>
+ 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);
+}