diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn index 58d9cb1f7c4622..a90f59f2f33952 100644 --- a/src/controller/python/BUILD.gn +++ b/src/controller/python/BUILD.gn @@ -63,6 +63,11 @@ shared_library("ChipDeviceCtrl") { "ChipDeviceController-StorageDelegate.cpp", "ChipDeviceController-StorageDelegate.h", "OpCredsBinding.cpp", + "chip/bdx/bdx-transfer.cpp", + "chip/bdx/bdx-transfer.h", + "chip/bdx/bdx.cpp", + "chip/bdx/test-bdx-transfer-server.cpp", + "chip/bdx/test-bdx-transfer-server.h", "chip/clusters/attribute.cpp", "chip/clusters/command.cpp", "chip/commissioning/PlaceholderOperationalCredentialsIssuer.h", @@ -166,6 +171,10 @@ chip_python_wheel_action("chip-core") { "chip/ChipStack.py", "chip/FabricAdmin.py", "chip/__init__.py", + "chip/bdx/Bdx.py", + "chip/bdx/BdxProtocol.py", + "chip/bdx/BdxTransfer.py", + "chip/bdx/__init__.py", "chip/ble/__init__.py", "chip/ble/commissioning/__init__.py", "chip/ble/get_adapters.py", @@ -235,6 +244,7 @@ chip_python_wheel_action("chip-core") { py_packages = [ "chip", + "chip.bdx", "chip.ble", "chip.ble.commissioning", "chip.configuration", diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index 8c751f7f791dd0..1e8f74fbd28c7d 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -48,6 +48,7 @@ from . import FabricAdmin from . import clusters as Clusters from . import discovery +from .bdx import Bdx from .clusters import Attribute as ClusterAttribute from .clusters import ClusterObjects as ClusterObjects from .clusters import Command as ClusterCommand @@ -1343,6 +1344,36 @@ def WriteGroupAttribute( # An empty list is the expected return for sending group write attribute. return [] + def TestOnlyPrepareToReceiveBdxData(self) -> asyncio.Future: + ''' + Sets up the system to expect a node to initiate a BDX transfer. The transfer will send data here. + + Returns: + - a future that will yield a BdxTransfer with the init message from the transfer. + ''' + self.CheckIsActive() + + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + + Bdx.PrepareToReceiveBdxData(future).raise_on_error() + return future + + def TestOnlyPrepareToSendBdxData(self, data: bytes) -> asyncio.Future: + ''' + Sets up the system to expect a node to initiate a BDX transfer. The transfer will send data to the node. + + Returns: + - a future that will yield a BdxTransfer with the init message from the transfer. + ''' + self.CheckIsActive() + + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + + Bdx.PrepareToSendBdxData(future, data).raise_on_error() + return future + def _parseAttributePathTuple(self, pathTuple: typing.Union[ None, # Empty tuple, all wildcard typing.Tuple[int], # Endpoint diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py index e029b77f0a7476..94ad734e37a617 100644 --- a/src/controller/python/chip/ChipStack.py +++ b/src/controller/python/chip/ChipStack.py @@ -36,6 +36,7 @@ import chip.native from chip.native import PyChipError +from .bdx import Bdx from .clusters import Attribute as ClusterAttribute from .clusters import Command as ClusterCommand from .exceptions import ChipStackError, ChipStackException, DeviceError @@ -175,6 +176,7 @@ def HandleChipThreadRun(callback): im.InitIMDelegate() ClusterAttribute.Init() ClusterCommand.Init() + Bdx.Init() builtins.chipStack = self diff --git a/src/controller/python/chip/bdx/Bdx.py b/src/controller/python/chip/bdx/Bdx.py new file mode 100644 index 00000000000000..9745273ecc1e33 --- /dev/null +++ b/src/controller/python/chip/bdx/Bdx.py @@ -0,0 +1,228 @@ +# +# 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. +# + +import asyncio +import builtins +import ctypes +from asyncio.futures import Future +from ctypes import CFUNCTYPE, POINTER, c_char_p, c_size_t, c_uint8, c_uint16, c_uint64, c_void_p, py_object +from typing import Callable, Optional + +import chip +from chip.native import PyChipError + +from . import BdxTransfer + +c_uint8_p = POINTER(c_uint8) + + +_OnTransferObtainedCallbackFunct = CFUNCTYPE( + None, py_object, c_void_p, c_uint8, c_uint16, c_uint64, c_uint64, c_uint8_p, c_uint16, c_uint8_p, c_size_t) +_OnFailedToObtainTransferCallbackFunct = CFUNCTYPE(None, py_object, PyChipError) +_OnDataReceivedCallbackFunct = CFUNCTYPE(None, py_object, c_uint8_p, c_size_t) +_OnTransferCompletedCallbackFunct = CFUNCTYPE(None, py_object, PyChipError) + + +class AsyncTransferObtainedTransaction: + ''' The Python context when obtaining a transfer. This is passed into the C++ code to be sent back to Python as part + of the callback when a transfer is obtained, and sets the result of the future after being called back. + ''' + def __init__(self, future, event_loop, data=None): + self._future = future + self._data = data + self._event_loop = event_loop + + def _handleTransfer(self, bdxTransfer, initMessage: BdxTransfer.InitMessage): + transfer = BdxTransfer.BdxTransfer(bdx_transfer=bdxTransfer, init_message=initMessage, data=self._data) + self._future.set_result(transfer) + + def handleTransfer(self, bdxTransfer, initMessage: BdxTransfer.InitMessage): + self._event_loop.call_soon_threadsafe(self._handleTransfer, bdxTransfer, initMessage) + + def _handleError(self, result: PyChipError): + self._future.set_exception(result.to_exception()) + + def handleError(self, result: PyChipError): + self._event_loop.call_soon_threadsafe(self._handleError, result) + + +class AsyncTransferCompletedTransaction: + ''' The Python context when accepting a transfer. This is passed into the C++ code to be sent back to Python as part + of the callback when the transfer completes, and sets the result of the future after being called back. + ''' + def __init__(self, future, event_loop): + self._future = future + self._event_loop = event_loop + + def _handleResult(self, result: PyChipError): + if result.is_success: + self._future.set_result(result) + else: + self._future.set_exception(result.to_exception()) + + def handleResult(self, result: PyChipError): + self._event_loop.call_soon_threadsafe(self._handleResult, result) + + +@_OnTransferObtainedCallbackFunct +def _OnTransferObtainedCallback(transaction: AsyncTransferObtainedTransaction, bdxTransfer, transferControlFlags: int, + maxBlockSize: int, startOffset: int, length: int, fileDesignator, fileDesignatorLength: int, + metadata, metadataLength: int): + fileDesignatorData = ctypes.string_at(fileDesignator, fileDesignatorLength) + metadataData = ctypes.string_at(metadata, metadataLength) + + initMessage = BdxTransfer.InitMessage( + transferControlFlags, + maxBlockSize, + startOffset, + length, + fileDesignatorData[:], + metadataData[:], + ) + + transaction.handleTransfer(bdxTransfer, initMessage) + + +@_OnFailedToObtainTransferCallbackFunct +def _OnFailedToObtainTransferCallback(transaction: AsyncTransferObtainedTransaction, result: PyChipError): + transaction.handleError(result) + + +@_OnDataReceivedCallbackFunct +def _OnDataReceivedCallback(context, dataBuffer: c_uint8_p, bufferLength: int): + data = ctypes.string_at(dataBuffer, bufferLength) + context(data) + + +@_OnTransferCompletedCallbackFunct +def _OnTransferCompletedCallback(transaction: AsyncTransferCompletedTransaction, result: PyChipError): + transaction.handleResult(result) + + +def _PrepareForBdxTransfer(future: Future, data: Optional[bytes]) -> PyChipError: + ''' Prepares the BDX system for a BDX transfer. The BDX transfer is set as the future's result. This must be called + before the BDX transfer is initiated. + + Returns the CHIP_ERROR result from the C++ side. + ''' + handle = chip.native.GetLibraryHandle() + transaction = AsyncTransferObtainedTransaction(future=future, event_loop=asyncio.get_running_loop(), data=data) + + ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction)) + res = builtins.chipStack.Call( + lambda: handle.pychip_Bdx_ExpectBdxTransfer(ctypes.py_object(transaction)) + ) + if not res.is_success: + ctypes.pythonapi.Py_DecRef(ctypes.py_object(transaction)) + return res + + +def PrepareToReceiveBdxData(future: Future) -> PyChipError: + ''' Prepares the BDX system for a BDX transfer where this device receives data. This must be called before the BDX + transfer is initiated. + + When a BDX transfer is found it's set as the future's result. If an error occurs while waiting it is set as the future's exception. + + Returns an error if there was an issue preparing to wait a BDX transfer. + ''' + return _PrepareForBdxTransfer(future, None) + + +def PrepareToSendBdxData(future: Future, data: bytes) -> PyChipError: + ''' Prepares the BDX system for a BDX transfer where this device sends data. This must be called before the BDX + transfer is initiated. + + When a BDX transfer is found it's set as the future's result. If an error occurs while waiting it is set as the future's exception. + + Returns an error if there was an issue preparing to wait a BDX transfer. + ''' + return _PrepareForBdxTransfer(future, data) + + +def AcceptTransferAndReceiveData(transfer: c_void_p, dataReceivedClosure: Callable[[bytes], None], transferComplete: Future): + ''' Accepts a BDX transfer with the intent of receiving data. + + The data will be returned block-by-block in dataReceivedClosure. + transferComplete will be fulfilled when the transfer completes. + + Returns an error if one is encountered while accepting the transfer. + ''' + handle = chip.native.GetLibraryHandle() + complete_transaction = AsyncTransferCompletedTransaction(future=transferComplete, event_loop=asyncio.get_running_loop()) + ctypes.pythonapi.Py_IncRef(ctypes.py_object(dataReceivedClosure)) + ctypes.pythonapi.Py_IncRef(ctypes.py_object(complete_transaction)) + res = builtins.chipStack.Call( + lambda: handle.pychip_Bdx_AcceptTransferAndReceiveData(transfer, dataReceivedClosure, complete_transaction) + ) + if not res.is_success: + ctypes.pythonapi.Py_DecRef(ctypes.py_object(dataReceivedClosure)) + ctypes.pythonapi.Py_DecRef(ctypes.py_object(complete_transaction)) + return res + + +def AcceptTransferAndSendData(transfer: c_void_p, data: bytearray, transferComplete: Future): + ''' Accepts a BDX transfer with the intent of sending data. + + The data will be copied by C++. + transferComplete will be fulfilled when the transfer completes. + + Returns an error if one is encountered while accepting the transfer. + ''' + handle = chip.native.GetLibraryHandle() + complete_transaction = AsyncTransferCompletedTransaction(future=transferComplete, event_loop=asyncio.get_running_loop()) + ctypes.pythonapi.Py_IncRef(ctypes.py_object(complete_transaction)) + res = builtins.chipStack.Call( + lambda: handle.pychip_Bdx_AcceptTransferAndSendData(transfer, c_char_p(data), len(data), complete_transaction) + ) + if not res.is_success: + ctypes.pythonapi.Py_DecRef(ctypes.py_object(complete_transaction)) + return res + + +async def RejectTransfer(transfer: c_void_p): + ''' Rejects a BDX transfer. + + Returns an error if one is encountered while rejecting the transfer. + ''' + handle = chip.native.GetLibraryHandle() + return await builtins.chipStack.CallAsyncWithResult( + lambda: handle.pychip_Bdx_RejectTransfer(transfer) + ) + + +def Init(): + handle = chip.native.GetLibraryHandle() + # Uses one of the type decorators as an indicator for everything being initialized. + if not handle.pychip_Bdx_ExpectBdxTransfer.argtypes: + setter = chip.native.NativeLibraryHandleMethodArguments(handle) + + setter.Set('pychip_Bdx_ExpectBdxTransfer', + PyChipError, [py_object]) + setter.Set('pychip_Bdx_StopExpectingBdxTransfer', + PyChipError, [py_object]) + setter.Set('pychip_Bdx_AcceptTransferAndReceiveData', + PyChipError, [c_void_p, py_object, py_object]) + setter.Set('pychip_Bdx_AcceptTransferAndSendData', + PyChipError, [c_void_p, c_uint8_p, c_size_t]) + setter.Set('pychip_Bdx_RejectTransfer', + PyChipError, [c_void_p]) + setter.Set('pychip_Bdx_InitCallbacks', None, [ + _OnTransferObtainedCallbackFunct, _OnFailedToObtainTransferCallbackFunct, _OnDataReceivedCallbackFunct, + _OnTransferCompletedCallbackFunct]) + + handle.pychip_Bdx_InitCallbacks( + _OnTransferObtainedCallback, _OnFailedToObtainTransferCallback, _OnDataReceivedCallback, _OnTransferCompletedCallback) diff --git a/src/controller/python/chip/bdx/BdxProtocol.py b/src/controller/python/chip/bdx/BdxProtocol.py new file mode 100644 index 00000000000000..6d53fda44da692 --- /dev/null +++ b/src/controller/python/chip/bdx/BdxProtocol.py @@ -0,0 +1,23 @@ +# +# 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. +# + +# These BDX constants are defined in the spec. + +# SendInit/ReceiveInit Proposed Transfer Control field structure. +SENDER_DRIVE = 0x10 +RECEIVER_DRIVE = 0x20 +ASYNC = 0x40 diff --git a/src/controller/python/chip/bdx/BdxTransfer.py b/src/controller/python/chip/bdx/BdxTransfer.py new file mode 100644 index 00000000000000..e6f86620648eca --- /dev/null +++ b/src/controller/python/chip/bdx/BdxTransfer.py @@ -0,0 +1,80 @@ +# +# 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. +# + +import asyncio +from ctypes import c_void_p +from dataclasses import dataclass +from typing import Optional + +from . import Bdx + + +@dataclass +class InitMessage: + ''' The details of the init message received at the start of a BDX transfer. + ''' + # The transfer control flag constants SENDER_DRIVE, RECEIVER_DRIVE, and ASYNC are defined in BdxProtocol.py. + TransferControlFlags: int + MaxBlockSize: int + StartOffset: int + Length: int + FileDesignator: bytes + Metadata: bytes + + +class BdxTransfer: + ''' A representation of a BDX transfer. + + This is created when a BDX init message is received, and stores the details of that init message. + The transfer can be accepted by calling accept_and_send_data or accept_and_receive_data. + The transfer can be rejected by calling reject. + ''' + def __init__(self, bdx_transfer: c_void_p, init_message: InitMessage, data: Optional[bytes] = None): + self.init_message = init_message + self._bdx_transfer = bdx_transfer + # _data is a bytearray when receiving data, so the data to send is converted to one as well for consistency. + self._data = bytearray(data) if data else None + + async def accept_and_send_data(self) -> None: + ''' Accepts the transfer with the intent of sending data. + ''' + assert self._data is not None + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + res = Bdx.AcceptTransferAndSendData(self._bdx_transfer, self._data, future) + res.raise_on_error() + await future + + async def accept_and_receive_data(self) -> bytes: + ''' Accepts the transfer with the intent of receiving data. + + Returns the data received when the transfer is complete. + ''' + assert self._data is None + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + self._data = bytearray() + res = Bdx.AcceptTransferAndReceiveData(self._bdx_transfer, lambda data: self._data.extend(data), future) + res.raise_on_error() + await future + return bytes(self._data) + + async def reject(self) -> None: + ''' Rejects the transfer. + ''' + res = await Bdx.RejectTransfer(self._bdx_transfer) + res.raise_on_error() diff --git a/src/controller/python/chip/bdx/__init__.py b/src/controller/python/chip/bdx/__init__.py new file mode 100644 index 00000000000000..53a2cdd27b04a7 --- /dev/null +++ b/src/controller/python/chip/bdx/__init__.py @@ -0,0 +1,27 @@ +# +# 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. +# + +# +# @file +# Provides BDX Python APIs for CHIP. +# + +"""Provides BDX Python APIs for CHIP.""" + +from . import Bdx, BdxProtocol, BdxTransfer + +__all__ = ["BdxTransfer", "InitMessage"] diff --git a/src/controller/python/chip/bdx/bdx-transfer.cpp b/src/controller/python/chip/bdx/bdx-transfer.cpp new file mode 100644 index 00000000000000..25264f8107d932 --- /dev/null +++ b/src/controller/python/chip/bdx/bdx-transfer.cpp @@ -0,0 +1,214 @@ +/* + * + * 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. + */ + +#include + +#include + +namespace chip { +namespace bdx { +namespace { + +constexpr uint32_t kMaxBdxBlockSize = 1024; +constexpr System::Clock::Timeout kBdxPollInterval = System::Clock::Milliseconds32(50); +constexpr System::Clock::Timeout kBdxTimeout = System::Clock::Seconds16(5 * 60); + +} // namespace + +void BdxTransfer::SetDelegate(BdxTransfer::Delegate * delegate) +{ + mDelegate = delegate; +} + +CHIP_ERROR BdxTransfer::AcceptAndReceiveData() +{ + VerifyOrReturnError(mAwaitingAccept, CHIP_ERROR_INCORRECT_STATE); + mAwaitingAccept = false; + + TransferSession::TransferAcceptData acceptData; + acceptData.ControlMode = TransferControlFlags::kSenderDrive; + acceptData.MaxBlockSize = mTransfer.GetTransferBlockSize(); + acceptData.StartOffset = mTransfer.GetStartOffset(); + acceptData.Length = mTransfer.GetTransferLength(); + return mTransfer.AcceptTransfer(acceptData); +} + +CHIP_ERROR BdxTransfer::AcceptAndSendData(const ByteSpan & data_to_send) +{ + VerifyOrReturnError(mAwaitingAccept, CHIP_ERROR_INCORRECT_STATE); + mAwaitingAccept = false; + mData.assign(data_to_send.begin(), data_to_send.end()); + mDataCount = data_to_send.size(); + + TransferSession::TransferAcceptData acceptData; + acceptData.ControlMode = TransferControlFlags::kReceiverDrive; + acceptData.MaxBlockSize = mTransfer.GetTransferBlockSize(); + acceptData.StartOffset = mTransfer.GetStartOffset(); + acceptData.Length = mTransfer.GetTransferLength(); + return mTransfer.AcceptTransfer(acceptData); +} + +CHIP_ERROR BdxTransfer::Reject() +{ + VerifyOrReturnError(mAwaitingAccept, CHIP_ERROR_INCORRECT_STATE); + mAwaitingAccept = false; + return mTransfer.RejectTransfer(StatusCode::kTransferFailedUnknownError); +} + +void BdxTransfer::HandleTransferSessionOutput(TransferSession::OutputEvent & event) +{ + ChipLogDetail(BDX, "Received event %s", event.ToString(event.EventType)); + + switch (event.EventType) + { + case TransferSession::OutputEventType::kInitReceived: + mAwaitingAccept = true; + mDelegate->InitMessageReceived(this, event.transferInitData); + break; + case TransferSession::OutputEventType::kStatusReceived: + ChipLogError(BDX, "Received StatusReport %x", ::chip::to_underlying(event.statusData.statusCode)); + EndSession(ChipError(ChipError::SdkPart::kIMClusterStatus, ::chip::to_underlying(event.statusData.statusCode))); + break; + case TransferSession::OutputEventType::kInternalError: + EndSession(CHIP_ERROR_INTERNAL); + break; + case TransferSession::OutputEventType::kTransferTimeout: + EndSession(CHIP_ERROR_TIMEOUT); + break; + case TransferSession::OutputEventType::kBlockReceived: + if (mDelegate) + { + ByteSpan data(event.blockdata.Data, event.blockdata.Length); + mDelegate->DataReceived(this, data); + mTransfer.PrepareBlockAck(); + } + else + { + ChipLogError(BDX, "Block received without a delegate!"); + } + break; + case TransferSession::OutputEventType::kMsgToSend: + SendMessage(event); + if (event.msgTypeData.HasMessageType(MessageType::BlockAckEOF)) + { + // TODO: Ending the session here means the StandaloneAck for the BlockAckEOF message hasn't been received. + EndSession(CHIP_NO_ERROR); + } + break; + case TransferSession::OutputEventType::kAckEOFReceived: + EndSession(CHIP_NO_ERROR); + break; + case TransferSession::OutputEventType::kQueryWithSkipReceived: + mDataTransferredCount = std::min(mDataTransferredCount + event.bytesToSkip.BytesToSkip, mDataCount); + SendBlock(); + break; + case TransferSession::OutputEventType::kQueryReceived: + SendBlock(); + break; + case TransferSession::OutputEventType::kAckReceived: + case TransferSession::OutputEventType::kAcceptReceived: + case TransferSession::OutputEventType::kNone: + // Nothing to do. + break; + default: + // Should never happen. + ChipLogError(BDX, "Unhandled BDX transfer session event type %d.", static_cast(event.EventType)); + chipDie(); + break; + } +} + +void BdxTransfer::EndSession(CHIP_ERROR result) +{ + if (mDelegate) + { + mDelegate->TransferCompleted(this, result); + } + ResetTransfer(); + if (mExchangeCtx) + { + mExchangeCtx->Close(); + } +} + +void BdxTransfer::OnExchangeClosing(Messaging::ExchangeContext * exchangeContext) +{ + mExchangeCtx = nullptr; +} + +CHIP_ERROR BdxTransfer::SendMessage(TransferSession::OutputEvent & event) +{ + VerifyOrReturnError(mExchangeCtx != nullptr, CHIP_ERROR_INCORRECT_STATE); + + ::chip::Messaging::SendFlags sendFlags; + if (!event.msgTypeData.HasMessageType(Protocols::SecureChannel::MsgType::StatusReport) && + !event.msgTypeData.HasMessageType(MessageType::BlockAckEOF)) + { + sendFlags.Set(Messaging::SendMessageFlags::kExpectResponse); + } + return mExchangeCtx->SendMessage(event.msgTypeData.ProtocolId, event.msgTypeData.MessageType, event.MsgData.Retain(), + sendFlags); +} + +CHIP_ERROR BdxTransfer::SendBlock() +{ + VerifyOrReturnError(mExchangeCtx != nullptr, CHIP_ERROR_INCORRECT_STATE); + + size_t dataRemaining = mDataCount - mDataTransferredCount; + TransferSession::BlockData block; + block.Data = mData.data() + mDataTransferredCount; + block.Length = std::min(mTransfer.GetTransferBlockSize(), dataRemaining); + block.IsEof = block.Length == dataRemaining; + ReturnErrorOnFailure(mTransfer.PrepareBlock(block)); + mDataTransferredCount += block.Length; + ScheduleImmediatePoll(); + return CHIP_NO_ERROR; +} + +CHIP_ERROR BdxTransfer::OnMessageReceived(chip::Messaging::ExchangeContext * exchangeContext, + const chip::PayloadHeader & payloadHeader, chip::System::PacketBufferHandle && payload) +{ + bool has_send_init = payloadHeader.HasMessageType(MessageType::SendInit); + bool has_receive_init = payloadHeader.HasMessageType(MessageType::ReceiveInit); + if (has_send_init || has_receive_init) + { + FabricIndex fabricIndex = exchangeContext->GetSessionHandle()->GetFabricIndex(); + NodeId peerNodeId = exchangeContext->GetSessionHandle()->GetPeer().GetNodeId(); + VerifyOrReturnError(fabricIndex != kUndefinedFabricIndex, CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(peerNodeId != kUndefinedNodeId, CHIP_ERROR_INVALID_ARGUMENT); + + TransferControlFlags flags; + TransferRole role; + if (has_send_init) + { + flags = TransferControlFlags::kSenderDrive; + role = TransferRole::kReceiver; + } + else + { + flags = TransferControlFlags::kReceiverDrive; + role = TransferRole::kSender; + } + ReturnLogErrorOnFailure( + Responder::PrepareForTransfer(mSystemLayer, role, flags, kMaxBdxBlockSize, kBdxTimeout, kBdxPollInterval)); + } + + return Responder::OnMessageReceived(exchangeContext, payloadHeader, std::move(payload)); +} + +} // namespace bdx +} // namespace chip diff --git a/src/controller/python/chip/bdx/bdx-transfer.h b/src/controller/python/chip/bdx/bdx-transfer.h new file mode 100644 index 00000000000000..3ddf849d8cb728 --- /dev/null +++ b/src/controller/python/chip/bdx/bdx-transfer.h @@ -0,0 +1,90 @@ +/* + * + * 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. + */ + +#include + +#include +#include + +#include +#include + +#pragma once + +namespace chip { +namespace bdx { + +// A class that represents a BDX transfer initiated by the other end of the transfer. This implements most of the transfer, +// but uses a delegate for transfer control and data handling. The delegate must call one of ReceiveData, SendData, or Reject +// after or during a call to InitMessageReceived. +class BdxTransfer : public Responder +{ +public: + // The delegate is informed when specific events occur during the transfer. + class Delegate + { + public: + virtual ~Delegate() = default; + + // Called when the SendInit or ReceiveInit message is received. + virtual void InitMessageReceived(BdxTransfer * transfer, TransferSession::TransferInitData init_data) = 0; + // Called when a data block arrives. This is only used when the transfer is sending data to this controller. + virtual void DataReceived(BdxTransfer * transfer, const ByteSpan & block) = 0; + // Called when the transfer completes. The outcome of the transfer (successful or otherwise) is indicated by result. + virtual void TransferCompleted(BdxTransfer * transfer, CHIP_ERROR result) = 0; + }; + + BdxTransfer(System::Layer * systemLayer) : mSystemLayer(systemLayer) {} + + ~BdxTransfer() override = default; + + // Accepts the transfer with the intent of receiving data. This will send an AcceptSend message to the other end of the + // transfer. When a block of data arrives the delegate is invoked with the block. + CHIP_ERROR AcceptAndReceiveData(); + + // Accepts the transfer with the intent of sending data. This will send an AcceptReceive message to the other end of the + // transfer. + CHIP_ERROR AcceptAndSendData(const ByteSpan & data_to_send); + + // Rejects the transfer. + CHIP_ERROR Reject(); + + void SetDelegate(Delegate * delegate); + + // Responder virtual method overrides. + void HandleTransferSessionOutput(TransferSession::OutputEvent & event) override; + void OnExchangeClosing(Messaging::ExchangeContext * exchangeContext) override; + CHIP_ERROR OnMessageReceived(chip::Messaging::ExchangeContext * exchangeContext, const chip::PayloadHeader & payloadHeader, + chip::System::PacketBufferHandle && payload) override; + +private: + void EndSession(CHIP_ERROR result); + CHIP_ERROR SendMessage(TransferSession::OutputEvent & event); + CHIP_ERROR SendBlock(); + + Delegate * mDelegate = nullptr; + bool mAwaitingAccept = false; + + System::Layer * mSystemLayer = nullptr; + + std::vector mData; + size_t mDataCount = 0; + size_t mDataTransferredCount = 0; +}; + +} // namespace bdx +} // namespace chip diff --git a/src/controller/python/chip/bdx/bdx.cpp b/src/controller/python/chip/bdx/bdx.cpp new file mode 100644 index 00000000000000..1d466df06258f0 --- /dev/null +++ b/src/controller/python/chip/bdx/bdx.cpp @@ -0,0 +1,263 @@ +/* + * + * 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. + */ + +#include + +#include + +#include +#include +#include +#include +#include + +// The BDX transfer system is split into: +// * BdxTransfer: A transfer object that contains the information about a transfer and is an ExchangeDelegate. +// It owns the data for a transfer, either copying what was sent from Python or requiring the Python side to +// copy it during a callback. +// * TransferMap: A map that associates the BdxTransfer object with its Python context using TransferInfo objects. +// It owns the TransferInfo objects but doesn't own the BdxTransfer objects or the Python context objects. +// * TransferDelegate: A delegate that calls back into Python when certain events happen in C++. It uses the +// TransferMap but doesn't own it. +// TestBdxTransferServer: A server that listens for incoming BDX messages, creates BdxTransfer objects, and +// informs the delegate when certain events happen. It owns the BdxTransfer objects but not the delegate. A +// BdxTransfer object is created when a BDX message is received and destroyed when the transfer completes or +// fails. +// The TransferMap, TransferDelegate, and TestBdxTransferServer instances are all owned by this file. + +using PyObject = void *; + +namespace chip { +namespace python { + +// The Python callbacks to call when certain events happen. +using OnTransferObtainedCallback = void (*)(PyObject context, void * bdxTransfer, bdx::TransferControlFlags transferControlFlags, + uint16_t maxBlockSize, uint64_t startOffset, uint64_t length, + const uint8_t * fileDesignator, uint16_t fileDesignatorLength, const uint8_t * metadata, + size_t metadataLength); +using OnFailedToObtainTransferCallback = void (*)(PyObject context, PyChipError result); +using OnDataReceivedCallback = void (*)(PyObject context, const uint8_t * dataBuffer, size_t bufferLength); +using OnTransferCompletedCallback = void (*)(PyObject context, PyChipError result); + +// The callback methods provided by python. +OnTransferObtainedCallback gOnTransferObtainedCallback = nullptr; +OnFailedToObtainTransferCallback gOnFailedToObtainTransferCallback = nullptr; +OnDataReceivedCallback gOnDataReceivedCallback = nullptr; +OnTransferCompletedCallback gOnTransferCompletedCallback = nullptr; + +// The information for a single transfer. +struct TransferInfo +{ + // The transfer object. Owned by the transfer server. + bdx::BdxTransfer * Transfer = nullptr; + // The contexts for different python callbacks. Owned by the python side. + PyObject OnTransferObtainedContext = nullptr; + PyObject OnDataReceivedContext = nullptr; + PyObject OnTransferCompletedContext = nullptr; + + bool operator==(const TransferInfo & other) const { return Transfer == other.Transfer; } +}; + +// The set of transfers. +class TransferMap +{ +public: + // Returns the transfer data associated with the given transfer. + TransferInfo * TransferInfoForTransfer(bdx::BdxTransfer * transfer) + { + std::vector::iterator result = std::find_if( + mTransfers.begin(), mTransfers.end(), [transfer](const TransferInfo & data) { return data.Transfer == transfer; }); + VerifyOrReturnValue(result != mTransfers.end(), nullptr); + return &*result; + } + + // Returns the transfer data that has the given context when a transfer is obtained. + TransferInfo * TransferInfoForTransferObtainedContext(PyObject transferObtainedContext) + { + std::vector::iterator result = + std::find_if(mTransfers.begin(), mTransfers.end(), [transferObtainedContext](const TransferInfo & data) { + return data.OnTransferObtainedContext == transferObtainedContext; + }); + VerifyOrReturnValue(result != mTransfers.end(), nullptr); + return &*result; + } + + // This returns the next transfer data that has no associated BdxTransfer. + TransferInfo * NextUnassociatedTransferInfo() + { + std::vector::iterator result = + std::find_if(mTransfers.begin(), mTransfers.end(), [](const TransferInfo & data) { return data.Transfer == nullptr; }); + VerifyOrReturnValue(result != mTransfers.end(), nullptr); + return &*result; + } + + // Creates a new transfer data. + TransferInfo * CreateUnassociatedTransferInfo() { return &mTransfers.emplace_back(); } + + void RemoveTransferInfo(TransferInfo * transferInfo) + { + std::vector::iterator result = std::find(mTransfers.begin(), mTransfers.end(), *transferInfo); + VerifyOrReturn(result != mTransfers.end()); + mTransfers.erase(result); + } + +private: + std::vector mTransfers; +}; + +// A method to release a transfer. +void ReleaseTransfer(System::Layer * systemLayer, bdx::BdxTransfer * transfer); + +// A delegate to forward events from a transfer to the appropriate Python callback and context. +class TransferDelegate : public bdx::BdxTransfer::Delegate +{ +public: + TransferDelegate(TransferMap * transfers) : mTransfers(transfers) {} + ~TransferDelegate() override = default; + + void Init(System::Layer * systemLayer) { mSystemLayer = systemLayer; } + + void InitMessageReceived(bdx::BdxTransfer * transfer, bdx::TransferSession::TransferInitData init_data) override + { + TransferInfo * transferInfo = mTransfers->NextUnassociatedTransferInfo(); + if (gOnTransferObtainedCallback && transferInfo) + { + transferInfo->Transfer = transfer; + gOnTransferObtainedCallback(transferInfo->OnTransferObtainedContext, transfer, init_data.TransferCtlFlags, + init_data.MaxBlockSize, init_data.StartOffset, init_data.Length, init_data.FileDesignator, + init_data.FileDesLength, init_data.Metadata, init_data.MetadataLength); + } + } + + void DataReceived(bdx::BdxTransfer * transfer, const ByteSpan & block) override + { + TransferInfo * transferInfo = mTransfers->TransferInfoForTransfer(transfer); + if (gOnDataReceivedCallback && transferInfo) + { + gOnDataReceivedCallback(transferInfo->OnDataReceivedContext, block.data(), block.size()); + } + } + + void TransferCompleted(bdx::BdxTransfer * transfer, CHIP_ERROR result) override + { + TransferInfo * transferInfo = mTransfers->TransferInfoForTransfer(transfer); + if (!transferInfo && result != CHIP_NO_ERROR) + { + // The transfer failed during initialisation. + transferInfo = mTransfers->NextUnassociatedTransferInfo(); + if (gOnFailedToObtainTransferCallback && transferInfo) + { + gOnFailedToObtainTransferCallback(transferInfo->OnTransferObtainedContext, ToPyChipError(result)); + } + } + else if (gOnTransferCompletedCallback && transferInfo) + { + gOnTransferCompletedCallback(transferInfo->OnTransferCompletedContext, ToPyChipError(result)); + mTransfers->RemoveTransferInfo(transferInfo); + } + ReleaseTransfer(mSystemLayer, transfer); + } + +private: + TransferMap * mTransfers = nullptr; + System::Layer * mSystemLayer = nullptr; +}; + +TransferMap gTransfers; +TransferDelegate gBdxTransferDelegate(&gTransfers); +bdx::TestBdxTransferServer gBdxTransferServer(&gBdxTransferDelegate); + +void ReleaseTransfer(System::Layer * systemLayer, bdx::BdxTransfer * transfer) +{ + systemLayer->ScheduleWork( + [](auto * theSystemLayer, auto * appState) -> void { + auto * theTransfer = static_cast(appState); + gBdxTransferServer.Release(theTransfer); + }, + transfer); +} + +} // namespace python +} // namespace chip + +using namespace chip::python; + +// These methods are expected to be called from Python. +extern "C" { + +// Initialises the BDX system. +void pychip_Bdx_InitCallbacks(OnTransferObtainedCallback onTransferObtainedCallback, + OnFailedToObtainTransferCallback onFailedToObtainTransferCallback, + OnDataReceivedCallback onDataReceivedCallback, + OnTransferCompletedCallback onTransferCompletedCallback) +{ + gOnTransferObtainedCallback = onTransferObtainedCallback; + gOnFailedToObtainTransferCallback = onFailedToObtainTransferCallback; + gOnDataReceivedCallback = onDataReceivedCallback; + gOnTransferCompletedCallback = onTransferCompletedCallback; + chip::Controller::DeviceControllerFactory & factory = chip::Controller::DeviceControllerFactory::GetInstance(); + chip::System::Layer * systemLayer = factory.GetSystemState()->SystemLayer(); + gBdxTransferDelegate.Init(systemLayer); + gBdxTransferServer.Init(systemLayer, factory.GetSystemState()->ExchangeMgr()); +} + +// Prepares the BDX system to expect a new transfer. +PyChipError pychip_Bdx_ExpectBdxTransfer(PyObject transferObtainedContext) +{ + TransferInfo * transferInfo = gTransfers.CreateUnassociatedTransferInfo(); + VerifyOrReturnValue(transferInfo != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + transferInfo->OnTransferObtainedContext = transferObtainedContext; + gBdxTransferServer.ExpectATransfer(); + return ToPyChipError(CHIP_NO_ERROR); +} + +// Stops expecting a transfer. +PyChipError pychip_Bdx_StopExpectingBdxTransfer(PyObject transferObtainedContext) +{ + TransferInfo * transferInfo = gTransfers.TransferInfoForTransferObtainedContext(transferObtainedContext); + VerifyOrReturnValue(transferInfo != nullptr, ToPyChipError(CHIP_ERROR_NOT_FOUND)); + gBdxTransferServer.StopExpectingATransfer(); + gTransfers.RemoveTransferInfo(transferInfo); + return ToPyChipError(CHIP_NO_ERROR); +} + +// Accepts a transfer with the intent to receive data from the other device. +PyChipError pychip_Bdx_AcceptTransferAndReceiveData(chip::bdx::BdxTransfer * transfer, PyObject dataReceivedContext, + PyObject transferCompletedContext) +{ + TransferInfo * transferInfo = gTransfers.TransferInfoForTransfer(transfer); + transferInfo->OnDataReceivedContext = dataReceivedContext; + transferInfo->OnTransferCompletedContext = transferCompletedContext; + return ToPyChipError(transfer->AcceptAndReceiveData()); +} + +// Accepts a transfer with the intent to send data to the other device. +PyChipError pychip_Bdx_AcceptTransferAndSendData(chip::bdx::BdxTransfer * transfer, const uint8_t * dataBuffer, size_t dataLength, + PyObject transferCompletedContext) +{ + TransferInfo * transferInfo = gTransfers.TransferInfoForTransfer(transfer); + transferInfo->OnTransferCompletedContext = transferCompletedContext; + chip::ByteSpan data(dataBuffer, dataLength); + return ToPyChipError(transfer->AcceptAndSendData(data)); +} + +// Rejects a transfer. +PyChipError pychip_Bdx_RejectTransfer(chip::bdx::BdxTransfer * transfer) +{ + return ToPyChipError(transfer->Reject()); +} +} diff --git a/src/controller/python/chip/bdx/test-bdx-transfer-server.cpp b/src/controller/python/chip/bdx/test-bdx-transfer-server.cpp new file mode 100644 index 00000000000000..86a0c9b9fbe61a --- /dev/null +++ b/src/controller/python/chip/bdx/test-bdx-transfer-server.cpp @@ -0,0 +1,95 @@ +/* + * + * 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. + */ + +#include + +namespace chip { +namespace bdx { + +TestBdxTransferServer::TestBdxTransferServer(BdxTransfer::Delegate * bdxTransferDelegate) : + mBdxTransferDelegate(bdxTransferDelegate) +{} + +TestBdxTransferServer::~TestBdxTransferServer() +{ + mTransferPool.ReleaseAll(); +} + +CHIP_ERROR TestBdxTransferServer::Init(System::Layer * systemLayer, Messaging::ExchangeManager * exchangeManager) +{ + VerifyOrReturnError(systemLayer != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(exchangeManager != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + mSystemLayer = systemLayer; + mExchangeManager = exchangeManager; + // This removes the BdxTransferServer registered as part of CHIPDeviceControllerFactory. + mExchangeManager->UnregisterUnsolicitedMessageHandlerForType(MessageType::SendInit); + return mExchangeManager->RegisterUnsolicitedMessageHandlerForProtocol(Protocols::BDX::Id, this); +} + +void TestBdxTransferServer::Shutdown() +{ + VerifyOrReturn(mExchangeManager != nullptr); + LogErrorOnFailure(mExchangeManager->UnregisterUnsolicitedMessageHandlerForProtocol(Protocols::BDX::Id)); + mExchangeManager = nullptr; +} + +void TestBdxTransferServer::ExpectATransfer() +{ + ++mExpectedTransfers; +} + +void TestBdxTransferServer::StopExpectingATransfer() +{ + if (mExpectedTransfers > 0) + { + --mExpectedTransfers; + } +} + +void TestBdxTransferServer::Release(BdxTransfer * bdxTransfer) +{ + mTransferPool.ReleaseObject(bdxTransfer); +} + +CHIP_ERROR TestBdxTransferServer::OnUnsolicitedMessageReceived(const PayloadHeader & payloadHeader, + Messaging::ExchangeDelegate *& delegate) +{ + VerifyOrReturnValue(mExpectedTransfers != 0, CHIP_ERROR_HANDLER_NOT_SET); + + BdxTransfer * transfer = mTransferPool.CreateObject(mSystemLayer); + if (transfer == nullptr) + { + ChipLogError(BDX, "Failed to allocate BDX transfer. The pool (size %d) is exhausted.", static_cast(kTransferPoolSize)); + return CHIP_ERROR_NO_MEMORY; + } + transfer->SetDelegate(mBdxTransferDelegate); + delegate = transfer; + + --mExpectedTransfers; + + return CHIP_NO_ERROR; +} + +void TestBdxTransferServer::OnExchangeCreationFailed(Messaging::ExchangeDelegate * delegate) +{ + BdxTransfer * bdxTransfer = static_cast(delegate); + mBdxTransferDelegate->TransferCompleted(bdxTransfer, CHIP_ERROR_CONNECTION_ABORTED); + Release(bdxTransfer); +} + +} // namespace bdx +} // namespace chip diff --git a/src/controller/python/chip/bdx/test-bdx-transfer-server.h b/src/controller/python/chip/bdx/test-bdx-transfer-server.h new file mode 100644 index 00000000000000..7caa298fae2216 --- /dev/null +++ b/src/controller/python/chip/bdx/test-bdx-transfer-server.h @@ -0,0 +1,69 @@ +/* + * + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace chip { +namespace bdx { + +// This class handles unsolicited BDX messages. It keeps track of the number of expect transfers, and will only allocate +// BdxTransfer objects if a transfer is expected. +// +// The controller must inform this manager when a transfer is expected: +// bdxTransferServer->ExpectATransfer(); +// At which point the next unsolicited BDX init message will allocate a BdxTransfer object. +class TestBdxTransferServer : public Messaging::UnsolicitedMessageHandler +{ +public: + TestBdxTransferServer(BdxTransfer::Delegate * bdxTransferDelegate); + ~TestBdxTransferServer() override; + + CHIP_ERROR Init(System::Layer * systemLayer, Messaging::ExchangeManager * exchangeManager); + void Shutdown(); + + // These keep track of the number of expected transfers. A transfer must be expected before this will allocate a + // BdxTransfer object. + void ExpectATransfer(); + void StopExpectingATransfer(); + + void Release(BdxTransfer * bdxTransfer); + + CHIP_ERROR OnUnsolicitedMessageReceived(const PayloadHeader & payloadHeader, Messaging::ExchangeDelegate *& delegate) override; + void OnExchangeCreationFailed(Messaging::ExchangeDelegate * delegate) override; + +private: + // The maximum number of transfers to support at once. This number was chosen because it should be sufficient for + // current tests that use BDX. + static constexpr size_t kTransferPoolSize = 2; + + ObjectPool mTransferPool; + System::Layer * mSystemLayer = nullptr; + Messaging::ExchangeManager * mExchangeManager = nullptr; + BdxTransfer::Delegate * mBdxTransferDelegate = nullptr; + size_t mExpectedTransfers = 0; +}; + +} // namespace bdx +} // namespace chip diff --git a/src/python_testing/TestBdxTransfer.py b/src/python_testing/TestBdxTransfer.py new file mode 100644 index 00000000000000..77efd010912352 --- /dev/null +++ b/src/python_testing/TestBdxTransfer.py @@ -0,0 +1,112 @@ +# +# 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. +# + +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: +# run1: +# app: ${ALL_CLUSTERS_APP} +# app-args: > +# --discriminator 1234 +# --KVS kvs1 +# --trace-to json:${TRACE_APP}.json +# --end_user_support_log ${END_USER_SUPPORT_LOG} +# script-args: > +# --storage-path admin_storage.json +# --commissioning-method on-network +# --discriminator 1234 +# --passcode 20202021 +# --trace-to json:${TRACE_TEST_JSON}.json +# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# --string-arg "end_user_support_log:${END_USER_SUPPORT_LOG}" +# factory-reset: true +# quiet: false +# === END CI TEST ARGUMENTS === + +import asyncio + +import chip.clusters as Clusters +from chip.bdx import BdxProtocol, BdxTransfer +from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts + + +class TestBdxTransfer(MatterBaseTest): + def desc_bdx_transfer(self) -> str: + return "Test a BDX transfer with the diagnostic logs cluster" + + def steps_bdx_transfer(self) -> list[TestStep]: + steps = [ + TestStep(1, "Set up the system to receive a BDX transfer."), + TestStep(2, "Send the command to request logs."), + TestStep(3, "Wait for the command's response or a BDX transfer."), + TestStep(4, "Verify the init message's parameters."), + TestStep(5, "Accept the transfer and obtain the data."), + TestStep(6, "Verify the obtained data."), + TestStep(7, "Check the command's response."), + ] + return steps + + @async_test_body + async def test_bdx_transfer(self): + self.step(1) + bdx_future: asyncio.futures.Future = self.default_controller.TestOnlyPrepareToReceiveBdxData() + + self.step(2) + file_designator = "filename" + command: Clusters.DiagnosticLogs.Commands.RetrieveLogsRequest = Clusters.DiagnosticLogs.Commands.RetrieveLogsRequest( + intent=Clusters.DiagnosticLogs.Enums.IntentEnum.kEndUserSupport, + requestedProtocol=Clusters.DiagnosticLogs.Enums.TransferProtocolEnum.kBdx, + transferFileDesignator=file_designator + ) + command_send_future = asyncio.create_task(self.default_controller.SendCommand( + self.dut_node_id, + 0, + command, + responseType=Clusters.DiagnosticLogs.Commands.RetrieveLogsResponse) + ) + + self.step(3) + done, pending = await asyncio.wait([command_send_future, bdx_future], return_when=asyncio.FIRST_COMPLETED) + + self.step(4) + asserts.assert_true(bdx_future in done, "BDX transfer didn't start") + bdx_transfer: BdxTransfer.BdxTransfer = bdx_future.result() + asserts.assert_equal(bdx_transfer.init_message.TransferControlFlags, BdxProtocol.SENDER_DRIVE, "Invalid transfer control flags") + asserts.assert_equal(bdx_transfer.init_message.MaxBlockSize, 1024, "Invalid max block size") + asserts.assert_equal(bdx_transfer.init_message.StartOffset, 0, "Invalid start offset") + asserts.assert_equal(bdx_transfer.init_message.FileDesignator, + bytes(file_designator, encoding='utf8'), + "Invalid file designator") + + self.step(5) + data = await bdx_transfer.accept_and_receive_data() + + self.step(6) + data_file = open(self.user_params["end_user_support_log"], "rb") + asserts.assert_equal(data, data_file.read(), "Transferred data doesn't match") + + self.step(7) + command_response: Clusters.DiagnosticLogs.Commands.RetrieveLogsResponse + if command_send_future in done: + command_response = command_send_future.result() + else: + command_response = await command_send_future + asserts.assert_equal(command_response.status, Clusters.DiagnosticLogs.Enums.StatusEnum.kSuccess, "Invalid command response") + + +if __name__ == "__main__": + default_matter_test_main()