| /* |
| * 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/codegen-data-model-provider/CodegenDataModelProvider.h> |
| |
| #include <app-common/zap-generated/attribute-type.h> |
| #include <app/RequiredPrivilege.h> |
| #include <app/util/IMClusterCommandHandler.h> |
| #include <app/util/attribute-storage.h> |
| #include <app/util/endpoint-config-api.h> |
| #include <lib/core/DataModelTypes.h> |
| |
| #include <optional> |
| #include <variant> |
| |
| namespace chip { |
| namespace app { |
| namespace { |
| |
| /// Load the cluster information into the specified destination |
| std::variant<CHIP_ERROR, DataModel::ClusterInfo> LoadClusterInfo(const ConcreteClusterPath & path, const EmberAfCluster & cluster) |
| { |
| DataVersion * versionPtr = emberAfDataVersionStorage(path); |
| if (versionPtr == nullptr) |
| { |
| ChipLogError(AppServer, "Failed to get data version for %d/" ChipLogFormatMEI, static_cast<int>(path.mEndpointId), |
| ChipLogValueMEI(cluster.clusterId)); |
| return CHIP_ERROR_NOT_FOUND; |
| } |
| |
| DataModel::ClusterInfo info(*versionPtr); |
| |
| // TODO: set entry flags: |
| // info->flags.Set(ClusterQualityFlags::kDiagnosticsData) |
| |
| return info; |
| } |
| |
| /// Converts a EmberAfCluster into a ClusterEntry |
| std::variant<CHIP_ERROR, DataModel::ClusterEntry> ClusterEntryFrom(EndpointId endpointId, const EmberAfCluster & cluster) |
| { |
| ConcreteClusterPath clusterPath(endpointId, cluster.clusterId); |
| auto info = LoadClusterInfo(clusterPath, cluster); |
| |
| if (CHIP_ERROR * err = std::get_if<CHIP_ERROR>(&info)) |
| { |
| return *err; |
| } |
| |
| if (DataModel::ClusterInfo * infoValue = std::get_if<DataModel::ClusterInfo>(&info)) |
| { |
| return DataModel::ClusterEntry{ |
| .path = clusterPath, |
| .info = *infoValue, |
| }; |
| } |
| return CHIP_ERROR_INCORRECT_STATE; |
| } |
| |
| /// Finds the first server cluster entry for the given endpoint data starting at [start_index] |
| /// |
| /// Returns an invalid entry if no more server clusters are found |
| DataModel::ClusterEntry FirstServerClusterEntry(EndpointId endpointId, const EmberAfEndpointType * endpoint, unsigned start_index, |
| unsigned & found_index) |
| { |
| for (unsigned cluster_idx = start_index; cluster_idx < endpoint->clusterCount; cluster_idx++) |
| { |
| const EmberAfCluster & cluster = endpoint->cluster[cluster_idx]; |
| if (!cluster.IsServer()) |
| { |
| continue; |
| } |
| |
| found_index = cluster_idx; |
| auto entry = ClusterEntryFrom(endpointId, cluster); |
| |
| if (DataModel::ClusterEntry * entryValue = std::get_if<DataModel::ClusterEntry>(&entry)) |
| { |
| return *entryValue; |
| } |
| |
| #if CHIP_ERROR_LOGGING |
| if (CHIP_ERROR * errValue = std::get_if<CHIP_ERROR>(&entry)) |
| { |
| ChipLogError(AppServer, "Failed to load cluster entry: %" CHIP_ERROR_FORMAT, errValue->Format()); |
| } |
| else |
| { |
| // Should NOT be possible: entryFrom has only 2 variants |
| ChipLogError(AppServer, "Failed to load cluster entry, UNKNOWN entry return type"); |
| } |
| #endif |
| } |
| |
| return DataModel::ClusterEntry::kInvalid; |
| } |
| |
| /// Load the attribute information into the specified destination |
| /// |
| /// `info` is assumed to be default-constructed/clear (i.e. this sets flags, but does not reset them). |
| void LoadAttributeInfo(const ConcreteAttributePath & path, const EmberAfAttributeMetadata & attribute, |
| DataModel::AttributeInfo * info) |
| { |
| info->readPrivilege = RequiredPrivilege::ForReadAttribute(path); |
| if (!attribute.IsReadOnly()) |
| { |
| info->writePrivilege = RequiredPrivilege::ForWriteAttribute(path); |
| } |
| |
| info->flags.Set(DataModel::AttributeQualityFlags::kListAttribute, (attribute.attributeType == ZCL_ARRAY_ATTRIBUTE_TYPE)); |
| info->flags.Set(DataModel::AttributeQualityFlags::kTimed, attribute.MustUseTimedWrite()); |
| |
| // NOTE: we do NOT provide additional info for: |
| // - IsExternal/IsSingleton/IsAutomaticallyPersisted is not used by IM handling |
| // - IsSingleton spec defines it for CLUSTERS where as we have it for ATTRIBUTES |
| // - Several specification flags are not available (reportable, quieter reporting, |
| // fixed, source attribution) |
| |
| // TODO: Set additional flags: |
| // info->flags.Set(DataModel::AttributeQualityFlags::kFabricScoped) |
| // info->flags.Set(DataModel::AttributeQualityFlags::kFabricSensitive) |
| // info->flags.Set(DataModel::AttributeQualityFlags::kChangesOmitted) |
| } |
| |
| DataModel::AttributeEntry AttributeEntryFrom(const ConcreteClusterPath & clusterPath, const EmberAfAttributeMetadata & attribute) |
| { |
| DataModel::AttributeEntry entry; |
| |
| entry.path = ConcreteAttributePath(clusterPath.mEndpointId, clusterPath.mClusterId, attribute.attributeId); |
| LoadAttributeInfo(entry.path, attribute, &entry.info); |
| |
| return entry; |
| } |
| |
| DataModel::CommandEntry CommandEntryFrom(const ConcreteClusterPath & clusterPath, CommandId clusterCommandId) |
| { |
| DataModel::CommandEntry entry; |
| entry.path = ConcreteCommandPath(clusterPath.mEndpointId, clusterPath.mClusterId, clusterCommandId); |
| entry.info.invokePrivilege = RequiredPrivilege::ForInvokeCommand(entry.path); |
| |
| entry.info.flags.Set(DataModel::CommandQualityFlags::kTimed, CommandNeedsTimedInvoke(clusterPath.mClusterId, clusterCommandId)); |
| |
| entry.info.flags.Set(DataModel::CommandQualityFlags::kFabricScoped, |
| CommandIsFabricScoped(clusterPath.mClusterId, clusterCommandId)); |
| |
| return entry; |
| } |
| |
| const ConcreteCommandPath kInvalidCommandPath(kInvalidEndpointId, kInvalidClusterId, kInvalidCommandId); |
| |
| } // namespace |
| |
| std::optional<CommandId> CodegenDataModelProvider::EmberCommandListIterator::First(const CommandId * list) |
| { |
| VerifyOrReturnValue(list != nullptr, std::nullopt); |
| mCurrentList = mCurrentHint = list; |
| |
| VerifyOrReturnValue(*mCurrentList != kInvalidCommandId, std::nullopt); |
| return *mCurrentList; |
| } |
| |
| std::optional<CommandId> CodegenDataModelProvider::EmberCommandListIterator::Next(const CommandId * list, CommandId previousId) |
| { |
| VerifyOrReturnValue(list != nullptr, std::nullopt); |
| VerifyOrReturnValue(previousId != kInvalidCommandId, std::nullopt); |
| |
| if (mCurrentList != list) |
| { |
| // invalidate the hint if switching lists... |
| mCurrentHint = nullptr; |
| mCurrentList = list; |
| } |
| |
| if ((mCurrentHint == nullptr) || (*mCurrentHint != previousId)) |
| { |
| // we did not find a usable hint. Search from the to set the hint |
| mCurrentHint = mCurrentList; |
| while ((*mCurrentHint != kInvalidCommandId) && (*mCurrentHint != previousId)) |
| { |
| mCurrentHint++; |
| } |
| } |
| |
| VerifyOrReturnValue(*mCurrentHint == previousId, std::nullopt); |
| |
| // hint is valid and can be used immediately |
| mCurrentHint++; // this is the next value |
| return (*mCurrentHint == kInvalidCommandId) ? std::nullopt : std::make_optional(*mCurrentHint); |
| } |
| |
| bool CodegenDataModelProvider::EmberCommandListIterator::Exists(const CommandId * list, CommandId toCheck) |
| { |
| VerifyOrReturnValue(list != nullptr, false); |
| VerifyOrReturnValue(toCheck != kInvalidCommandId, false); |
| |
| if (mCurrentList != list) |
| { |
| // invalidate the hint if switching lists... |
| mCurrentHint = nullptr; |
| mCurrentList = list; |
| } |
| |
| // maybe already positioned correctly |
| if ((mCurrentHint != nullptr) && (*mCurrentHint == toCheck)) |
| { |
| return true; |
| } |
| |
| // move and try to find it |
| mCurrentHint = mCurrentList; |
| while ((*mCurrentHint != kInvalidCommandId) && (*mCurrentHint != toCheck)) |
| { |
| mCurrentHint++; |
| } |
| |
| return (*mCurrentHint == toCheck); |
| } |
| |
| DataModel::ActionReturnStatus CodegenDataModelProvider::Invoke(const DataModel::InvokeRequest & request, |
| TLV::TLVReader & input_arguments, CommandHandler * handler) |
| { |
| // TODO: CommandHandlerInterface support is currently |
| // residing in InteractionModelEngine itself. We may want to separate this out |
| // into its own registry, similar to attributes, so that IM is decoupled from actual storage of things. |
| // |
| // Open issue at https://github.com/project-chip/connectedhomeip/issues/34258 |
| |
| // Ember dispatching automatically uses `handler` to set an appropriate result or status |
| // This never fails (as handler error is encoded as needed). |
| DispatchSingleClusterCommand(request.path, input_arguments, handler); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| EndpointId CodegenDataModelProvider::FirstEndpoint() |
| { |
| // find the first enabled index |
| const uint16_t lastEndpointIndex = emberAfEndpointCount(); |
| for (uint16_t endpoint_idx = 0; endpoint_idx < lastEndpointIndex; endpoint_idx++) |
| { |
| if (emberAfEndpointIndexIsEnabled(endpoint_idx)) |
| { |
| mEndpointIterationHint = endpoint_idx; |
| return emberAfEndpointFromIndex(endpoint_idx); |
| } |
| } |
| |
| // No enabled endpoint found. Give up |
| return kInvalidEndpointId; |
| } |
| |
| std::optional<unsigned> CodegenDataModelProvider::TryFindEndpointIndex(EndpointId id) const |
| { |
| const uint16_t lastEndpointIndex = emberAfEndpointCount(); |
| |
| if ((mEndpointIterationHint < lastEndpointIndex) && emberAfEndpointIndexIsEnabled(mEndpointIterationHint) && |
| (id == emberAfEndpointFromIndex(mEndpointIterationHint))) |
| { |
| return std::make_optional(mEndpointIterationHint); |
| } |
| |
| // Linear search, this may be slow |
| uint16_t idx = emberAfIndexFromEndpoint(id); |
| if (idx == kEmberInvalidEndpointIndex) |
| { |
| return std::nullopt; |
| } |
| |
| return std::make_optional<unsigned>(idx); |
| } |
| |
| EndpointId CodegenDataModelProvider::NextEndpoint(EndpointId before) |
| { |
| const uint16_t lastEndpointIndex = emberAfEndpointCount(); |
| |
| std::optional<unsigned> before_idx = TryFindEndpointIndex(before); |
| if (!before_idx.has_value()) |
| { |
| return kInvalidEndpointId; |
| } |
| |
| // find the first enabled index |
| for (uint16_t endpoint_idx = static_cast<uint16_t>(*before_idx + 1); endpoint_idx < lastEndpointIndex; endpoint_idx++) |
| { |
| if (emberAfEndpointIndexIsEnabled(endpoint_idx)) |
| { |
| mEndpointIterationHint = endpoint_idx; |
| return emberAfEndpointFromIndex(endpoint_idx); |
| } |
| } |
| |
| // No enabled enpoint after "before" was found, give up |
| return kInvalidEndpointId; |
| } |
| |
| DataModel::ClusterEntry CodegenDataModelProvider::FirstCluster(EndpointId endpointId) |
| { |
| const EmberAfEndpointType * endpoint = emberAfFindEndpointType(endpointId); |
| VerifyOrReturnValue(endpoint != nullptr, DataModel::ClusterEntry::kInvalid); |
| VerifyOrReturnValue(endpoint->clusterCount > 0, DataModel::ClusterEntry::kInvalid); |
| VerifyOrReturnValue(endpoint->cluster != nullptr, DataModel::ClusterEntry::kInvalid); |
| |
| return FirstServerClusterEntry(endpointId, endpoint, 0, mClusterIterationHint); |
| } |
| |
| std::optional<unsigned> CodegenDataModelProvider::TryFindServerClusterIndex(const EmberAfEndpointType * endpoint, |
| ClusterId id) const |
| { |
| const unsigned clusterCount = endpoint->clusterCount; |
| |
| if (mClusterIterationHint < clusterCount) |
| { |
| const EmberAfCluster & cluster = endpoint->cluster[mClusterIterationHint]; |
| if (cluster.IsServer() && (cluster.clusterId == id)) |
| { |
| return std::make_optional(mClusterIterationHint); |
| } |
| } |
| |
| // linear search, this may be slow |
| // does NOT use emberAfClusterIndex to not iterate over endpoints as we have |
| // already found the correct endpoint |
| for (unsigned cluster_idx = 0; cluster_idx < clusterCount; cluster_idx++) |
| { |
| const EmberAfCluster & cluster = endpoint->cluster[cluster_idx]; |
| if (cluster.IsServer() && (cluster.clusterId == id)) |
| { |
| return std::make_optional(cluster_idx); |
| } |
| } |
| |
| return std::nullopt; |
| } |
| |
| DataModel::ClusterEntry CodegenDataModelProvider::NextCluster(const ConcreteClusterPath & before) |
| { |
| // TODO: This search still seems slow (ember will loop). Should use index hints as long |
| // as ember API supports it |
| const EmberAfEndpointType * endpoint = emberAfFindEndpointType(before.mEndpointId); |
| |
| VerifyOrReturnValue(endpoint != nullptr, DataModel::ClusterEntry::kInvalid); |
| VerifyOrReturnValue(endpoint->clusterCount > 0, DataModel::ClusterEntry::kInvalid); |
| VerifyOrReturnValue(endpoint->cluster != nullptr, DataModel::ClusterEntry::kInvalid); |
| |
| std::optional<unsigned> cluster_idx = TryFindServerClusterIndex(endpoint, before.mClusterId); |
| if (!cluster_idx.has_value()) |
| { |
| return DataModel::ClusterEntry::kInvalid; |
| } |
| |
| return FirstServerClusterEntry(before.mEndpointId, endpoint, *cluster_idx + 1, mClusterIterationHint); |
| } |
| |
| std::optional<DataModel::ClusterInfo> CodegenDataModelProvider::GetClusterInfo(const ConcreteClusterPath & path) |
| { |
| const EmberAfCluster * cluster = FindServerCluster(path); |
| |
| VerifyOrReturnValue(cluster != nullptr, std::nullopt); |
| |
| auto info = LoadClusterInfo(path, *cluster); |
| |
| if (CHIP_ERROR * err = std::get_if<CHIP_ERROR>(&info)) |
| { |
| #if CHIP_ERROR_LOGGING |
| ChipLogError(AppServer, "Failed to load cluster info: %" CHIP_ERROR_FORMAT, err->Format()); |
| #else |
| (void) err->Format(); |
| #endif |
| return std::nullopt; |
| } |
| |
| return std::make_optional(std::get<DataModel::ClusterInfo>(info)); |
| } |
| |
| DataModel::AttributeEntry CodegenDataModelProvider::FirstAttribute(const ConcreteClusterPath & path) |
| { |
| const EmberAfCluster * cluster = FindServerCluster(path); |
| |
| VerifyOrReturnValue(cluster != nullptr, DataModel::AttributeEntry::kInvalid); |
| VerifyOrReturnValue(cluster->attributeCount > 0, DataModel::AttributeEntry::kInvalid); |
| VerifyOrReturnValue(cluster->attributes != nullptr, DataModel::AttributeEntry::kInvalid); |
| |
| mAttributeIterationHint = 0; |
| return AttributeEntryFrom(path, cluster->attributes[0]); |
| } |
| |
| std::optional<unsigned> CodegenDataModelProvider::TryFindAttributeIndex(const EmberAfCluster * cluster, AttributeId id) const |
| { |
| const unsigned attributeCount = cluster->attributeCount; |
| |
| // attempt to find this based on the embedded hint |
| if ((mAttributeIterationHint < attributeCount) && (cluster->attributes[mAttributeIterationHint].attributeId == id)) |
| { |
| return std::make_optional(mAttributeIterationHint); |
| } |
| |
| // linear search is required. This may be slow |
| for (unsigned attribute_idx = 0; attribute_idx < attributeCount; attribute_idx++) |
| { |
| |
| if (cluster->attributes[attribute_idx].attributeId == id) |
| { |
| return std::make_optional(attribute_idx); |
| } |
| } |
| |
| return std::nullopt; |
| } |
| |
| const EmberAfCluster * CodegenDataModelProvider::FindServerCluster(const ConcreteClusterPath & path) |
| { |
| // cache things |
| if (mPreviouslyFoundCluster.has_value() && (mPreviouslyFoundCluster->path == path)) |
| { |
| return mPreviouslyFoundCluster->cluster; |
| } |
| |
| const EmberAfCluster * cluster = emberAfFindServerCluster(path.mEndpointId, path.mClusterId); |
| if (cluster != nullptr) |
| { |
| mPreviouslyFoundCluster = std::make_optional<ClusterReference>(path, cluster); |
| } |
| return cluster; |
| } |
| |
| DataModel::AttributeEntry CodegenDataModelProvider::NextAttribute(const ConcreteAttributePath & before) |
| { |
| const EmberAfCluster * cluster = FindServerCluster(before); |
| VerifyOrReturnValue(cluster != nullptr, DataModel::AttributeEntry::kInvalid); |
| VerifyOrReturnValue(cluster->attributeCount > 0, DataModel::AttributeEntry::kInvalid); |
| VerifyOrReturnValue(cluster->attributes != nullptr, DataModel::AttributeEntry::kInvalid); |
| |
| // find the given attribute in the list and then return the next one |
| std::optional<unsigned> attribute_idx = TryFindAttributeIndex(cluster, before.mAttributeId); |
| if (!attribute_idx.has_value()) |
| { |
| return DataModel::AttributeEntry::kInvalid; |
| } |
| |
| unsigned next_idx = *attribute_idx + 1; |
| if (next_idx < cluster->attributeCount) |
| { |
| mAttributeIterationHint = next_idx; |
| return AttributeEntryFrom(before, cluster->attributes[next_idx]); |
| } |
| |
| // iteration complete |
| return DataModel::AttributeEntry::kInvalid; |
| } |
| |
| std::optional<DataModel::AttributeInfo> CodegenDataModelProvider::GetAttributeInfo(const ConcreteAttributePath & path) |
| { |
| const EmberAfCluster * cluster = FindServerCluster(path); |
| |
| VerifyOrReturnValue(cluster != nullptr, std::nullopt); |
| VerifyOrReturnValue(cluster->attributeCount > 0, std::nullopt); |
| VerifyOrReturnValue(cluster->attributes != nullptr, std::nullopt); |
| |
| std::optional<unsigned> attribute_idx = TryFindAttributeIndex(cluster, path.mAttributeId); |
| |
| if (!attribute_idx.has_value()) |
| { |
| return std::nullopt; |
| } |
| |
| DataModel::AttributeInfo info; |
| LoadAttributeInfo(path, cluster->attributes[*attribute_idx], &info); |
| return std::make_optional(info); |
| } |
| |
| DataModel::CommandEntry CodegenDataModelProvider::FirstAcceptedCommand(const ConcreteClusterPath & path) |
| { |
| const EmberAfCluster * cluster = FindServerCluster(path); |
| |
| VerifyOrReturnValue(cluster != nullptr, DataModel::CommandEntry::kInvalid); |
| |
| std::optional<CommandId> commandId = mAcceptedCommandsIterator.First(cluster->acceptedCommandList); |
| VerifyOrReturnValue(commandId.has_value(), DataModel::CommandEntry::kInvalid); |
| |
| return CommandEntryFrom(path, *commandId); |
| } |
| |
| DataModel::CommandEntry CodegenDataModelProvider::NextAcceptedCommand(const ConcreteCommandPath & before) |
| { |
| const EmberAfCluster * cluster = FindServerCluster(before); |
| |
| VerifyOrReturnValue(cluster != nullptr, DataModel::CommandEntry::kInvalid); |
| |
| std::optional<CommandId> commandId = mAcceptedCommandsIterator.Next(cluster->acceptedCommandList, before.mCommandId); |
| VerifyOrReturnValue(commandId.has_value(), DataModel::CommandEntry::kInvalid); |
| |
| return CommandEntryFrom(before, *commandId); |
| } |
| |
| std::optional<DataModel::CommandInfo> CodegenDataModelProvider::GetAcceptedCommandInfo(const ConcreteCommandPath & path) |
| { |
| const EmberAfCluster * cluster = FindServerCluster(path); |
| |
| VerifyOrReturnValue(cluster != nullptr, std::nullopt); |
| VerifyOrReturnValue(mAcceptedCommandsIterator.Exists(cluster->acceptedCommandList, path.mCommandId), std::nullopt); |
| |
| return CommandEntryFrom(path, path.mCommandId).info; |
| } |
| |
| ConcreteCommandPath CodegenDataModelProvider::FirstGeneratedCommand(const ConcreteClusterPath & path) |
| { |
| const EmberAfCluster * cluster = FindServerCluster(path); |
| |
| VerifyOrReturnValue(cluster != nullptr, kInvalidCommandPath); |
| |
| std::optional<CommandId> commandId = mGeneratedCommandsIterator.First(cluster->generatedCommandList); |
| VerifyOrReturnValue(commandId.has_value(), kInvalidCommandPath); |
| return ConcreteCommandPath(path.mEndpointId, path.mClusterId, *commandId); |
| } |
| |
| ConcreteCommandPath CodegenDataModelProvider::NextGeneratedCommand(const ConcreteCommandPath & before) |
| { |
| const EmberAfCluster * cluster = FindServerCluster(before); |
| |
| VerifyOrReturnValue(cluster != nullptr, kInvalidCommandPath); |
| |
| std::optional<CommandId> commandId = mGeneratedCommandsIterator.Next(cluster->generatedCommandList, before.mCommandId); |
| VerifyOrReturnValue(commandId.has_value(), kInvalidCommandPath); |
| |
| return ConcreteCommandPath(before.mEndpointId, before.mClusterId, *commandId); |
| } |
| |
| } // namespace app |
| } // namespace chip |