Skip to content

Commit

Permalink
Add a way to run custom cluster logic for attribute write (#11520)
Browse files Browse the repository at this point in the history
* Add a way to run custom cluster logic for attribute write

* Address review comments
  • Loading branch information
yufengwangca authored and pull[bot] committed Nov 13, 2023
1 parent b7d9f2b commit 35b5d57
Show file tree
Hide file tree
Showing 7 changed files with 675 additions and 437 deletions.
34 changes: 34 additions & 0 deletions src/app/AttributeAccessInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include <app/ConcreteAttributePath.h>
#include <app/MessageDef/AttributeDataIB.h>
#include <app/data-model/Decode.h>
#include <app/data-model/Encode.h>
#include <app/data-model/List.h> // So we can encode lists
#include <app/data-model/TagBoundEncoder.h>
Expand Down Expand Up @@ -101,6 +102,25 @@ class AttributeValueEncoder : protected TagBoundEncoder
const FabricIndex mAccessingFabricIndex;
};

class AttributeValueDecoder
{
public:
AttributeValueDecoder(TLV::TLVReader & aReader) : mReader(aReader) {}

template <typename T>
CHIP_ERROR Decode(T & aArg)
{
mTriedDecode = true;
return DataModel::Decode(mReader, aArg);
}

bool TriedDecode() const { return mTriedDecode; }

private:
TLV::TLVReader & mReader;
bool mTriedDecode = false;
};

class AttributeAccessInterface
{
public:
Expand All @@ -127,6 +147,20 @@ class AttributeAccessInterface
*/
virtual CHIP_ERROR Read(const ConcreteAttributePath & aPath, AttributeValueEncoder & aEncoder) = 0;

/**
* Callback for writing attributes.
*
* @param [in] aPath indicates which exact data is being written.
* @param [in] aDecoder the AttributeValueDecoder to use for decoding the
* data. If this function returns scucess and no attempt is
* made to decode data using aDecoder, the
* AttributeAccessInterface did not try to write any data. In
* this case, normal attribute access will happen for the write.
* This may involve writing to the attribute store or external
* attribute callbacks.
*/
virtual CHIP_ERROR Write(const ConcreteAttributePath & aPath, AttributeValueDecoder & aDecoder) { return CHIP_NO_ERROR; }

/**
* Mechanism for keeping track of a chain of AttributeAccessInterfaces.
*/
Expand Down
154 changes: 136 additions & 18 deletions src/app/clusters/test-cluster-server/test-cluster-server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,51 @@ using namespace chip::app::Clusters::TestCluster;
using namespace chip::app::Clusters::TestCluster::Commands;
using namespace chip::app::Clusters::TestCluster::Attributes;

// The number of elements in the test attribute list
constexpr uint8_t kAttributeListLength = 4;

// The maximum length of the test attribute list element in bytes
constexpr uint8_t kAttributeEntryLength = 6;

namespace {

class OctetStringData
{
public:
uint8_t * Data() { return mDataBuf; }
size_t Length() const { return mDataLen; }
void SetLength(size_t size) { mDataLen = size; }

private:
uint8_t mDataBuf[kAttributeEntryLength];
size_t mDataLen = 0;
};

class TestAttrAccess : public AttributeAccessInterface
{
public:
// Register for the Test Cluster cluster on all endpoints.
TestAttrAccess() : AttributeAccessInterface(Optional<EndpointId>::Missing(), TestCluster::Id) {}

CHIP_ERROR Read(const ConcreteAttributePath & aPath, AttributeValueEncoder & aEncoder) override;
CHIP_ERROR Write(const ConcreteAttributePath & aPath, AttributeValueDecoder & aDecoder) override;

private:
CHIP_ERROR ReadListInt8uAttribute(AttributeValueEncoder & aEncoder);
CHIP_ERROR WriteListInt8uAttribute(AttributeValueDecoder & aDecoder);
CHIP_ERROR ReadListOctetStringAttribute(AttributeValueEncoder & aEncoder);
CHIP_ERROR WriteListOctetStringAttribute(AttributeValueDecoder & aDecoder);
CHIP_ERROR ReadListStructOctetStringAttribute(AttributeValueEncoder & aEncoder);
CHIP_ERROR WriteListStructOctetStringAttribute(AttributeValueDecoder & aDecoder);
CHIP_ERROR ReadListNullablesAndOptionalsStructAttribute(AttributeValueEncoder & aEncoder);
CHIP_ERROR WriteListNullablesAndOptionalsStructAttribute(AttributeValueDecoder & aDecoder);
};

TestAttrAccess gAttrAccess;
uint8_t gListUint8Data[kAttributeListLength];
OctetStringData gListOctetStringData[kAttributeListLength];
OctetStringData gListOperationalCert[kAttributeListLength];
Structs::TestListStructOctet::Type listStructOctetStringData[kAttributeListLength];

CHIP_ERROR TestAttrAccess::Read(const ConcreteAttributePath & aPath, AttributeValueEncoder & aEncoder)
{
Expand All @@ -83,54 +110,140 @@ CHIP_ERROR TestAttrAccess::Read(const ConcreteAttributePath & aPath, AttributeVa
return CHIP_NO_ERROR;
}

CHIP_ERROR TestAttrAccess::Write(const ConcreteAttributePath & aPath, AttributeValueDecoder & aDecoder)
{
switch (aPath.mAttributeId)
{
case ListInt8u::Id: {
return WriteListInt8uAttribute(aDecoder);
}
case ListOctetString::Id: {
return WriteListOctetStringAttribute(aDecoder);
}
case ListStructOctetString::Id: {
return WriteListStructOctetStringAttribute(aDecoder);
}
case ListNullablesAndOptionalsStruct::Id: {
return WriteListNullablesAndOptionalsStructAttribute(aDecoder);
}
default: {
break;
}
}

return CHIP_NO_ERROR;
}

CHIP_ERROR TestAttrAccess::ReadListInt8uAttribute(AttributeValueEncoder & aEncoder)
{
return aEncoder.EncodeList([](const TagBoundEncoder & encoder) -> CHIP_ERROR {
constexpr uint8_t maxValue = 4;
for (uint8_t value = 1; value <= maxValue; value++)
for (uint8_t index = 0; index < kAttributeListLength; index++)
{
ReturnErrorOnFailure(encoder.Encode(value));
ReturnErrorOnFailure(encoder.Encode(gListUint8Data[index]));
}
return CHIP_NO_ERROR;
});
}

CHIP_ERROR TestAttrAccess::WriteListInt8uAttribute(AttributeValueDecoder & aDecoder)
{
ListInt8u::TypeInfo::DecodableType list;

ReturnErrorOnFailure(aDecoder.Decode(list));

uint8_t index = 0;
auto iter = list.begin();
while (iter.Next())
{
auto & entry = iter.GetValue();

VerifyOrReturnError(index < kAttributeListLength, CHIP_ERROR_BUFFER_TOO_SMALL);
gListUint8Data[index++] = entry;
}

return iter.GetStatus();
}

CHIP_ERROR TestAttrAccess::ReadListOctetStringAttribute(AttributeValueEncoder & aEncoder)
{
return aEncoder.EncodeList([](const TagBoundEncoder & encoder) -> CHIP_ERROR {
constexpr uint16_t attributeCount = 4;
char data[6] = { 'T', 'e', 's', 't', 'N', '\0' };

for (uint8_t index = 0; index < attributeCount; index++)
for (uint8_t index = 0; index < kAttributeListLength; index++)
{
snprintf(data + strlen(data) - 1, 2, "%d", index);
ByteSpan span(Uint8::from_char(data), strlen(data));
ByteSpan span(gListOctetStringData[index].Data(), gListOctetStringData[index].Length());
ReturnErrorOnFailure(encoder.Encode(span));
}
return CHIP_NO_ERROR;
});
}

CHIP_ERROR TestAttrAccess::WriteListOctetStringAttribute(AttributeValueDecoder & aDecoder)
{
ListOctetString::TypeInfo::DecodableType list;

ReturnErrorOnFailure(aDecoder.Decode(list));

uint8_t index = 0;
auto iter = list.begin();
while (iter.Next())
{
const auto & entry = iter.GetValue();

VerifyOrReturnError(index < kAttributeListLength, CHIP_ERROR_BUFFER_TOO_SMALL);
VerifyOrReturnError(entry.size() <= kAttributeEntryLength, CHIP_ERROR_BUFFER_TOO_SMALL);
memcpy(gListOctetStringData[index].Data(), entry.data(), entry.size());
gListOctetStringData[index].SetLength(entry.size());
index++;
}

return iter.GetStatus();
}

CHIP_ERROR TestAttrAccess::ReadListStructOctetStringAttribute(AttributeValueEncoder & aEncoder)
{
return aEncoder.EncodeList([](const TagBoundEncoder & encoder) -> CHIP_ERROR {
constexpr uint16_t attributeCount = 4;
char data[6] = { 'T', 'e', 's', 't', 'N', '\0' };

for (uint8_t index = 0; index < attributeCount; index++)
for (uint8_t index = 0; index < kAttributeListLength; index++)
{
snprintf(data + strlen(data) - 1, 2, "%d", index);
ByteSpan span(Uint8::from_char(data), strlen(data));

Structs::TestListStructOctet::Type structOctet;
structOctet.fabricIndex = index;
structOctet.operationalCert = span;
structOctet.fabricIndex = listStructOctetStringData[index].fabricIndex;
structOctet.operationalCert = listStructOctetStringData[index].operationalCert;
ReturnErrorOnFailure(encoder.Encode(structOctet));
}

return CHIP_NO_ERROR;
});
}

CHIP_ERROR TestAttrAccess::WriteListStructOctetStringAttribute(AttributeValueDecoder & aDecoder)
{
ListStructOctetString::TypeInfo::DecodableType list;

ReturnErrorOnFailure(aDecoder.Decode(list));

uint8_t index = 0;
auto iter = list.begin();
while (iter.Next())
{
const auto & entry = iter.GetValue();

VerifyOrReturnError(index < kAttributeListLength, CHIP_ERROR_BUFFER_TOO_SMALL);
VerifyOrReturnError(entry.operationalCert.size() <= kAttributeEntryLength, CHIP_ERROR_BUFFER_TOO_SMALL);
memcpy(gListOperationalCert[index].Data(), entry.operationalCert.data(), entry.operationalCert.size());
gListOperationalCert[index].SetLength(entry.operationalCert.size());

listStructOctetStringData[index].fabricIndex = entry.fabricIndex;
listStructOctetStringData[index].operationalCert =
ByteSpan(gListOperationalCert[index].Data(), gListOperationalCert[index].Length());
index++;
}

if (iter.GetStatus() != CHIP_NO_ERROR)
{
return CHIP_ERROR_INVALID_DATA_LIST;
}

return CHIP_NO_ERROR;
}

CHIP_ERROR TestAttrAccess::ReadListNullablesAndOptionalsStructAttribute(AttributeValueEncoder & aEncoder)
{
return aEncoder.EncodeList([](const TagBoundEncoder & encoder) -> CHIP_ERROR {
Expand All @@ -142,6 +255,11 @@ CHIP_ERROR TestAttrAccess::ReadListNullablesAndOptionalsStructAttribute(Attribut
});
}

CHIP_ERROR TestAttrAccess::WriteListNullablesAndOptionalsStructAttribute(AttributeValueDecoder & aDecoder)
{
// TODO Add yaml test case for NullablesAndOptionalsStruct list
return CHIP_NO_ERROR;
}
} // namespace

bool emberAfTestClusterClusterTestCallback(app::CommandHandler *, const app::ConcreteCommandPath & commandPath,
Expand Down
30 changes: 0 additions & 30 deletions src/app/tests/suites/TestCluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -732,36 +732,6 @@ tests:
arguments:
value: ""

# Tests for List attribute

- label: "Read attribute LIST"
command: "readAttribute"
attribute: "list_int8u"
response:
value: [1, 2, 3, 4]

# Tests for List Octet String attribute

- label: "Read attribute LIST_OCTET_STRING"
command: "readAttribute"
attribute: "list_octet_string"
response:
value: ["Test0", "Test1", "Test2", "Test3"]

# Tests for List Struct Octet String attribute

- label: "Read attribute LIST_STRUCT_OCTET_STRING"
command: "readAttribute"
attribute: "list_struct_octet_string"
response:
value:
[
{ fabricIndex: 0, operationalCert: "Test0" },
{ fabricIndex: 1, operationalCert: "Test1" },
{ fabricIndex: 2, operationalCert: "Test2" },
{ fabricIndex: 3, operationalCert: "Test3" },
]

# Tests for Epoch Microseconds

- label: "Read attribute EPOCH_US Default Value"
Expand Down
49 changes: 49 additions & 0 deletions src/app/tests/suites/TestClusterComplexTypes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,55 @@ tests:
- name: "value"
value: false

- label:
"Write attribute LIST With List of INT8U and none of them is set to 0"
command: "writeAttribute"
attribute: "list_int8u"
arguments:
value: [1, 2, 3, 4]

- label: "Read attribute LIST With List of INT8U"
command: "readAttribute"
attribute: "list_int8u"
response:
value: [1, 2, 3, 4]

- label: "Write attribute LIST With List of OCTET_STRING"
command: "writeAttribute"
attribute: "list_octet_string"
arguments:
value: ["Test0", "Test1", "Test2", "Test3"]

- label: "Read attribute LIST With List of OCTET_STRING"
command: "readAttribute"
attribute: "list_octet_string"
response:
value: ["Test0", "Test1", "Test2", "Test3"]

- label: "Write attribute LIST With List of LIST_STRUCT_OCTET_STRING"
command: "writeAttribute"
attribute: "list_struct_octet_string"
arguments:
value:
[
{ fabricIndex: 0, operationalCert: "Test0" },
{ fabricIndex: 1, operationalCert: "Test1" },
{ fabricIndex: 2, operationalCert: "Test2" },
{ fabricIndex: 3, operationalCert: "Test3" },
]

- label: "Read attribute LIST With List of LIST_STRUCT_OCTET_STRING"
command: "readAttribute"
attribute: "list_struct_octet_string"
response:
value:
[
{ fabricIndex: 0, operationalCert: "Test0" },
{ fabricIndex: 1, operationalCert: "Test1" },
{ fabricIndex: 2, operationalCert: "Test2" },
{ fabricIndex: 3, operationalCert: "Test3" },
]

# Tests for Nullables and Optionals

- label: "Send Test Command with optional arg set."
Expand Down
16 changes: 16 additions & 0 deletions src/app/util/ember-compatibility-functions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,25 @@ CHIP_ERROR WriteSingleClusterData(ClusterInfo & aClusterInfo, TLV::TLVReader & a
attributePathParams.mClusterId = aClusterInfo.mClusterId;
attributePathParams.mAttributeId = aClusterInfo.mAttributeId;

// TODO: Refactor WriteSingleClusterData and all dependent functions to take ConcreteAttributePath instead of ClusterInfo
// as the input argument.
AttributeAccessInterface * attrOverride = findAttributeAccessOverride(aClusterInfo.mEndpointId, aClusterInfo.mClusterId);
if (attrOverride != nullptr)
{
ConcreteAttributePath path(aClusterInfo.mEndpointId, aClusterInfo.mClusterId, aClusterInfo.mAttributeId);
AttributeValueDecoder valueDecoder(aReader);
ReturnErrorOnFailure(attrOverride->Write(path, valueDecoder));

if (valueDecoder.TriedDecode())
{
return apWriteHandler->AddStatus(attributePathParams, Protocols::InteractionModel::Status::Success);
}
}

auto imCode = WriteSingleClusterDataInternal(aClusterInfo, aReader, apWriteHandler);
return apWriteHandler->AddStatus(attributePathParams, imCode);
}

} // namespace app
} // namespace chip

Expand Down
Loading

0 comments on commit 35b5d57

Please sign in to comment.