BDX transfer support for Python tests (#34821)
* Add the python-C++ translation.
* Add a BDX transfer server to handle unsolicited BDX init messages.
* Add the manager to implement the transfer pool.
* Add the initial implementation of a BDX transfer.
* Use BdxTransfer in the other classes.
* Update constructors to set the delegates etc. correctly.
* Implement the C++ side of the barrier. Move the data callback into the transfer delegate.
* Add a way to map the transfer to the python contexts.
* Fix some of the minor TODOs.
* Add init/shutdown to the transfer server.
* Start on the implementation of the Python side.
Also add the transfer obtained context to the C++ methods relating to expecting transfers.
* Listen for all BDX protocol messages rather than just the init messages.
* Fix minor issues in the transfer server.
* Implement a good chunk of the python side.
* Fix compile errors.
* Fix a number of issues preventing the BDX python code from running at all.
* Return the results of the python-C methods.
* Fix the async-ness of the methods that prepare the system to receive a BDX transfer.
Also run the python BDX initialisation.
* Initialise the BDX transfer server.
Also ignore the BDX transfer server implementation that only handles diagnostic logs.
* Fixes necessary to await on the future from PrepareToReceive/SendBdxData.
* Call Responder::PrepareForTransfer from BdxTransfer.
* Correctly schedule satisfying the future on the event loop.
* Use the real property to determine if a PyChipError was a success.
* Fix sending the accept message.
* Acknowledge received blocks so the BDX transfer continues.
Also don't ignore all messages after the init.
* Fix the parameters of the python callback methods.
* Add another async transaction class to handle the transfer completed callback.
* Add comments to the C++ code.
* Add a test for the BDX transfer that uses the diagnostic logs cluster.
* Move the calls to release a transfer out of the manager so it works the way one would expect.
* Delay releasing the C++ BDX transfer object until after it's no longer in use.
* Verify the diagnostic logs response is a success.
* Restyled by whitespace
* Restyled by clang-format
* Restyled by gn
* Restyled by autopep8
* Restyled by isort
* Improve BdxTransferManager's comments.
* Use a vector for the data to send over a BDX transfer rather than a raw pointer.
* Minor renames.
* Improve the error message when the BDX transfer pool is exhausted.
* Minor fixes.
* remove a check that was inadvertently kept.
* print a log message when something that shouldn't happen inevitably does.
* use user_params to get the end user support log test parameter.
* Pass the status report's status code up the stack.
* Merge the BDX transfer server into the manager.
* Rename BdxTransferManager to TestBdxTransferServer.
* Minor cleanup.
* Rename TransferData to TransferInfo.
* Change `!=` to `is not` in python.
* Add missing type annotation.
* Improve the documentation of the ownership in the C++ side.
* Restyled by clang-format
* Restyled by autopep8
* Update the new test to work with the new formatting.
Also remove an unnecessary conversion to bytearray.
* Lint fixes.
* Fix clang-tidy errors.
* Several fixes suggested by Andrei.
* Fix a name in a comment.
* Fix issues preventing test from working.
Also:
* Split the accept function into one for sending data and one for receiving data.
* Return bytes instead of a bytearray when receiving data.
* Add typing to the data callback.
* Rename the methods that accept transfers so it's clear which way the data is flowing.
* Add doc comments to the Python classes and methods.
* Fix issues found by mypy.
* Restyled by clang-format
* Restyled by autopep8
* Fix python lint error.
* Explicitly truncate the status code when generating the error.
* Generate the diagnostic log to transfer in the test.
---------
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn
index 58d9cb1..a90f59f 100644
--- a/src/controller/python/BUILD.gn
+++ b/src/controller/python/BUILD.gn
@@ -63,6 +63,11 @@
"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/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 @@
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 8c751f7..1e8f74f 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 @@
# 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 e029b77..94ad734 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 @@
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 0000000..7641644
--- /dev/null
+++ b/src/controller/python/chip/bdx/Bdx.py
@@ -0,0 +1,230 @@
+#
+# 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 0000000..6d53fda
--- /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 0000000..7e5ef63
--- /dev/null
+++ b/src/controller/python/chip/bdx/BdxTransfer.py
@@ -0,0 +1,81 @@
+#
+# 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 0000000..8c3c1fb
--- /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__ = ["Bdx", "BdxProtocol", "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 0000000..f84fde9
--- /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 <controller/python/chip/bdx/bdx-transfer.h>
+
+#include <protocols/bdx/BdxTransferSession.h>
+
+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, static_cast<uint8_t>(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<size_t>(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<int>(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<size_t>(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 0000000..3ddf849
--- /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 <vector>
+
+#include <lib/support/Span.h>
+#include <messaging/ExchangeContext.h>
+
+#include <protocols/bdx/BdxTransferSession.h>
+#include <protocols/bdx/TransferFacilitator.h>
+
+#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<uint8_t> 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 0000000..1d466df
--- /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 <vector>
+
+#include <protocols/bdx/BdxMessages.h>
+
+#include <controller/CHIPDeviceControllerFactory.h>
+#include <controller/CHIPDeviceControllerSystemState.h>
+#include <controller/python/chip/bdx/bdx-transfer.h>
+#include <controller/python/chip/bdx/test-bdx-transfer-server.h>
+#include <controller/python/chip/native/PyChipError.h>
+
+// 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<TransferInfo>::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<TransferInfo>::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<TransferInfo>::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<TransferInfo>::iterator result = std::find(mTransfers.begin(), mTransfers.end(), *transferInfo);
+ VerifyOrReturn(result != mTransfers.end());
+ mTransfers.erase(result);
+ }
+
+private:
+ std::vector<TransferInfo> 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<bdx::BdxTransfer *>(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 0000000..86a0c9b
--- /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 <controller/python/chip/bdx/test-bdx-transfer-server.h>
+
+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<int>(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<BdxTransfer *>(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 0000000..7caa298
--- /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 <lib/support/Pool.h>
+#include <messaging/ExchangeDelegate.h>
+#include <messaging/ExchangeMgr.h>
+#include <protocols/bdx/BdxTransferSession.h>
+#include <transport/raw/MessageHeader.h>
+
+#include <controller/python/chip/bdx/bdx-transfer.h>
+
+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<BdxTransfer, kTransferPoolSize> 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 0000000..bd17310
--- /dev/null
+++ b/src/python_testing/TestBdxTransfer.py
@@ -0,0 +1,119 @@
+#
+# 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 /tmp/eusl.txt
+# 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
+# factory-reset: true
+# quiet: false
+# === END CI TEST ARGUMENTS ===
+
+import asyncio
+import random
+
+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, "Generate the diagnostic log file."),
+ TestStep(2, "Set up the system to receive a BDX transfer."),
+ TestStep(3, "Send the command to request logs."),
+ TestStep(4, "Wait for the command's response or a BDX transfer."),
+ TestStep(5, "Verify the init message's parameters."),
+ TestStep(6, "Accept the transfer and obtain the data."),
+ TestStep(7, "Verify the obtained data."),
+ TestStep(8, "Check the command's response."),
+ ]
+ return steps
+
+ @async_test_body
+ async def test_bdx_transfer(self):
+ self.step(1)
+ expected_data = random.randbytes(9240)
+ diagnostic_file = open("/tmp/eusl.txt", "wb")
+ diagnostic_file.write(expected_data)
+ diagnostic_file.close()
+
+ self.step(2)
+ bdx_future: asyncio.futures.Future = self.default_controller.TestOnlyPrepareToReceiveBdxData()
+
+ self.step(3)
+ 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(4)
+ done, pending = await asyncio.wait([command_send_future, bdx_future], return_when=asyncio.FIRST_COMPLETED)
+
+ self.step(5)
+ 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(6)
+ data = await bdx_transfer.accept_and_receive_data()
+
+ self.step(7)
+ asserts.assert_equal(data, expected_data, "Transferred data doesn't match")
+
+ self.step(8)
+ 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()