From 5400000ad64e08fdfc89f2a7c3a1076821500f04 Mon Sep 17 00:00:00 2001 From: Terence Hampson Date: Wed, 13 Dec 2023 15:49:25 -0500 Subject: [PATCH] Enable chip-repl to send batch commands (#30851) This enables chip-repl to send Batch commands in a single invoke request command. This is required in order to automate testing csg test plan for batch invoke commands. --- src/controller/python/chip/ChipDeviceCtrl.py | 39 +++++ .../python/chip/clusters/Command.py | 159 ++++++++++++++++- .../python/chip/clusters/command.cpp | 161 +++++++++++++++++- .../python/chip/interaction_model/__init__.py | 10 +- .../python/chip/interaction_model/delegate.py | 6 + src/lib/core/BUILD.gn | 1 + src/lib/core/DataModelTypes.h | 1 + src/lib/core/core.gni | 3 + 8 files changed, 359 insertions(+), 21 deletions(-) diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index 8f70c07c185fbf..8fa20dc75246f3 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -901,6 +901,45 @@ async def SendCommand(self, nodeid: int, endpoint: int, payload: ClusterObjects. interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse).raise_on_error() return await future + async def SendBatchCommands(self, nodeid: int, commands: typing.List[ClusterCommand.InvokeRequestInfo], + timedRequestTimeoutMs: typing.Optional[int] = None, + interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None, + suppressResponse: typing.Optional[bool] = None): + ''' + Send a batch of cluster-object encapsulated commands to a node and get returned a future that can be awaited upon to receive + the responses. If a valid responseType is passed in, that will be used to deserialize the object. If not, + the type will be automatically deduced from the metadata received over the wire. + + nodeId: Target's Node ID + commands: A list of InvokeRequestInfo containing the commands to invoke. + 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. + busyWaitMs: How long to wait in ms after sending command to device before performing any other operations. + suppressResponse: Do not send a response to this action + + Returns: + - List of command responses in the same order as what was given in `commands`. The type of the response is defined by the command. + - A value of `None` indicates success. + - If only a single command fails, for example with `UNSUPPORTED_COMMAND`, the corresponding index associated with the command will, + contain `interaction_model.Status.UnsupportedCommand`. + - If a command is not responded to by server, command will contain `interaction_model.Status.Failure` + Raises: + - InteractionModelError if error with sending of InvokeRequestMessage fails as a whole. + ''' + self.CheckIsActive() + + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + + device = self.GetConnectedDeviceSync(nodeid, timeoutMs=interactionTimeoutMs) + + ClusterCommand.SendBatchCommands( + future, eventLoop, device.deviceProxy, commands, + timedRequestTimeoutMs=timedRequestTimeoutMs, + interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse).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 diff --git a/src/controller/python/chip/clusters/Command.py b/src/controller/python/chip/clusters/Command.py index af1336541b2e3e..eabbc2337d129a 100644 --- a/src/controller/python/chip/clusters/Command.py +++ b/src/controller/python/chip/clusters/Command.py @@ -23,7 +23,7 @@ from asyncio.futures import Future from ctypes import CFUNCTYPE, c_bool, c_char_p, c_size_t, c_uint8, c_uint16, c_uint32, c_void_p, py_object from dataclasses import dataclass -from typing import Type, Union +from typing import List, Optional, Type, Union import chip.exceptions import chip.interaction_model @@ -42,6 +42,13 @@ class CommandPath: CommandId: int +@dataclass +class InvokeRequestInfo: + EndpointId: int + Command: ClusterCommand + ResponseType: Optional[Type] = None + + @dataclass class Status: IMStatus: int @@ -94,7 +101,11 @@ def _handleResponse(self, path: CommandPath, status: Status, response: bytes): else: self._future.set_result(None) - def handleResponse(self, path: CommandPath, status: Status, response: bytes): + def handleResponse(self, path: CommandPath, index: int, status: Status, response: bytes): + # For AsyncCommandTransaction we only expect to ever get one response so we don't bother + # checking `index`. We just share a callback API with batch commands. If we ever get a + # second call to `handleResponse` we will see a different error on trying to set future + # that has already been set. self._event_loop.call_soon_threadsafe( self._handleResponse, path, status, response) @@ -105,9 +116,79 @@ def _handleError(self, imError: Status, chipError: PyChipError, exception: Excep self._future.set_exception(chipError.to_exception()) else: try: + # If you got an exception from this call other than AttributeError please + # add it to the except block below. We changed Exception->AttributeError as + # that is what we thought we are trying to catch here. + self._future.set_exception( + chip.interaction_model.InteractionModelError(chip.interaction_model.Status(imError.IMStatus), imError.ClusterStatus)) + except AttributeError: + logger.exception("Failed to map interaction model status received: %s. Remapping to Failure." % imError) + self._future.set_exception(chip.interaction_model.InteractionModelError( + chip.interaction_model.Status.Failure, imError.ClusterStatus)) + + def handleError(self, status: Status, chipError: PyChipError): + self._event_loop.call_soon_threadsafe( + self._handleError, status, chipError, None + ) + + def handleDone(self): + ctypes.pythonapi.Py_DecRef(ctypes.py_object(self)) + + +class AsyncBatchCommandsTransaction: + def __init__(self, future: Future, eventLoop, expectTypes: List[Type]): + self._event_loop = eventLoop + self._future = future + self._expect_types = expectTypes + default_im_failure = chip.interaction_model.InteractionModelError( + chip.interaction_model.Status.NoCommandResponse) + self._responses = [default_im_failure] * len(expectTypes) + + def _handleResponse(self, path: CommandPath, index: int, status: Status, response: bytes): + if index > len(self._responses): + self._handleError(status, 0, IndexError(f"CommandSenderCallback has given us an unexpected index value {index}")) + return + + if (len(response) == 0): + self._responses[index] = None + else: + # If a type hasn't been assigned, let's auto-deduce it. + if (self._expect_types[index] is None): + self._expect_types[index] = FindCommandClusterObject(False, path) + + if self._expect_types[index]: + try: + # If you got an exception from this call other than AttributeError please + # add it to the except block below. We changed Exception->AttributeError as + # that is what we thought we are trying to catch here. + self._responses[index] = self._expect_types[index].FromTLV(response) + except AttributeError as ex: + self._handleError(status, 0, ex) + else: + self._responses[index] = None + + def handleResponse(self, path: CommandPath, index: int, status: Status, response: bytes): + self._event_loop.call_soon_threadsafe( + self._handleResponse, path, index, status, response) + + def _handleError(self, imError: Status, chipError: PyChipError, exception: Exception): + if self._future.done(): + # TODO Right now this even callback happens if there was a real IM Status error on one command. + # We need to update OnError to allow providing a CommandRef that we can try associating with it. + logger.exception(f"Recieved another error, but we have sent error. imError:{imError}, chipError {chipError}") + return + if exception: + self._future.set_exception(exception) + elif chipError != 0: + self._future.set_exception(chipError.to_exception()) + else: + try: + # If you got an exception from this call other than AttributeError please + # add it to the except block below. We changed Exception->AttributeError as + # that is what we thought we are trying to catch here. self._future.set_exception( chip.interaction_model.InteractionModelError(chip.interaction_model.Status(imError.IMStatus), imError.ClusterStatus)) - except Exception: + except AttributeError: logger.exception("Failed to map interaction model status received: %s. Remapping to Failure." % imError) self._future.set_exception(chip.interaction_model.InteractionModelError( chip.interaction_model.Status.Failure, imError.ClusterStatus)) @@ -117,9 +198,18 @@ def handleError(self, status: Status, chipError: PyChipError): self._handleError, status, chipError, None ) + def _handleDone(self): + self._future.set_result(self._responses) + ctypes.pythonapi.Py_DecRef(ctypes.py_object(self)) + + def handleDone(self): + self._event_loop.call_soon_threadsafe( + self._handleDone + ) + _OnCommandSenderResponseCallbackFunct = CFUNCTYPE( - None, py_object, c_uint16, c_uint32, c_uint32, c_uint16, c_uint8, c_void_p, c_uint32) + None, py_object, c_uint16, c_uint32, c_uint32, c_size_t, c_uint16, c_uint8, c_void_p, c_uint32) _OnCommandSenderErrorCallbackFunct = CFUNCTYPE( None, py_object, c_uint16, c_uint8, PyChipError) _OnCommandSenderDoneCallbackFunct = CFUNCTYPE( @@ -127,10 +217,10 @@ def handleError(self, status: Status, chipError: PyChipError): @_OnCommandSenderResponseCallbackFunct -def _OnCommandSenderResponseCallback(closure, endpoint: int, cluster: int, command: int, +def _OnCommandSenderResponseCallback(closure, endpoint: int, cluster: int, command: int, index: int, imStatus: int, clusterStatus: int, payload, size): data = ctypes.string_at(payload, size) - closure.handleResponse(CommandPath(endpoint, cluster, command), Status( + closure.handleResponse(CommandPath(endpoint, cluster, command), index, Status( imStatus, clusterStatus), data[:]) @@ -141,7 +231,7 @@ def _OnCommandSenderErrorCallback(closure, imStatus: int, clusterStatus: int, ch @_OnCommandSenderDoneCallbackFunct def _OnCommandSenderDoneCallback(closure): - ctypes.pythonapi.Py_DecRef(ctypes.py_object(closure)) + closure.handleDone() def TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(future: Future, eventLoop, responseType, device, commandPath, payload): @@ -201,6 +291,59 @@ def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPa )) +def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRequestInfo], + timedRequestTimeoutMs: Optional[int] = None, interactionTimeoutMs: Optional[int] = None, busyWaitMs: Optional[int] = None, + suppressResponse: Optional[bool] = 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. + - None (on a successful response containing no data) + - Raises an exception if any errors are encountered. + + If no response type is provided above, the type will be automatically deduced. + + 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. + ''' + handle = chip.native.GetLibraryHandle() + + responseTypes = [] + commandargs = [] + for command in commands: + clusterCommand = command.Command + responseType = command.ResponseType + if (responseType is not None) and (not issubclass(responseType, ClusterCommand)): + raise ValueError("responseType must be a ClusterCommand or None") + if clusterCommand.must_use_timed_invoke and timedRequestTimeoutMs is None or timedRequestTimeoutMs == 0: + raise chip.interaction_model.InteractionModelError(chip.interaction_model.Status.NeedsTimedInteraction) + + commandPath = chip.interaction_model.CommandPathIBStruct.build({ + "EndpointId": command.EndpointId, + "ClusterId": clusterCommand.cluster_id, + "CommandId": clusterCommand.command_id}) + payloadTLV = clusterCommand.ToTLV() + + commandargs.append(c_char_p(commandPath)) + commandargs.append(c_char_p(bytes(payloadTLV))) + commandargs.append(c_size_t(len(payloadTLV))) + + responseTypes.append(responseType) + + transaction = AsyncBatchCommandsTransaction(future, eventLoop, responseTypes) + ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction)) + + return builtins.chipStack.Call( + lambda: handle.pychip_CommandSender_SendBatchCommands( + py_object(transaction), device, + c_uint16(0 if timedRequestTimeoutMs is None else timedRequestTimeoutMs), + c_uint16(0 if interactionTimeoutMs is None else interactionTimeoutMs), + c_uint16(0 if busyWaitMs is None else busyWaitMs), + c_bool(False if suppressResponse is None else suppressResponse), + c_size_t(len(commands)), *commandargs) + ) + + 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) @@ -227,6 +370,8 @@ 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, c_bool]) + setter.Set('pychip_CommandSender_SendBatchCommands', + PyChipError, [py_object, c_void_p, c_uint16, c_uint16, c_uint16, c_bool, c_size_t]) setter.Set('pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke', PyChipError, [py_object, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16, c_bool]) setter.Set('pychip_CommandSender_SendGroupCommand', diff --git a/src/controller/python/chip/clusters/command.cpp b/src/controller/python/chip/clusters/command.cpp index c824c251995932..168a3cf818eb5b 100644 --- a/src/controller/python/chip/clusters/command.cpp +++ b/src/controller/python/chip/clusters/command.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -38,6 +39,10 @@ PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * de const uint8_t * payload, size_t length, uint16_t interactionTimeoutMs, uint16_t busyWaitMs, bool suppressResponse); +PyChipError pychip_CommandSender_SendBatchCommands(void * appContext, DeviceProxy * device, uint16_t timedRequestTimeoutMs, + uint16_t interactionTimeoutMs, uint16_t busyWaitMs, bool suppressResponse, + size_t n, ...); + PyChipError pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke( void * appContext, DeviceProxy * device, chip::EndpointId endpointId, chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload, size_t length, uint16_t interactionTimeoutMs, uint16_t busyWaitMs, bool suppressResponse); @@ -51,7 +56,7 @@ namespace chip { namespace python { using OnCommandSenderResponseCallback = void (*)(PyObject appContext, chip::EndpointId endpointId, chip::ClusterId clusterId, - chip::CommandId commandId, + chip::CommandId commandId, size_t index, std::underlying_type_t status, chip::ClusterStatus clusterStatus, const uint8_t * payload, uint32_t length); using OnCommandSenderErrorCallback = void (*)(PyObject appContext, @@ -63,14 +68,25 @@ OnCommandSenderResponseCallback gOnCommandSenderResponseCallback = nullptr; OnCommandSenderErrorCallback gOnCommandSenderErrorCallback = nullptr; OnCommandSenderDoneCallback gOnCommandSenderDoneCallback = nullptr; +struct __attribute__((packed)) CommandPath +{ + chip::EndpointId endpointId; + chip::ClusterId clusterId; + chip::CommandId commandId; +}; + class CommandSenderCallback : public CommandSender::Callback { public: - CommandSenderCallback(PyObject appContext) : mAppContext(appContext) {} + CommandSenderCallback(PyObject appContext, bool isBatchedCommands) : + mAppContext(appContext), mIsBatchedCommands(isBatchedCommands) + {} - void OnResponse(CommandSender * apCommandSender, const ConcreteCommandPath & aPath, const app::StatusIB & aStatus, - TLV::TLVReader * aData) override + void OnResponseWithAdditionalData(CommandSender * apCommandSender, const ConcreteCommandPath & aPath, + const app::StatusIB & aStatus, TLV::TLVReader * aData, + const CommandSender::AdditionalResponseData & aAdditionalResponseData) override { + CHIP_ERROR err = CHIP_NO_ERROR; uint8_t buffer[CHIP_CONFIG_DEFAULT_UDP_MTU_SIZE]; uint32_t size = 0; // When the apData is nullptr, means we did not receive a valid attribute data from server, status will be some error @@ -80,7 +96,7 @@ class CommandSenderCallback : public CommandSender::Callback // Python need to read from full TLV data the TLVReader may contain some unclean states. TLV::TLVWriter writer; writer.Init(buffer); - CHIP_ERROR err = writer.CopyContainer(TLV::AnonymousTag(), *aData); + err = writer.CopyContainer(TLV::AnonymousTag(), *aData); if (err != CHIP_NO_ERROR) { this->OnError(apCommandSender, err); @@ -89,8 +105,23 @@ class CommandSenderCallback : public CommandSender::Callback size = writer.GetLengthWritten(); } + if (err != CHIP_NO_ERROR) + { + this->OnError(apCommandSender, err); + return; + } + + chip::CommandRef commandRef = aAdditionalResponseData.mCommandRef.ValueOr(0); + size_t index = 0; + err = GetIndexFromCommandRef(commandRef, index); + if (err != CHIP_NO_ERROR && mIsBatchedCommands) + { + this->OnError(apCommandSender, err); + return; + } + gOnCommandSenderResponseCallback( - mAppContext, aPath.mEndpointId, aPath.mClusterId, aPath.mCommandId, to_underlying(aStatus.mStatus), + mAppContext, aPath.mEndpointId, aPath.mClusterId, aPath.mCommandId, index, to_underlying(aStatus.mStatus), aStatus.mClusterStatus.HasValue() ? aStatus.mClusterStatus.Value() : chip::python::kUndefinedClusterStatus, buffer, size); } @@ -114,8 +145,32 @@ class CommandSenderCallback : public CommandSender::Callback delete this; }; + CHIP_ERROR GetIndexFromCommandRef(uint16_t aCommandRef, size_t & aIndex) + { + auto search = mCommandRefToIndex.find(aCommandRef); + if (search == mCommandRefToIndex.end()) + { + return CHIP_ERROR_KEY_NOT_FOUND; + } + aIndex = mCommandRefToIndex[aCommandRef]; + return CHIP_NO_ERROR; + } + + CHIP_ERROR AddCommandRefToIndexLookup(uint16_t aCommandRef, size_t aIndex) + { + auto search = mCommandRefToIndex.find(aCommandRef); + if (search != mCommandRefToIndex.end()) + { + return CHIP_ERROR_DUPLICATE_KEY_ID; + } + mCommandRefToIndex[aCommandRef] = aIndex; + return CHIP_NO_ERROR; + } + private: PyObject mAppContext = nullptr; + std::unordered_map mCommandRefToIndex; + bool mIsBatchedCommands; }; } // namespace python @@ -142,7 +197,8 @@ PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * de VerifyOrReturnError(device->GetSecureSession().HasValue(), ToPyChipError(CHIP_ERROR_MISSING_SECURE_SESSION)); - std::unique_ptr callback = std::make_unique(appContext); + std::unique_ptr callback = + std::make_unique(appContext, /* isBatchedCommands =*/false); std::unique_ptr sender = std::make_unique(callback.get(), device->GetExchangeManager(), /* is timed request */ timedRequestTimeoutMs != 0, suppressResponse); @@ -172,12 +228,94 @@ PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * de sender.release(); callback.release(); + // TODO(#30985): Reconsider the purpose of busyWait and if it can be broken out into it's + // own method/primitive. + if (busyWaitMs) + { + usleep(busyWaitMs * 1000); + } + +exit: + return ToPyChipError(err); +} + +PyChipError pychip_CommandSender_SendBatchCommands(void * appContext, DeviceProxy * device, uint16_t timedRequestTimeoutMs, + uint16_t interactionTimeoutMs, uint16_t busyWaitMs, bool suppressResponse, + size_t n, ...) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + + VerifyOrReturnError(device->GetSecureSession().HasValue(), ToPyChipError(CHIP_ERROR_MISSING_SECURE_SESSION)); + auto remoteSessionParameters = device->GetSecureSession().Value()->GetRemoteSessionParameters(); + CommandSender::ConfigParameters config; + config.SetRemoteMaxPathsPerInvoke(remoteSessionParameters.GetMaxPathsPerInvoke()); + + std::unique_ptr callback = + std::make_unique(appContext, /* isBatchedCommands =*/true); + std::unique_ptr sender = + std::make_unique(callback.get(), device->GetExchangeManager(), + /* is timed request */ timedRequestTimeoutMs != 0, suppressResponse); + + // TODO(#30986): Move away from passing these command through variadic arguments. + va_list args; + va_start(args, n); + + SuccessOrExit(err = sender->SetCommandSenderConfig(config)); + + { + for (size_t i = 0; i < n; i++) + { + void * commandPath = va_arg(args, void *); + void * tlv = va_arg(args, void *); + int length = va_arg(args, int); + + python::CommandPath invokeRequestInfoObj; + memcpy(&invokeRequestInfoObj, commandPath, sizeof(python::CommandPath)); + const uint8_t * tlvBuffer = reinterpret_cast(tlv); + + app::CommandPathParams cmdParams = { invokeRequestInfoObj.endpointId, /* group id */ 0, invokeRequestInfoObj.clusterId, + invokeRequestInfoObj.commandId, (app::CommandPathFlags::kEndpointIdValid) }; + + CommandSender::AdditionalCommandParameters additionalParams; + + SuccessOrExit(err = sender->PrepareCommand(cmdParams, additionalParams)); + { + auto writer = sender->GetCommandDataIBTLVWriter(); + VerifyOrExit(writer != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + TLV::TLVReader reader; + reader.Init(tlvBuffer, static_cast(length)); + reader.Next(); + SuccessOrExit(err = writer->CopyContainer(TLV::ContextTag(CommandDataIB::Tag::kFields), reader)); + } + + SuccessOrExit(err = sender->FinishCommand(timedRequestTimeoutMs != 0 ? Optional(timedRequestTimeoutMs) + : Optional::Missing(), + additionalParams)); + + // CommandSender provides us with the CommandReference for this associated command. In order to match responses + // we have to add CommandRef to index lookup. + VerifyOrExit(additionalParams.mCommandRef.HasValue(), err = CHIP_ERROR_INVALID_ARGUMENT); + SuccessOrExit(err = callback->AddCommandRefToIndexLookup(additionalParams.mCommandRef.Value(), i)); + } + } + + SuccessOrExit(err = sender->SendCommandRequest(device->GetSecureSession().Value(), + interactionTimeoutMs != 0 + ? MakeOptional(System::Clock::Milliseconds32(interactionTimeoutMs)) + : Optional::Missing())); + + sender.release(); + callback.release(); + + // TODO(#30985): Reconsider the purpose of busyWait and if it can be broken out into it's + // own method/primitive. if (busyWaitMs) { usleep(busyWaitMs * 1000); } exit: + va_end(args); return ToPyChipError(err); } @@ -191,8 +329,9 @@ PyChipError pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke( VerifyOrReturnError(device->GetSecureSession().HasValue(), ToPyChipError(CHIP_ERROR_MISSING_SECURE_SESSION)); - std::unique_ptr callback = std::make_unique(appContext); - std::unique_ptr sender = std::make_unique(callback.get(), device->GetExchangeManager(), + std::unique_ptr callback = + std::make_unique(appContext, /* isBatchedCommands =*/false); + std::unique_ptr sender = std::make_unique(callback.get(), device->GetExchangeManager(), /* is timed request */ true, suppressResponse); app::CommandPathParams cmdParams = { endpointId, /* group id */ 0, clusterId, commandId, @@ -219,6 +358,8 @@ PyChipError pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke( sender.release(); callback.release(); + // TODO(#30985): Reconsider the purpose of busyWait and if it can be broken out into it's + // own method/primitive. if (busyWaitMs) { usleep(busyWaitMs * 1000); @@ -264,6 +405,8 @@ PyChipError pychip_CommandSender_SendGroupCommand(chip::GroupId groupId, chip::C SuccessOrExit(err = sender->SendGroupCommandRequest(chip::SessionHandle(session))); } + // TODO(#30985): Reconsider the purpose of busyWait and if it can be broken out into it's + // own method/primitive. if (busyWaitMs) { usleep(busyWaitMs * 1000); diff --git a/src/controller/python/chip/interaction_model/__init__.py b/src/controller/python/chip/interaction_model/__init__.py index a2eb5d7ab4ff04..97c185bdcd96e5 100644 --- a/src/controller/python/chip/interaction_model/__init__.py +++ b/src/controller/python/chip/interaction_model/__init__.py @@ -26,12 +26,12 @@ from chip.exceptions import ChipStackException -from .delegate import (AttributePath, AttributePathIBstruct, DataVersionFilterIBstruct, EventPath, EventPathIBstruct, - SessionParameters, SessionParametersStruct) +from .delegate import (AttributePath, AttributePathIBstruct, CommandPathIBStruct, DataVersionFilterIBstruct, EventPath, + EventPathIBstruct, SessionParameters, SessionParametersStruct) -__all__ = ["AttributePath", "AttributePathIBstruct", "DataVersionFilterIBstruct", - "EventPath", "EventPathIBstruct", "InteractionModelError", - "SessionParameters", "SessionParametersStruct", "Status"] +__all__ = ["AttributePath", "AttributePathIBstruct", "CommandPathIBStruct", + "DataVersionFilterIBstruct", "EventPath", "EventPathIBstruct", + "InteractionModelError", "SessionParameters", "SessionParametersStruct", "Status"] # defined src/controller/python/chip/interaction_model/Delegate.h diff --git a/src/controller/python/chip/interaction_model/delegate.py b/src/controller/python/chip/interaction_model/delegate.py index 74acb3921ec820..6ce577b94580de 100644 --- a/src/controller/python/chip/interaction_model/delegate.py +++ b/src/controller/python/chip/interaction_model/delegate.py @@ -46,6 +46,12 @@ "AttributeId" / Int32ul, ) +CommandPathIBStruct = Struct( + "EndpointId" / Int16ul, + "ClusterId" / Int32ul, + "CommandId" / Int32ul, +) + # AttributePath should not contain padding AttributePathIBstruct = Struct( "EndpointId" / Int16ul, diff --git a/src/lib/core/BUILD.gn b/src/lib/core/BUILD.gn index 85f8b8c6449592..88e3e37e8c3736 100644 --- a/src/lib/core/BUILD.gn +++ b/src/lib/core/BUILD.gn @@ -68,6 +68,7 @@ buildconfig_header("chip_buildconfig") { "CHIP_CONFIG_BIG_ENDIAN_TARGET=${chip_target_is_big_endian}", "CHIP_CONFIG_TLV_VALIDATE_CHAR_STRING_ON_WRITE=${chip_tlv_validate_char_string_on_write}", "CHIP_CONFIG_TLV_VALIDATE_CHAR_STRING_ON_READ=${chip_tlv_validate_char_string_on_read}", + "CHIP_CONFIG_SENDING_BATCH_COMMANDS_ENABLED=${chip_enable_sending_batch_commands}", ] visibility = [ ":chip_config_header" ] diff --git a/src/lib/core/DataModelTypes.h b/src/lib/core/DataModelTypes.h index 1d84c00e114afc..e0cd1d2fa1d773 100644 --- a/src/lib/core/DataModelTypes.h +++ b/src/lib/core/DataModelTypes.h @@ -32,6 +32,7 @@ typedef uint32_t AttributeId; typedef uint32_t ClusterId; typedef uint8_t ClusterStatus; typedef uint32_t CommandId; +typedef uint16_t CommandRef; typedef uint64_t CompressedFabricId; typedef uint32_t DataVersion; typedef uint32_t DeviceTypeId; diff --git a/src/lib/core/core.gni b/src/lib/core/core.gni index 51ef44493b8461..8c7032ebac675a 100644 --- a/src/lib/core/core.gni +++ b/src/lib/core/core.gni @@ -98,6 +98,9 @@ declare_args() { # - SHALL be valid UTF8 chip_tlv_validate_char_string_on_write = true chip_tlv_validate_char_string_on_read = false + + chip_enable_sending_batch_commands = + current_os == "linux" || current_os == "mac" || current_os == "ios" } if (chip_target_style == "") {