Skip to content

Commit 0563483

Browse files
andy31415andreilitvinrestyled-commitsbzbarsky-appletehampson
authored andcommitted
Add support for using CommandHandlerInterface for accepted/generated command listing (project-chip#35753)
* Add support for using CommandHandlerInterface for accepted/generated commands. Note that iteration is still O(N^2) which is not ideal, however at least the use of this is infrequent and list of commands is somewhat short. * Fix includes * Comment update * Comment update * Fix includes * comments * Restyled by clang-format * Update src/app/codegen-data-model-provider/CodegenDataModelProvider.cpp Co-authored-by: Boris Zbarsky <[email protected]> * Update src/app/codegen-data-model-provider/CodegenDataModelProvider.cpp Co-authored-by: Terence Hampson <[email protected]> * Make use of FindExact operation * Use CommandEntryFrom * Fix naming * Added TODO on slow next iteration * Update code flow to hopefully make it somewhat easier to read * Remove extra include * Make code even more readable I hope * Comment update * Fix includes * Restyled by clang-format * Update src/app/codegen-data-model-provider/CodegenDataModelProvider.cpp Co-authored-by: Boris Zbarsky <[email protected]> * Restyled by clang-format --------- Co-authored-by: Andrei Litvin <[email protected]> Co-authored-by: Restyled.io <[email protected]> Co-authored-by: Boris Zbarsky <[email protected]> Co-authored-by: Terence Hampson <[email protected]>
1 parent 02008bc commit 0563483

File tree

2 files changed

+289
-4
lines changed

2 files changed

+289
-4
lines changed

src/app/codegen-data-model-provider/CodegenDataModelProvider.cpp

+175-2
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
#include <app/codegen-data-model-provider/CodegenDataModelProvider.h>
1818

1919
#include <app-common/zap-generated/attribute-type.h>
20+
#include <app/CommandHandlerInterface.h>
2021
#include <app/CommandHandlerInterfaceRegistry.h>
22+
#include <app/ConcreteClusterPath.h>
23+
#include <app/ConcreteCommandPath.h>
2124
#include <app/RequiredPrivilege.h>
25+
#include <app/data-model-provider/MetadataTypes.h>
2226
#include <app/util/IMClusterCommandHandler.h>
2327
#include <app/util/attribute-storage.h>
2428
#include <app/util/endpoint-config-api.h>
29+
#include <lib/core/CHIPError.h>
2530
#include <lib/core/DataModelTypes.h>
2631

2732
#include <optional>
@@ -31,6 +36,113 @@ namespace chip {
3136
namespace app {
3237
namespace {
3338

39+
/// Handles going through callback-based enumeration of generated/accepted commands
40+
/// for CommandHandlerInterface based items.
41+
///
42+
/// Offers the ability to focus on some operation for finding a given
43+
/// command id:
44+
/// - FindFirst will return the first found element
45+
/// - FindExact finds the element with the given id
46+
/// - FindNext finds the element following the given id
47+
class EnumeratorCommandFinder
48+
{
49+
public:
50+
using HandlerCallbackFunction = CHIP_ERROR (CommandHandlerInterface::*)(const ConcreteClusterPath &,
51+
CommandHandlerInterface::CommandIdCallback, void *);
52+
53+
enum class Operation
54+
{
55+
kFindFirst, // Find the first value in the list
56+
kFindExact, // Find the given value
57+
kFindNext // Find the value AFTER this value
58+
};
59+
60+
EnumeratorCommandFinder(HandlerCallbackFunction callback) :
61+
mCallback(callback), mOperation(Operation::kFindFirst), mTarget(kInvalidCommandId)
62+
{}
63+
64+
/// Find the given command ID that matches the given operation/path.
65+
///
66+
/// If operation is kFindFirst, then path commandID is ignored. Otherwise it is used as a key to
67+
/// kFindExact or kFindNext.
68+
///
69+
/// Returns:
70+
/// - std::nullopt if no command found using the command handler interface
71+
/// - kInvalidCommandId if the find failed (but command handler interface does provide a list)
72+
/// - valid id if command handler interface usage succeeds
73+
std::optional<CommandId> FindCommandId(Operation operation, const ConcreteCommandPath & path);
74+
75+
/// Uses FindCommandId to find the given command and loads the command entry data
76+
std::optional<DataModel::CommandEntry> FindCommandEntry(Operation operation, const ConcreteCommandPath & path);
77+
78+
private:
79+
HandlerCallbackFunction mCallback;
80+
Operation mOperation;
81+
CommandId mTarget;
82+
std::optional<CommandId> mFound = std::nullopt;
83+
84+
Loop HandlerCallback(CommandId id)
85+
{
86+
switch (mOperation)
87+
{
88+
case Operation::kFindFirst:
89+
mFound = id;
90+
return Loop::Break;
91+
case Operation::kFindExact:
92+
if (mTarget == id)
93+
{
94+
mFound = id; // found it
95+
return Loop::Break;
96+
}
97+
break;
98+
case Operation::kFindNext:
99+
if (mTarget == id)
100+
{
101+
// Once we found the ID, get the first
102+
mOperation = Operation::kFindFirst;
103+
}
104+
break;
105+
}
106+
return Loop::Continue; // keep searching
107+
}
108+
109+
static Loop HandlerCallbackFn(CommandId id, void * context)
110+
{
111+
auto self = static_cast<EnumeratorCommandFinder *>(context);
112+
return self->HandlerCallback(id);
113+
}
114+
};
115+
116+
std::optional<CommandId> EnumeratorCommandFinder::FindCommandId(Operation operation, const ConcreteCommandPath & path)
117+
{
118+
mOperation = operation;
119+
mTarget = path.mCommandId;
120+
121+
CommandHandlerInterface * interface =
122+
CommandHandlerInterfaceRegistry::Instance().GetCommandHandler(path.mEndpointId, path.mClusterId);
123+
124+
if (interface == nullptr)
125+
{
126+
return std::nullopt; // no data: no interface
127+
}
128+
129+
CHIP_ERROR err = (interface->*mCallback)(path, HandlerCallbackFn, this);
130+
if (err == CHIP_ERROR_NOT_IMPLEMENTED)
131+
{
132+
return std::nullopt; // no data provided by the interface
133+
}
134+
135+
if (err != CHIP_NO_ERROR)
136+
{
137+
// Report the error here since we lose actual error. This generally should NOT be possible as CommandHandlerInterface
138+
// usually returns unimplemented or should just work for our use case (our callback never fails)
139+
ChipLogError(DataManagement, "Enumerate error: %" CHIP_ERROR_FORMAT, err.Format());
140+
return kInvalidCommandId;
141+
}
142+
143+
return mFound.value_or(kInvalidCommandId);
144+
}
145+
34146
/// Load the cluster information into the specified destination
35147
std::variant<CHIP_ERROR, DataModel::ClusterInfo> LoadClusterInfo(const ConcreteClusterPath & path, const EmberAfCluster & cluster)
36148
{
@@ -160,6 +272,20 @@ DataModel::CommandEntry CommandEntryFrom(const ConcreteClusterPath & clusterPath
160272
return entry;
161273
}
162274

275+
std::optional<DataModel::CommandEntry> EnumeratorCommandFinder::FindCommandEntry(Operation operation,
276+
const ConcreteCommandPath & path)
277+
{
278+
279+
std::optional<CommandId> id = FindCommandId(operation, path);
280+
281+
if (!id.has_value())
282+
{
283+
return std::nullopt;
284+
}
285+
286+
return (*id == kInvalidCommandId) ? DataModel::CommandEntry::kInvalid : CommandEntryFrom(path, *id);
287+
}
288+
163289
const ConcreteCommandPath kInvalidCommandPath(kInvalidEndpointId, kInvalidClusterId, kInvalidCommandId);
164290

165291
} // namespace
@@ -492,6 +618,15 @@ std::optional<DataModel::AttributeInfo> CodegenDataModelProvider::GetAttributeIn
492618

493619
DataModel::CommandEntry CodegenDataModelProvider::FirstAcceptedCommand(const ConcreteClusterPath & path)
494620
{
621+
auto handlerInterfaceValue = EnumeratorCommandFinder(&CommandHandlerInterface::EnumerateAcceptedCommands)
622+
.FindCommandEntry(EnumeratorCommandFinder::Operation::kFindFirst,
623+
ConcreteCommandPath(path.mEndpointId, path.mClusterId, kInvalidCommandId));
624+
625+
if (handlerInterfaceValue.has_value())
626+
{
627+
return *handlerInterfaceValue;
628+
}
629+
495630
const EmberAfCluster * cluster = FindServerCluster(path);
496631

497632
VerifyOrReturnValue(cluster != nullptr, DataModel::CommandEntry::kInvalid);
@@ -504,6 +639,16 @@ DataModel::CommandEntry CodegenDataModelProvider::FirstAcceptedCommand(const Con
504639

505640
DataModel::CommandEntry CodegenDataModelProvider::NextAcceptedCommand(const ConcreteCommandPath & before)
506641
{
642+
// TODO: `Next` redirecting to a callback is slow O(n^2).
643+
// see https://github.com/project-chip/connectedhomeip/issues/35790
644+
auto handlerInterfaceValue = EnumeratorCommandFinder(&CommandHandlerInterface::EnumerateAcceptedCommands)
645+
.FindCommandEntry(EnumeratorCommandFinder::Operation::kFindNext, before);
646+
647+
if (handlerInterfaceValue.has_value())
648+
{
649+
return *handlerInterfaceValue;
650+
}
651+
507652
const EmberAfCluster * cluster = FindServerCluster(before);
508653

509654
VerifyOrReturnValue(cluster != nullptr, DataModel::CommandEntry::kInvalid);
@@ -516,6 +661,14 @@ DataModel::CommandEntry CodegenDataModelProvider::NextAcceptedCommand(const Conc
516661

517662
std::optional<DataModel::CommandInfo> CodegenDataModelProvider::GetAcceptedCommandInfo(const ConcreteCommandPath & path)
518663
{
664+
auto handlerInterfaceValue = EnumeratorCommandFinder(&CommandHandlerInterface::EnumerateAcceptedCommands)
665+
.FindCommandEntry(EnumeratorCommandFinder::Operation::kFindExact, path);
666+
667+
if (handlerInterfaceValue.has_value())
668+
{
669+
return handlerInterfaceValue->IsValid() ? std::make_optional(handlerInterfaceValue->info) : std::nullopt;
670+
}
671+
519672
const EmberAfCluster * cluster = FindServerCluster(path);
520673

521674
VerifyOrReturnValue(cluster != nullptr, std::nullopt);
@@ -526,17 +679,37 @@ std::optional<DataModel::CommandInfo> CodegenDataModelProvider::GetAcceptedComma
526679

527680
ConcreteCommandPath CodegenDataModelProvider::FirstGeneratedCommand(const ConcreteClusterPath & path)
528681
{
529-
const EmberAfCluster * cluster = FindServerCluster(path);
682+
std::optional<CommandId> commandId =
683+
EnumeratorCommandFinder(&CommandHandlerInterface::EnumerateGeneratedCommands)
684+
.FindCommandId(EnumeratorCommandFinder::Operation::kFindFirst,
685+
ConcreteCommandPath(path.mEndpointId, path.mClusterId, kInvalidCommandId));
686+
if (commandId.has_value())
687+
{
688+
return *commandId == kInvalidCommandId ? kInvalidCommandPath
689+
: ConcreteCommandPath(path.mEndpointId, path.mClusterId, *commandId);
690+
}
530691

692+
const EmberAfCluster * cluster = FindServerCluster(path);
531693
VerifyOrReturnValue(cluster != nullptr, kInvalidCommandPath);
532694

533-
std::optional<CommandId> commandId = mGeneratedCommandsIterator.First(cluster->generatedCommandList);
695+
commandId = mGeneratedCommandsIterator.First(cluster->generatedCommandList);
534696
VerifyOrReturnValue(commandId.has_value(), kInvalidCommandPath);
535697
return ConcreteCommandPath(path.mEndpointId, path.mClusterId, *commandId);
536698
}
537699

538700
ConcreteCommandPath CodegenDataModelProvider::NextGeneratedCommand(const ConcreteCommandPath & before)
539701
{
702+
// TODO: `Next` redirecting to a callback is slow O(n^2).
703+
// see https://github.com/project-chip/connectedhomeip/issues/35790
704+
auto nextId = EnumeratorCommandFinder(&CommandHandlerInterface::EnumerateGeneratedCommands)
705+
.FindCommandId(EnumeratorCommandFinder::Operation::kFindNext, before);
706+
707+
if (nextId.has_value())
708+
{
709+
return (*nextId == kInvalidCommandId) ? kInvalidCommandPath
710+
: ConcreteCommandPath(before.mEndpointId, before.mClusterId, *nextId);
711+
}
712+
540713
const EmberAfCluster * cluster = FindServerCluster(before);
541714

542715
VerifyOrReturnValue(cluster != nullptr, kInvalidCommandPath);

src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp

+114-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
* limitations under the License.
1616
*/
1717

18-
#include <vector>
19-
2018
#include <pw_unit_test/framework.h>
2119

2220
#include <app/codegen-data-model-provider/tests/EmberInvokeOverride.h>
@@ -30,11 +28,14 @@
3028
#include <app/AttributeAccessInterfaceRegistry.h>
3129
#include <app/AttributeEncodeState.h>
3230
#include <app/AttributeValueDecoder.h>
31+
#include <app/CommandHandlerInterface.h>
32+
#include <app/CommandHandlerInterfaceRegistry.h>
3333
#include <app/ConcreteAttributePath.h>
3434
#include <app/ConcreteCommandPath.h>
3535
#include <app/GlobalAttributes.h>
3636
#include <app/MessageDef/ReportDataMessage.h>
3737
#include <app/codegen-data-model-provider/CodegenDataModelProvider.h>
38+
#include <app/data-model-provider/MetadataTypes.h>
3839
#include <app/data-model-provider/OperationTypes.h>
3940
#include <app/data-model-provider/StringBuilderAdapters.h>
4041
#include <app/data-model-provider/tests/ReadTesting.h>
@@ -62,6 +63,8 @@
6263
#include <lib/support/Span.h>
6364
#include <protocols/interaction_model/StatusCode.h>
6465

66+
#include <vector>
67+
6568
using namespace chip;
6669
using namespace chip::Test;
6770
using namespace chip::app;
@@ -183,6 +186,61 @@ class MockAccessControl : public Access::AccessControl::Delegate, public Access:
183186
bool IsDeviceTypeOnEndpoint(DeviceTypeId deviceType, EndpointId endpoint) override { return true; }
184187
};
185188

189+
/// Overrides Enumerate*Commands in the CommandHandlerInterface to allow
190+
/// testing of behaviors when command enumeration is done in the interace.
191+
class CustomListCommandHandler : public CommandHandlerInterface
192+
{
193+
public:
194+
CustomListCommandHandler(Optional<EndpointId> endpointId, ClusterId clusterId) : CommandHandlerInterface(endpointId, clusterId)
195+
{
196+
CommandHandlerInterfaceRegistry::Instance().RegisterCommandHandler(this);
197+
}
198+
~CustomListCommandHandler() { CommandHandlerInterfaceRegistry::Instance().UnregisterCommandHandler(this); }
199+
200+
void InvokeCommand(HandlerContext & handlerContext) override { handlerContext.SetCommandNotHandled(); }
201+
202+
CHIP_ERROR EnumerateAcceptedCommands(const ConcreteClusterPath & cluster, CommandIdCallback callback, void * context) override
203+
{
204+
VerifyOrReturnError(mOverrideAccepted, CHIP_ERROR_NOT_IMPLEMENTED);
205+
206+
for (auto id : mAccepted)
207+
{
208+
if (callback(id, context) != Loop::Continue)
209+
{
210+
break;
211+
}
212+
}
213+
return CHIP_NO_ERROR;
214+
}
215+
216+
CHIP_ERROR EnumerateGeneratedCommands(const ConcreteClusterPath & cluster, CommandIdCallback callback, void * context) override
217+
{
218+
VerifyOrReturnError(mOverrideGenerated, CHIP_ERROR_NOT_IMPLEMENTED);
219+
220+
for (auto id : mGenerated)
221+
{
222+
if (callback(id, context) != Loop::Continue)
223+
{
224+
break;
225+
}
226+
}
227+
return CHIP_NO_ERROR;
228+
}
229+
230+
void SetOverrideAccepted(bool o) { mOverrideAccepted = o; }
231+
void SetOverrideGenerated(bool o) { mOverrideGenerated = o; }
232+
233+
std::vector<CommandId> & AcceptedVec() { return mAccepted; }
234+
std::vector<CommandId> & GeneratedVec() { return mGenerated; }
235+
236+
private:
237+
bool mOverrideAccepted = false;
238+
bool mOverrideGenerated = false;
239+
240+
std::vector<CommandId> mAccepted;
241+
std::vector<CommandId> mGenerated;
242+
};
243+
186244
class ScopedMockAccessControl
187245
{
188246
public:
@@ -1193,6 +1251,60 @@ TEST(TestCodegenModelViaMocks, IterateOverGeneratedCommands)
11931251
}
11941252
}
11951253

1254+
TEST(TestCodegenModelViaMocks, CommandHandlerInterfaceAcceptedCommands)
1255+
{
1256+
1257+
UseMockNodeConfig config(gTestNodeConfig);
1258+
CodegenDataModelProviderWithContext model;
1259+
1260+
// Command handler interface is capable to override accepted and generated commands.
1261+
// Validate that these work
1262+
CustomListCommandHandler handler(MakeOptional(kMockEndpoint1), MockClusterId(1));
1263+
1264+
// At this point, without overrides, there should be no accepted/generated commands
1265+
EXPECT_FALSE(model.FirstAcceptedCommand(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1))).IsValid());
1266+
EXPECT_FALSE(model.FirstGeneratedCommand(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1))).HasValidIds());
1267+
EXPECT_FALSE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint1, MockClusterId(1), 1234)).has_value());
1268+
1269+
handler.SetOverrideAccepted(true);
1270+
handler.SetOverrideGenerated(true);
1271+
1272+
// with overrides, the list is still empty ...
1273+
EXPECT_FALSE(model.FirstAcceptedCommand(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1))).IsValid());
1274+
EXPECT_FALSE(model.FirstGeneratedCommand(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1))).HasValidIds());
1275+
EXPECT_FALSE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint1, MockClusterId(1), 1234)).has_value());
1276+
1277+
// set some overrides
1278+
handler.AcceptedVec().push_back(1234);
1279+
handler.AcceptedVec().push_back(999);
1280+
1281+
handler.GeneratedVec().push_back(33);
1282+
1283+
DataModel::CommandEntry entry;
1284+
1285+
entry = model.FirstAcceptedCommand(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)));
1286+
EXPECT_TRUE(entry.IsValid());
1287+
EXPECT_EQ(entry.path, ConcreteCommandPath(kMockEndpoint1, MockClusterId(1), 1234));
1288+
1289+
entry = model.NextAcceptedCommand(entry.path);
1290+
EXPECT_TRUE(entry.IsValid());
1291+
EXPECT_EQ(entry.path, ConcreteCommandPath(kMockEndpoint1, MockClusterId(1), 999));
1292+
1293+
entry = model.NextAcceptedCommand(entry.path);
1294+
EXPECT_FALSE(entry.IsValid());
1295+
1296+
ConcreteCommandPath path = model.FirstGeneratedCommand(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)));
1297+
EXPECT_TRUE(path.HasValidIds());
1298+
EXPECT_EQ(path, ConcreteCommandPath(kMockEndpoint1, MockClusterId(1), 33));
1299+
path = model.NextGeneratedCommand(path);
1300+
EXPECT_FALSE(path.HasValidIds());
1301+
1302+
// Command finding should work
1303+
EXPECT_TRUE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint1, MockClusterId(1), 1234)).has_value());
1304+
EXPECT_FALSE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint1, MockClusterId(1), 88)).has_value());
1305+
EXPECT_FALSE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint1, MockClusterId(1), 33)).has_value());
1306+
}
1307+
11961308
TEST(TestCodegenModelViaMocks, EmberAttributeReadAclDeny)
11971309
{
11981310
UseMockNodeConfig config(gTestNodeConfig);

0 commit comments

Comments
 (0)