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()