Allow chip-repl to send group commands (#25158)
* Allow chip-repl to send group commands
With chip-repl now able to send we have updated the yamltests runner
that uses chip-repl to send group commands
* Address PR comments
* Restyle
* Address PR comments
* Restyle
diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn
index 82bc673..da9b080 100644
--- a/src/controller/python/BUILD.gn
+++ b/src/controller/python/BUILD.gn
@@ -51,6 +51,8 @@
sources += [ "chip/native/CommonStackInit.cpp" ]
+ defines = []
+
if (chip_controller) {
sources += [
"ChipCommissionableNodeController-ScriptBinding.cpp",
@@ -76,6 +78,8 @@
"chip/native/PyChipError.cpp",
"chip/utils/DeviceProxyUtils.cpp",
]
+ defines += [ "CHIP_CONFIG_MAX_GROUPS_PER_FABRIC=50" ]
+ defines += [ "CHIP_CONFIG_MAX_GROUP_KEYS_PER_FABRIC=50" ]
} else {
sources += [
"chip/server/Options.cpp",
diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
index e9cf070..eaced94 100644
--- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp
+++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
@@ -238,6 +238,7 @@
sGroupDataProvider.SetStorageDelegate(storageAdapter);
sGroupDataProvider.SetSessionKeystore(factoryParams.sessionKeystore);
PyReturnErrorOnFailure(ToPyChipError(sGroupDataProvider.Init()));
+ Credentials::SetGroupDataProvider(&sGroupDataProvider);
factoryParams.groupDataProvider = &sGroupDataProvider;
PyReturnErrorOnFailure(ToPyChipError(sPersistentStorageOpCertStore.Init(storageAdapter)));
diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp
index 3b12f8f..c7f3e1d 100644
--- a/src/controller/python/OpCredsBinding.cpp
+++ b/src/controller/python/OpCredsBinding.cpp
@@ -465,6 +465,21 @@
return ToPyChipError(CHIP_NO_ERROR);
}
+PyChipError pychip_OpCreds_InitGroupTestingData(chip::Controller::DeviceCommissioner * devCtrl)
+{
+ VerifyOrReturnError(devCtrl != nullptr, ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT));
+
+ uint8_t compressedFabricId[sizeof(uint64_t)] = { 0 };
+ chip::MutableByteSpan compressedFabricIdSpan(compressedFabricId);
+
+ CHIP_ERROR err = devCtrl->GetCompressedFabricIdBytes(compressedFabricIdSpan);
+ VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err));
+
+ err = chip::GroupTesting::InitData(&sGroupDataProvider, devCtrl->GetFabricIndex(), compressedFabricIdSpan);
+
+ return ToPyChipError(err);
+}
+
PyChipError pychip_OpCreds_SetMaximallyLargeCertsUsed(OpCredsContext * context, bool enabled)
{
VerifyOrReturnError(context != nullptr && context->mAdapter != nullptr, ToPyChipError(CHIP_ERROR_INCORRECT_STATE));
diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py
index 96e86ad..f373166 100644
--- a/src/controller/python/chip/ChipDeviceCtrl.py
+++ b/src/controller/python/chip/ChipDeviceCtrl.py
@@ -894,6 +894,18 @@
), payload, timedRequestTimeoutMs=timedRequestTimeoutMs, interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs).raise_on_error()
return await future
+ def SendGroupCommand(self, groupid: int, payload: ClusterObjects.ClusterCommand, busyWaitMs: typing.Union[None, int] = None):
+ '''
+ Send a group cluster-object encapsulated command to a group_id and get returned a future that can be awaited upon to get confirmation command was sent.
+ '''
+ self.CheckIsActive()
+
+ ClusterCommand.SendGroupCommand(
+ groupid, self.devCtrl, payload, busyWaitMs=busyWaitMs).raise_on_error()
+
+ # None is the expected return for sending group commands.
+ return None
+
async def WriteAttribute(self, nodeid: int, attributes: typing.List[typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor, int]], timedRequestTimeoutMs: typing.Union[None, int] = None, interactionTimeoutMs: typing.Union[None, int] = None, busyWaitMs: typing.Union[None, int] = None):
'''
Write a list of attributes on a target node.
@@ -1289,6 +1301,15 @@
self.devCtrl, py_object(self), csr.NOCSRElements, len(csr.NOCSRElements), nodeId)
)
+ def InitGroupTestingData(self):
+ """Populates the Device Controller's GroupDataProvider with known test group info and keys."""
+ self.CheckIsActive()
+
+ self._ChipStack.Call(
+ lambda: self._dmLib.pychip_OpCreds_InitGroupTestingData(
+ self.devCtrl)
+ ).raise_on_error()
+
# ----- Private Members -----
def _InitLib(self):
if self._dmLib is None:
@@ -1455,6 +1476,10 @@
]
self._dmLib.pychip_DeviceController_IssueNOCChain.restype = PyChipError
+ self._dmLib.pychip_OpCreds_InitGroupTestingData.argtypes = [
+ c_void_p]
+ self._dmLib.pychip_OpCreds_InitGroupTestingData.restype = PyChipError
+
self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback.argtypes = [
_IssueNOCChainCallbackPythonCallbackFunct]
self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback.restype = None
diff --git a/src/controller/python/chip/clusters/Command.py b/src/controller/python/chip/clusters/Command.py
index 056688f..df20371 100644
--- a/src/controller/python/chip/clusters/Command.py
+++ b/src/controller/python/chip/clusters/Command.py
@@ -175,6 +175,22 @@
))
+def SendGroupCommand(groupId: int, devCtrl: c_void_p, payload: ClusterCommand, busyWaitMs: Union[None, int] = None) -> PyChipError:
+ ''' Send a cluster-object encapsulated group command to a device and does the following:
+ - None (on a successful response containing no data)
+ - Raises an exception if any errors are encountered.
+ '''
+ handle = chip.native.GetLibraryHandle()
+
+ payloadTLV = payload.ToTLV()
+ return builtins.chipStack.Call(
+ lambda: handle.pychip_CommandSender_SendGroupCommand(
+ c_uint16(groupId), devCtrl,
+ payload.cluster_id, payload.command_id, payloadTLV, len(payloadTLV),
+ ctypes.c_uint16(0 if busyWaitMs is None else busyWaitMs),
+ ))
+
+
def Init():
handle = chip.native.GetLibraryHandle()
@@ -185,6 +201,8 @@
setter.Set('pychip_CommandSender_SendCommand',
PyChipError, [py_object, c_void_p, c_uint16, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16])
+ setter.Set('pychip_CommandSender_SendGroupCommand',
+ PyChipError, [c_uint16, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16])
setter.Set('pychip_CommandSender_InitCallbacks', None, [
_OnCommandSenderResponseCallbackFunct, _OnCommandSenderErrorCallbackFunct, _OnCommandSenderDoneCallbackFunct])
diff --git a/src/controller/python/chip/clusters/command.cpp b/src/controller/python/chip/clusters/command.cpp
index 169b925..0c812c4 100644
--- a/src/controller/python/chip/clusters/command.cpp
+++ b/src/controller/python/chip/clusters/command.cpp
@@ -37,6 +37,10 @@
chip::EndpointId endpointId, chip::ClusterId clusterId, chip::CommandId commandId,
const uint8_t * payload, size_t length, uint16_t interactionTimeoutMs,
uint16_t busyWaitMs);
+
+PyChipError pychip_CommandSender_SendGroupCommand(chip::GroupId groupId, chip::Controller::DeviceCommissioner * devCtrl,
+ chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload,
+ size_t length, uint16_t busyWaitMs);
}
namespace chip {
@@ -171,4 +175,46 @@
exit:
return ToPyChipError(err);
}
+
+PyChipError pychip_CommandSender_SendGroupCommand(chip::GroupId groupId, chip::Controller::DeviceCommissioner * devCtrl,
+ chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload,
+ size_t length, uint16_t busyWaitMs)
+{
+ CHIP_ERROR err = CHIP_NO_ERROR;
+
+ chip::Messaging::ExchangeManager * exchangeManager = chip::app::InteractionModelEngine::GetInstance()->GetExchangeManager();
+ VerifyOrReturnError(exchangeManager != nullptr, ToPyChipError(CHIP_ERROR_INCORRECT_STATE));
+
+ std::unique_ptr<CommandSender> sender = std::make_unique<CommandSender>(nullptr /* callback */, exchangeManager);
+
+ app::CommandPathParams cmdParams = { groupId, clusterId, commandId, (app::CommandPathFlags::kGroupIdValid) };
+
+ SuccessOrExit(err = sender->PrepareCommand(cmdParams, false));
+
+ {
+ auto writer = sender->GetCommandDataIBTLVWriter();
+ TLV::TLVReader reader;
+ VerifyOrExit(writer != nullptr, err = CHIP_ERROR_INCORRECT_STATE);
+ reader.Init(payload, length);
+ reader.Next();
+ SuccessOrExit(writer->CopyContainer(TLV::ContextTag(to_underlying(CommandDataIB::Tag::kFields)), reader));
+ }
+
+ SuccessOrExit(err = sender->FinishCommand(Optional<uint16_t>::Missing()));
+
+ {
+ auto fabricIndex = devCtrl->GetFabricIndex();
+
+ chip::Transport::OutgoingGroupSession session(groupId, fabricIndex);
+ SuccessOrExit(err = sender->SendGroupCommandRequest(chip::SessionHandle(session)));
+ }
+
+ if (busyWaitMs)
+ {
+ usleep(busyWaitMs * 1000);
+ }
+
+exit:
+ return ToPyChipError(err);
+}
}
diff --git a/src/controller/python/chip/yaml/runner.py b/src/controller/python/chip/yaml/runner.py
index b3559a1..8780c2b 100644
--- a/src/controller/python/chip/yaml/runner.py
+++ b/src/controller/python/chip/yaml/runner.py
@@ -156,6 +156,11 @@
self._expected_response_object = None
self._endpoint = test_step.endpoint
self._node_id = test_step.node_id
+ self._group_id = test_step.group_id
+
+ if self._node_id is None and self._group_id is None:
+ raise UnexpectedParsingError(
+ 'Both node_id and group_id are None, at least one needs to be provided')
command = context.data_model_lookup.get_command(self._cluster, self._command_name)
@@ -182,10 +187,15 @@
def run_action(self, dev_ctrl: ChipDeviceController) -> _ActionResult:
try:
- resp = asyncio.run(dev_ctrl.SendCommand(
- self._node_id, self._endpoint, self._request_object,
- timedRequestTimeoutMs=self._interation_timeout_ms,
- busyWaitMs=self._busy_wait_ms))
+ if self._group_id:
+ resp = dev_ctrl.SendGroupCommand(
+ self._group_id, self._request_object,
+ busyWaitMs=self._busy_wait_ms)
+ else:
+ resp = asyncio.run(dev_ctrl.SendCommand(
+ self._node_id, self._endpoint, self._request_object,
+ timedRequestTimeoutMs=self._interation_timeout_ms,
+ busyWaitMs=self._busy_wait_ms))
except chip.interaction_model.InteractionModelError as error:
return _ActionResult(status=_ActionStatus.ERROR, response=error)
@@ -736,6 +746,7 @@
self._certificate_authority_manager = certificate_authority_manager
self._dev_ctrls = {}
+ alpha_dev_ctrl.InitGroupTestingData()
self._dev_ctrls['alpha'] = alpha_dev_ctrl
def _invoke_action_factory(self, test_step, cluster: str):
@@ -1014,6 +1025,7 @@
fabric = certificate_authority.NewFabricAdmin(vendorId=0xFFF1,
fabricId=fabric_id)
dev_ctrl = fabric.NewController()
+ dev_ctrl.InitGroupTestingData()
self._dev_ctrls[action.identity] = dev_ctrl
else:
dev_ctrl = self._dev_ctrls['alpha']