From 44afdd2d7206a3d4689fb2d73a67cc30ac2ac4bf Mon Sep 17 00:00:00 2001 From: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> Date: Sat, 3 Aug 2024 11:24:35 +1200 Subject: [PATCH] Get plumbing in place for TBRM cluster certification tests (#34696) * Add TBRM cluster to network-manager-app and add TC_TBRM_2_1 script Also fix some GN dependency issues. The TBRM delegate is currently a crude mock. * zap_regen_all --- examples/network-manager-app/linux/BUILD.gn | 1 + examples/network-manager-app/linux/tbrm.cpp | 131 ++++++++++ .../network-manager-app.matter | 62 +++++ .../network-manager-app.zap | 223 ++++++++++++++++++ scripts/tests/chiptest/__init__.py | 2 +- .../thread-border-router-management-server.h | 6 +- .../DefaultThreadNetworkDirectoryStorage.cpp | 3 +- src/app/tests/BUILD.gn | 6 +- .../certification/Test_TC_TBRM_2_1.yaml | 83 +++++++ .../zcl/data-model/chip/matter-devices.xml | 3 +- ...hread-border-router-management-cluster.xml | 2 +- .../zap-generated/MTRDeviceTypeMetadata.mm | 2 +- 12 files changed, 513 insertions(+), 11 deletions(-) create mode 100644 examples/network-manager-app/linux/tbrm.cpp create mode 100644 src/app/tests/suites/certification/Test_TC_TBRM_2_1.yaml diff --git a/examples/network-manager-app/linux/BUILD.gn b/examples/network-manager-app/linux/BUILD.gn index ea973edfbfd15c..0d260d6c5d560c 100644 --- a/examples/network-manager-app/linux/BUILD.gn +++ b/examples/network-manager-app/linux/BUILD.gn @@ -19,6 +19,7 @@ executable("matter-network-manager-app") { sources = [ "include/CHIPProjectAppConfig.h", "main.cpp", + "tbrm.cpp", ] deps = [ diff --git a/examples/network-manager-app/linux/tbrm.cpp b/examples/network-manager-app/linux/tbrm.cpp new file mode 100644 index 00000000000000..908666ee61d8a0 --- /dev/null +++ b/examples/network-manager-app/linux/tbrm.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace chip; +using namespace chip::literals; +using namespace chip::app; +using namespace chip::app::Clusters; + +namespace { +class FakeBorderRouterDelegate final : public ThreadBorderRouterManagement::Delegate +{ + CHIP_ERROR Init(AttributeChangeCallback * attributeChangeCallback) override + { + mAttributeChangeCallback = attributeChangeCallback; + return CHIP_NO_ERROR; + } + + bool GetPanChangeSupported() override { return true; } + + void GetBorderRouterName(MutableCharSpan & borderRouterName) override + { + CopyCharSpanToMutableCharSpan("netman-br"_span, borderRouterName); + } + + CHIP_ERROR GetBorderAgentId(MutableByteSpan & borderAgentId) override + { + static constexpr uint8_t kBorderAgentId[] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff }; + VerifyOrReturnError(borderAgentId.size() == 16, CHIP_ERROR_INVALID_ARGUMENT); + return CopySpanToMutableSpan(ByteSpan(kBorderAgentId), borderAgentId); + } + + uint16_t GetThreadVersion() override { return /* Thread 1.3.1 */ 5; } + + bool GetInterfaceEnabled() override { return !mActiveDataset.IsEmpty(); } + + CHIP_ERROR GetDataset(Thread::OperationalDataset & dataset, DatasetType type) override + { + Thread::OperationalDataset * source; + switch (type) + { + case DatasetType::kActive: + source = &mActiveDataset; + break; + case DatasetType::kPending: + source = &mPendingDataset; + break; + default: + return CHIP_ERROR_INVALID_ARGUMENT; + } + VerifyOrReturnError(!source->IsEmpty(), CHIP_ERROR_NOT_FOUND); + return dataset.Init(source->AsByteSpan()); + } + + void SetActiveDataset(const Thread::OperationalDataset & activeDataset, uint32_t sequenceNum, + ActivateDatasetCallback * callback) override + { + if (mActivateDatasetCallback != nullptr) + { + callback->OnActivateDatasetComplete(sequenceNum, CHIP_ERROR_INCORRECT_STATE); + return; + } + + CHIP_ERROR err = mActiveDataset.Init(activeDataset.AsByteSpan()); + if (err != CHIP_NO_ERROR) + { + callback->OnActivateDatasetComplete(sequenceNum, err); + return; + } + + mActivateDatasetCallback = callback; + mActivateDatasetSequence = sequenceNum; + DeviceLayer::SystemLayer().StartTimer(System::Clock::Milliseconds32(3000), CompleteDatasetActivation, this); + } + + CHIP_ERROR CommitActiveDataset() override { return CHIP_NO_ERROR; } + CHIP_ERROR RevertActiveDataset() override { return CHIP_ERROR_NOT_IMPLEMENTED; } + CHIP_ERROR SetPendingDataset(const Thread::OperationalDataset & pendingDataset) override { return CHIP_ERROR_NOT_IMPLEMENTED; } + +private: + static void CompleteDatasetActivation(System::Layer *, void * context) + { + auto * self = static_cast(context); + auto * callback = self->mActivateDatasetCallback; + auto sequenceNum = self->mActivateDatasetSequence; + self->mActivateDatasetCallback = nullptr; + callback->OnActivateDatasetComplete(sequenceNum, CHIP_NO_ERROR); + } + + AttributeChangeCallback * mAttributeChangeCallback; + Thread::OperationalDataset mActiveDataset; + Thread::OperationalDataset mPendingDataset; + + ActivateDatasetCallback * mActivateDatasetCallback = nullptr; + uint32_t mActivateDatasetSequence; +}; + +FakeBorderRouterDelegate gBorderRouterDelegate{}; +} // namespace + +std::optional gThreadBorderRouterManagementServer; +void emberAfThreadBorderRouterManagementClusterInitCallback(EndpointId endpoint) +{ + VerifyOrDie(!gThreadBorderRouterManagementServer); + gThreadBorderRouterManagementServer.emplace(endpoint, &gBorderRouterDelegate, Server::GetInstance().GetFailSafeContext()) + .Init(); +} diff --git a/examples/network-manager-app/network-manager-common/network-manager-app.matter b/examples/network-manager-app/network-manager-common/network-manager-app.matter index d3a951ddc9f624..173b2a2c923601 100644 --- a/examples/network-manager-app/network-manager-common/network-manager-app.matter +++ b/examples/network-manager-app/network-manager-common/network-manager-app.matter @@ -1491,6 +1491,49 @@ provisional cluster WiFiNetworkManagement = 1105 { command access(invoke: manage) NetworkPassphraseRequest(): NetworkPassphraseResponse = 0; } +/** Manage the Thread network of Thread Border Router */ +provisional cluster ThreadBorderRouterManagement = 1106 { + revision 1; + + bitmap Feature : bitmap32 { + kPANChange = 0x1; + } + + provisional readonly attribute char_string<63> borderRouterName = 0; + provisional readonly attribute octet_string<254> borderAgentID = 1; + provisional readonly attribute int16u threadVersion = 2; + provisional readonly attribute boolean interfaceEnabled = 3; + provisional readonly attribute nullable int64u activeDatasetTimestamp = 4; + readonly attribute command_id generatedCommandList[] = 65528; + readonly attribute command_id acceptedCommandList[] = 65529; + readonly attribute event_id eventList[] = 65530; + readonly attribute attrib_id attributeList[] = 65531; + readonly attribute bitmap32 featureMap = 65532; + readonly attribute int16u clusterRevision = 65533; + + response struct DatasetResponse = 2 { + octet_string<254> dataset = 0; + } + + request struct SetActiveDatasetRequestRequest { + octet_string<254> activeDataset = 0; + optional int64u breadcrumb = 1; + } + + request struct SetPendingDatasetRequestRequest { + octet_string<254> pendingDataset = 0; + } + + /** Command to request the active operational dataset of the Thread network to which the border router is connected. This command must be sent over a valid CASE session */ + command access(invoke: manage) GetActiveDatasetRequest(): DatasetResponse = 0; + /** Command to request the pending dataset of the Thread network to which the border router is connected. This command must be sent over a valid CASE session */ + command access(invoke: manage) GetPendingDatasetRequest(): DatasetResponse = 1; + /** Command to set or update the active Dataset of the Thread network to which the Border Router is connected. */ + command access(invoke: manage) SetActiveDatasetRequest(SetActiveDatasetRequestRequest): DefaultSuccess = 3; + /** Command set or update the pending Dataset of the Thread network to which the Border Router is connected. */ + command access(invoke: manage) SetPendingDatasetRequest(SetPendingDatasetRequestRequest): DefaultSuccess = 4; +} + /** Manages the names and credentials of Thread networks visible to the user. */ provisional cluster ThreadNetworkDirectory = 1107 { revision 1; @@ -1816,6 +1859,25 @@ endpoint 1 { handle command NetworkPassphraseResponse; } + server cluster ThreadBorderRouterManagement { + callback attribute borderRouterName; + callback attribute borderAgentID; + callback attribute threadVersion; + callback attribute interfaceEnabled; + callback attribute activeDatasetTimestamp; + callback attribute generatedCommandList; + callback attribute acceptedCommandList; + callback attribute eventList; + callback attribute attributeList; + callback attribute featureMap; + ram attribute clusterRevision default = 1; + + handle command GetActiveDatasetRequest; + handle command GetPendingDatasetRequest; + handle command DatasetResponse; + handle command SetActiveDatasetRequest; + } + server cluster ThreadNetworkDirectory { callback attribute preferredExtendedPanID; callback attribute threadNetworks; diff --git a/examples/network-manager-app/network-manager-common/network-manager-app.zap b/examples/network-manager-app/network-manager-common/network-manager-app.zap index 64113c969dd774..0a14bcb26d7b03 100644 --- a/examples/network-manager-app/network-manager-common/network-manager-app.zap +++ b/examples/network-manager-app/network-manager-common/network-manager-app.zap @@ -3217,6 +3217,7 @@ "define": "WIFI_NETWORK_MANAGEMENT_CLUSTER", "side": "server", "enabled": 1, + "apiMaturity": "provisional", "commands": [ { "name": "NetworkPassphraseRequest", @@ -3366,6 +3367,227 @@ } ] }, + { + "name": "Thread Border Router Management", + "code": 1106, + "mfgCode": null, + "define": "THREAD_BORDER_ROUTER_MANAGEMENT_CLUSTER", + "side": "server", + "enabled": 1, + "apiMaturity": "provisional", + "commands": [ + { + "name": "GetActiveDatasetRequest", + "code": 0, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "GetPendingDatasetRequest", + "code": 1, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "DatasetResponse", + "code": 2, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "SetActiveDatasetRequest", + "code": 3, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + } + ], + "attributes": [ + { + "name": "BorderRouterName", + "code": 0, + "mfgCode": null, + "side": "server", + "type": "char_string", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "BorderAgentID", + "code": 1, + "mfgCode": null, + "side": "server", + "type": "octet_string", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "ThreadVersion", + "code": 2, + "mfgCode": null, + "side": "server", + "type": "int16u", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "InterfaceEnabled", + "code": 3, + "mfgCode": null, + "side": "server", + "type": "boolean", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "ActiveDatasetTimestamp", + "code": 4, + "mfgCode": null, + "side": "server", + "type": "int64u", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "GeneratedCommandList", + "code": 65528, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AcceptedCommandList", + "code": 65529, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "EventList", + "code": 65530, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AttributeList", + "code": 65531, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "FeatureMap", + "code": 65532, + "mfgCode": null, + "side": "server", + "type": "bitmap32", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "ClusterRevision", + "code": 65533, + "mfgCode": null, + "side": "server", + "type": "int16u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "1", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + } + ] + }, { "name": "Thread Network Directory", "code": 1107, @@ -3373,6 +3595,7 @@ "define": "THREAD_NETWORK_DIRECTORY_CLUSTER", "side": "server", "enabled": 1, + "apiMaturity": "provisional", "commands": [ { "name": "AddNetwork", diff --git a/scripts/tests/chiptest/__init__.py b/scripts/tests/chiptest/__init__.py index 29aa9d47dce42d..d85e82fec6fa6c 100644 --- a/scripts/tests/chiptest/__init__.py +++ b/scripts/tests/chiptest/__init__.py @@ -289,7 +289,7 @@ def target_for_name(name: str): return TestTarget.MWO if name.startswith("Test_TC_RVCRUNM_") or name.startswith("Test_TC_RVCCLEANM_") or name.startswith("Test_TC_RVCOPSTATE_"): return TestTarget.RVC - if name.startswith("Test_TC_THNETDIR_") or name.startswith("Test_TC_WIFINM_"): + if name.startswith("Test_TC_TBRM_") or name.startswith("Test_TC_THNETDIR_") or name.startswith("Test_TC_WIFINM_"): return TestTarget.NETWORK_MANAGER return TestTarget.ALL_CLUSTERS diff --git a/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.h b/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.h index a2b6d7949ffdff..639e7b13e77ac0 100644 --- a/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.h +++ b/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.h @@ -17,17 +17,13 @@ #pragma once -#include "thread-br-delegate.h" - #include #include -#include -#include #include #include #include +#include #include -#include #include #include diff --git a/src/app/clusters/thread-network-directory-server/DefaultThreadNetworkDirectoryStorage.cpp b/src/app/clusters/thread-network-directory-server/DefaultThreadNetworkDirectoryStorage.cpp index 6daaffce9ce91c..c272e0acdd7461 100644 --- a/src/app/clusters/thread-network-directory-server/DefaultThreadNetworkDirectoryStorage.cpp +++ b/src/app/clusters/thread-network-directory-server/DefaultThreadNetworkDirectoryStorage.cpp @@ -46,7 +46,8 @@ CHIP_ERROR DefaultThreadNetworkDirectoryStorage::StoreIndex() { VerifyOrDie(mInitialized); StorageKeyName key = DefaultStorageKeyAllocator::ThreadNetworkDirectoryIndex(); - return mStorage.SyncSetKeyValue(key.KeyName(), mExtendedPanIds, mCount * ExtendedPanId::size()); + static_assert(kCapacity * ExtendedPanId::size() <= UINT16_MAX); // kCapacity >= mCount + return mStorage.SyncSetKeyValue(key.KeyName(), mExtendedPanIds, static_cast(mCount * ExtendedPanId::size())); } bool DefaultThreadNetworkDirectoryStorage::FindNetwork(const ExtendedPanId & exPanId, index_t & outIndex) diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn index 681f16aedc1e6c..1bce08d55e0571 100644 --- a/src/app/tests/BUILD.gn +++ b/src/app/tests/BUILD.gn @@ -140,7 +140,11 @@ source_set("operational-state-test-srcs") { } source_set("thread-border-router-management-test-srcs") { - sources = [ "${chip_root}/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.cpp" ] + sources = [ + "${chip_root}/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.cpp", + "${chip_root}/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.h", + "${chip_root}/src/app/clusters/thread-border-router-management-server/thread-br-delegate.h", + ] public_deps = [ "${chip_root}/src/app", diff --git a/src/app/tests/suites/certification/Test_TC_TBRM_2_1.yaml b/src/app/tests/suites/certification/Test_TC_TBRM_2_1.yaml new file mode 100644 index 00000000000000..dd58b93543dfc7 --- /dev/null +++ b/src/app/tests/suites/certification/Test_TC_TBRM_2_1.yaml @@ -0,0 +1,83 @@ +# 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. + +name: "[TC-TBRM-2.1] Attributes with Server as DUT" + +PICS: + - TBRM.S + +config: + nodeId: 0x12344321 + cluster: Thread Border Router Management + endpoint: 1 + +tests: + - label: "Wait for the commissioned device to be retrieved" + cluster: DelayCommands + command: WaitForCommissionee + arguments: + values: + - name: nodeId + value: nodeId + + - label: "TH reads the BorderRouterName attribute from the DUT" + command: readAttribute + attribute: BorderRouterName + response: + constraints: + type: char_string + hasValue: true + minLength: 1 + maxLength: 63 + + - label: "TH reads the BorderAgentID attribute from the DUT" + command: readAttribute + attribute: BorderAgentID + response: + constraints: + type: octet_string + hasValue: true + minLength: 16 + maxLength: 16 + + - label: "TH reads the ThreadVersion attribute from the DUT" + command: readAttribute + attribute: ThreadVersion + response: + constraints: + type: int16u + hasValue: true + minValue: 4 + + - label: "TH reads the InterfaceEnabled attribute from the DUT" + command: readAttribute + attribute: InterfaceEnabled + response: + constraints: + type: boolean + hasValue: true + + - label: "TH reads the ActiveDatasetTimestamp attribute from the DUT" + command: readAttribute + attribute: ActiveDatasetTimestamp + response: + constraints: + type: int64u + # TODO: Attribute missing from cluster XML + # - label: "TH reads the PendingDatasetTimestamp attribute from the DUT" + # command: readAttribute + # attribute: PendingDatasetTimestamp + # response: + # constraints: + # type: int64u diff --git a/src/app/zap-templates/zcl/data-model/chip/matter-devices.xml b/src/app/zap-templates/zcl/data-model/chip/matter-devices.xml index f0266fffecb872..14f4b9edf39958 100644 --- a/src/app/zap-templates/zcl/data-model/chip/matter-devices.xml +++ b/src/app/zap-templates/zcl/data-model/chip/matter-devices.xml @@ -2428,11 +2428,12 @@ limitations under the License. + MA-thread-border-router - HRAP + CHIP Matter Thread Border Router 0x0103 0x0091 diff --git a/src/app/zap-templates/zcl/data-model/chip/thread-border-router-management-cluster.xml b/src/app/zap-templates/zcl/data-model/chip/thread-border-router-management-cluster.xml index b6ee4385fb73ff..d7565e0262c9da 100644 --- a/src/app/zap-templates/zcl/data-model/chip/thread-border-router-management-cluster.xml +++ b/src/app/zap-templates/zcl/data-model/chip/thread-border-router-management-cluster.xml @@ -23,7 +23,7 @@ limitations under the License. - HRAP + Network Infrastructure Thread Border Router Management 0x0452 THREAD_BORDER_ROUTER_MANAGEMENT_CLUSTER diff --git a/src/darwin/Framework/CHIP/zap-generated/MTRDeviceTypeMetadata.mm b/src/darwin/Framework/CHIP/zap-generated/MTRDeviceTypeMetadata.mm index aa82714a472572..7bab05ab23eccf 100644 --- a/src/darwin/Framework/CHIP/zap-generated/MTRDeviceTypeMetadata.mm +++ b/src/darwin/Framework/CHIP/zap-generated/MTRDeviceTypeMetadata.mm @@ -73,6 +73,7 @@ { 0x0000007B, DeviceTypeClass::Simple, "Matter Oven" }, { 0x0000007C, DeviceTypeClass::Simple, "Matter Laundry Dryer" }, { 0x00000090, DeviceTypeClass::Simple, "Matter Network Infrastructure Manager" }, + { 0x00000091, DeviceTypeClass::Simple, "Matter Thread Border Router" }, { 0x00000100, DeviceTypeClass::Simple, "Matter On/Off Light" }, { 0x00000101, DeviceTypeClass::Simple, "Matter Dimmable Light" }, { 0x00000103, DeviceTypeClass::Simple, "Matter On/Off Light Switch" }, @@ -98,7 +99,6 @@ { 0x00000510, DeviceTypeClass::Utility, "Matter Electrical Sensor" }, { 0x00000840, DeviceTypeClass::Simple, "Matter Control Bridge" }, { 0x00000850, DeviceTypeClass::Simple, "Matter On/Off Sensor" }, - { 0x00000091, DeviceTypeClass::Simple, "Matter Thread Border Router" }, }; static_assert(ExtractVendorFromMEI(0xFFF10001) != 0, "Must have class defined for \"Matter Orphan Clusters\" if it's a standard device type");