Skip to content

Commit

Permalink
Allow chip-repl to send group commands
Browse files Browse the repository at this point in the history
With chip-repl now able to send we have updated the yamltests runner
that uses chip-repl to send group commands
  • Loading branch information
tehampson committed Feb 17, 2023
1 parent 48706bd commit 6a1ce38
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 15 deletions.
3 changes: 2 additions & 1 deletion src/controller/python/ChipDeviceController-ScriptBinding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ chip::Controller::CommissioningParameters sCommissioningParameters;

chip::Controller::ScriptDevicePairingDelegate sPairingDelegate;
chip::Controller::ScriptPairingDeviceDiscoveryDelegate sPairingDeviceDiscoveryDelegate;
chip::Credentials::GroupDataProviderImpl sGroupDataProvider;
chip::Credentials::GroupDataProviderImpl sGroupDataProvider{ 5, 8 };
chip::Credentials::PersistentStorageOpCertStore sPersistentStorageOpCertStore;

// NOTE: Remote device ID is in sync with the echo server device id
Expand Down Expand Up @@ -234,6 +234,7 @@ PyChipError pychip_DeviceController_StackInit(Controller::Python::StorageAdapter

sGroupDataProvider.SetStorageDelegate(storageAdapter);
PyReturnErrorOnFailure(ToPyChipError(sGroupDataProvider.Init()));
Credentials::SetGroupDataProvider(&sGroupDataProvider);
factoryParams.groupDataProvider = &sGroupDataProvider;

PyReturnErrorOnFailure(ToPyChipError(sPersistentStorageOpCertStore.Init(storageAdapter)));
Expand Down
15 changes: 15 additions & 0 deletions src/controller/python/OpCredsBinding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,21 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co
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));
Expand Down
31 changes: 31 additions & 0 deletions src/controller/python/chip/ChipDeviceCtrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,24 @@ async def SendCommand(self, nodeid: int, endpoint: int, payload: ClusterObjects.
), payload, timedRequestTimeoutMs=timedRequestTimeoutMs, interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs).raise_on_error()
return await future

async def SendGroupCommand(self, groupid: int, payload: ClusterObjects.ClusterCommand, timedRequestTimeoutMs: typing.Union[None, int] = None, interactionTimeoutMs: typing.Union[None, int] = None, 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.
timedWriteTimeoutMs: Timeout for a timed invoke request. Omit or set to 'None' to indicate a non-timed request.
interactionTimeoutMs: Overall timeout for the interaction. Omit or set to 'None' to have the SDK automatically compute the right
timeout value based on transport characteristics as well as the responsiveness of the target.
'''
self.CheckIsActive()

eventLoop = asyncio.get_running_loop()
future = eventLoop.create_future()

ClusterCommand.SendGroupCommand(
future, eventLoop, groupid, self.devCtrl, payload, timedRequestTimeoutMs=timedRequestTimeoutMs,
interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs).raise_on_error()
return await future

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.
Expand Down Expand Up @@ -1289,6 +1307,15 @@ def IssueNOCChain(self, csr: Clusters.OperationalCredentials.Commands.CSRRespons
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:
Expand Down Expand Up @@ -1455,6 +1482,10 @@ def _InitLib(self):
]
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
Expand Down
50 changes: 48 additions & 2 deletions src/controller/python/chip/clusters/Command.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,22 @@ def handleError(self, status: Status, chipError: PyChipError):
self._handleError, status, chipError, None
)

def _handleGroupCommandDone(self):
self._future.set_result(None)

def handleGroupCommandDone(self):
self._event_loop.call_soon_threadsafe(
self._handleGroupCommandDone)


_OnCommandSenderResponseCallbackFunct = CFUNCTYPE(
None, py_object, c_uint16, c_uint32, c_uint32, c_uint16, c_uint8, c_void_p, c_uint32)
_OnCommandSenderErrorCallbackFunct = CFUNCTYPE(
None, py_object, c_uint16, c_uint8, PyChipError)
_OnCommandSenderDoneCallbackFunct = CFUNCTYPE(
None, py_object)
_OnGroupCommandSenderDoneCallbackFunct = CFUNCTYPE(
None, py_object)


@_OnCommandSenderResponseCallbackFunct
Expand All @@ -142,6 +151,13 @@ def _OnCommandSenderDoneCallback(closure):
ctypes.pythonapi.Py_DecRef(ctypes.py_object(closure))


@_OnGroupCommandSenderDoneCallbackFunct
def _OnGroupCommandSenderDoneCallback(closure):
# Group commands have no response so we leverage the group command done callback
closure.handleGroupCommandDone()
ctypes.pythonapi.Py_DecRef(ctypes.py_object(closure))


def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPath: CommandPath, payload: ClusterCommand, timedRequestTimeoutMs: Union[None, int] = None, interactionTimeoutMs: Union[None, int] = None, busyWaitMs: Union[None, int] = None) -> PyChipError:
''' Send a cluster-object encapsulated command to a device and does the following:
- On receipt of a successful data response, returns the cluster-object equivalent through the provided future.
Expand Down Expand Up @@ -175,6 +191,34 @@ def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPa
))


def SendGroupCommand(future: Future, eventLoop, groupId: int, devCtrl: c_void_p, payload: ClusterCommand, timedRequestTimeoutMs: Union[None, int] = None, interactionTimeoutMs: Union[None, int] = None, 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.
If a valid timedRequestTimeoutMs is provided, a timed interaction will be initiated instead.
If a valid interactionTimeoutMs is provided, the interaction will terminate with a CHIP_ERROR_TIMEOUT if a response
has not been received within that timeout. If it isn't provided, a sensible value will be automatically computed that
accounts for the underlying characteristics of both the transport and the responsiveness of the receiver.
'''
if payload.must_use_timed_invoke and timedRequestTimeoutMs is None or timedRequestTimeoutMs == 0:
raise chip.interaction_model.InteractionModelError(chip.interaction_model.Status.NeedsTimedInteraction)

handle = chip.native.GetLibraryHandle()
transaction = AsyncCommandTransaction(future, eventLoop, None)

payloadTLV = payload.ToTLV()
ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction))
return builtins.chipStack.Call(
lambda: handle.pychip_CommandSender_SendGroupCommand(
ctypes.py_object(transaction), c_uint16(groupId), devCtrl,
c_uint16(0 if timedRequestTimeoutMs is None else timedRequestTimeoutMs),
payload.cluster_id, payload.command_id, payloadTLV, len(payloadTLV),
ctypes.c_uint16(0 if interactionTimeoutMs is None else interactionTimeoutMs),
ctypes.c_uint16(0 if busyWaitMs is None else busyWaitMs),
))


def Init():
handle = chip.native.GetLibraryHandle()

Expand All @@ -185,8 +229,10 @@ def Init():

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, [py_object, c_uint16, c_void_p, c_uint16, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16, c_uint16])
setter.Set('pychip_CommandSender_InitCallbacks', None, [
_OnCommandSenderResponseCallbackFunct, _OnCommandSenderErrorCallbackFunct, _OnCommandSenderDoneCallbackFunct])
_OnCommandSenderResponseCallbackFunct, _OnCommandSenderErrorCallbackFunct, _OnCommandSenderDoneCallbackFunct, _OnGroupCommandSenderDoneCallbackFunct])

handle.pychip_CommandSender_InitCallbacks(
_OnCommandSenderResponseCallback, _OnCommandSenderErrorCallback, _OnCommandSenderDoneCallback)
_OnCommandSenderResponseCallback, _OnCommandSenderErrorCallback, _OnCommandSenderDoneCallback, _OnGroupCommandSenderDoneCallback)
93 changes: 85 additions & 8 deletions src/controller/python/chip/clusters/command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,30 @@ PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * de
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(void * appContext, chip::GroupId groupId,
chip::Controller::DeviceCommissioner * devCtrl, uint16_t timedRequestTimeoutMs,
chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload,
size_t length, uint16_t interactionTimeoutMs, uint16_t busyWaitMs);
}

namespace chip {
namespace python {

using OnCommandSenderResponseCallback = void (*)(PyObject appContext, chip::EndpointId endpointId, chip::ClusterId clusterId,
using OnCommandSenderResponseCallback = void (*)(PyObject appContext, chip::EndpointId endpointId, chip::ClusterId clusterId,
chip::CommandId commandId,
std::underlying_type_t<Protocols::InteractionModel::Status> status,
chip::ClusterStatus clusterStatus, const uint8_t * payload, uint32_t length);
using OnCommandSenderErrorCallback = void (*)(PyObject appContext,
using OnCommandSenderErrorCallback = void (*)(PyObject appContext,
std::underlying_type_t<Protocols::InteractionModel::Status> status,
chip::ClusterStatus clusterStatus, PyChipError chiperror);
using OnCommandSenderDoneCallback = void (*)(PyObject appContext);
using OnCommandSenderDoneCallback = void (*)(PyObject appContext);
using OnGroupCommandSenderDoneCallback = void (*)(PyObject appContext);

OnCommandSenderResponseCallback gOnCommandSenderResponseCallback = nullptr;
OnCommandSenderErrorCallback gOnCommandSenderErrorCallback = nullptr;
OnCommandSenderDoneCallback gOnCommandSenderDoneCallback = nullptr;
OnCommandSenderDoneCallback gOnGroupCommandSenderDoneCallback = nullptr;

class CommandSenderCallback : public CommandSender::Callback
{
Expand Down Expand Up @@ -106,10 +113,29 @@ class CommandSenderCallback : public CommandSender::Callback
delete this;
};

private:
protected:
PyObject mAppContext = nullptr;
};

class GroupCommandSenderCallback : public CommandSenderCallback
{
public:
GroupCommandSenderCallback(PyObject appContext) : CommandSenderCallback(appContext) {}
void OnResponse(CommandSender * apCommandSender, const ConcreteCommandPath & aPath, const app::StatusIB & aStatus,
TLV::TLVReader * aData) override
{
// Group messages are not expected to provide a response.
chipDie();
}

void OnDone(CommandSender * apCommandSender) override
{
gOnGroupCommandSenderDoneCallback(mAppContext);
delete apCommandSender;
delete this;
};
};

} // namespace python
} // namespace chip

Expand All @@ -118,11 +144,13 @@ using namespace chip::python;
extern "C" {
void pychip_CommandSender_InitCallbacks(OnCommandSenderResponseCallback onCommandSenderResponseCallback,
OnCommandSenderErrorCallback onCommandSenderErrorCallback,
OnCommandSenderDoneCallback onCommandSenderDoneCallback)
OnCommandSenderDoneCallback onCommandSenderDoneCallback,
OnGroupCommandSenderDoneCallback onGroupCommandSenderDoneCallback)
{
gOnCommandSenderResponseCallback = onCommandSenderResponseCallback;
gOnCommandSenderErrorCallback = onCommandSenderErrorCallback;
gOnCommandSenderDoneCallback = onCommandSenderDoneCallback;
gOnCommandSenderResponseCallback = onCommandSenderResponseCallback;
gOnCommandSenderErrorCallback = onCommandSenderErrorCallback;
gOnCommandSenderDoneCallback = onCommandSenderDoneCallback;
gOnGroupCommandSenderDoneCallback = onGroupCommandSenderDoneCallback;
}

PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * device, uint16_t timedRequestTimeoutMs,
Expand Down Expand Up @@ -168,6 +196,55 @@ PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * de
usleep(busyWaitMs * 1000);
}

exit:
return ToPyChipError(err);
}

PyChipError pychip_CommandSender_SendGroupCommand(void * appContext, chip::GroupId groupId,
chip::Controller::DeviceCommissioner * devCtrl, uint16_t timedRequestTimeoutMs,
chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload,
size_t length, uint16_t interactionTimeoutMs, 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<GroupCommandSenderCallback> callback = std::make_unique<GroupCommandSenderCallback>(appContext);
std::unique_ptr<CommandSender> sender = std::make_unique<CommandSender>(callback.get(), exchangeManager,
/* is timed request */ timedRequestTimeoutMs != 0);

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(timedRequestTimeoutMs != 0 ? Optional<uint16_t>(timedRequestTimeoutMs)
: Optional<uint16_t>::Missing()));

{
auto fabricIndex = devCtrl->GetFabricIndex();

chip::Transport::OutgoingGroupSession session(groupId, fabricIndex);
SuccessOrExit(err = sender->SendGroupCommandRequest(chip::SessionHandle(session)));
}

sender.release();
callback.release();

if (busyWaitMs)
{
usleep(busyWaitMs * 1000);
}

exit:
return ToPyChipError(err);
}
Expand Down
21 changes: 17 additions & 4 deletions src/controller/python/chip/yaml/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext):
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)

Expand All @@ -182,10 +187,16 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext):

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 = asyncio.run(dev_ctrl.SendGroupCommand(
self._group_id, self._request_object,
timedRequestTimeoutMs=self._interation_timeout_ms,
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)

Expand Down Expand Up @@ -736,6 +747,7 @@ def __init__(self, test_spec_definition, certificate_authority_manager, alpha_de
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):
Expand Down Expand Up @@ -1014,6 +1026,7 @@ def _get_dev_ctrl(self, action: BaseAction):
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']
Expand Down

0 comments on commit 6a1ce38

Please sign in to comment.