Merge remote-tracking branch 'GITHUB/main' into HEAD am: f19b70c0cc

Original change: https://android-review.googlesource.com/c/platform/external/python/bumble/+/2748101

Change-Id: If1b69fe068af762b4fc74da44f0efce82eb2b4f3
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/apps/show.py b/apps/show.py
index 5cd2309..f849e3a 100644
--- a/apps/show.py
+++ b/apps/show.py
@@ -104,7 +104,7 @@
 )
 @click.option(
     '--vendors',
-    type=click.Choice(['android']),
+    type=click.Choice(['android', 'zephyr']),
     multiple=True,
     help='Support vendor-specific commands (list one or more)',
 )
@@ -114,6 +114,8 @@
     for vendor in vendors:
         if vendor == 'android':
             import bumble.vendor.android.hci
+        elif vendor == 'zephyr':
+            import bumble.vendor.zephyr.hci
 
     input = open(filename, 'rb')
     if format == 'h4':
diff --git a/bumble/core.py b/bumble/core.py
index b00c40e..4dff432 100644
--- a/bumble/core.py
+++ b/bumble/core.py
@@ -142,6 +142,10 @@
         self.peer_address = peer_address
 
 
+class ConnectionParameterUpdateError(BaseError):
+    """Connection Parameter Update Error"""
+
+
 # -----------------------------------------------------------------------------
 # UUID
 #
diff --git a/bumble/device.py b/bumble/device.py
index fca2e7a..9a784e7 100644
--- a/bumble/device.py
+++ b/bumble/device.py
@@ -141,6 +141,7 @@
     BT_LE_TRANSPORT,
     BT_PERIPHERAL_ROLE,
     AdvertisingData,
+    ConnectionParameterUpdateError,
     CommandTimeoutError,
     ConnectionPHY,
     InvalidStateError,
@@ -723,6 +724,7 @@
         connection_interval_max,
         max_latency,
         supervision_timeout,
+        use_l2cap=False,
     ):
         return await self.device.update_connection_parameters(
             self,
@@ -730,6 +732,7 @@
             connection_interval_max,
             max_latency,
             supervision_timeout,
+            use_l2cap=use_l2cap,
         )
 
     async def set_phy(self, tx_phys=None, rx_phys=None, phy_options=None):
@@ -2110,11 +2113,30 @@
         supervision_timeout,
         min_ce_length=0,
         max_ce_length=0,
-    ):
+        use_l2cap=False,
+    ) -> None:
         '''
         NOTE: the name of the parameters may look odd, but it just follows the names
         used in the Bluetooth spec.
         '''
+
+        if use_l2cap:
+            if connection.role != BT_PERIPHERAL_ROLE:
+                raise InvalidStateError(
+                    'only peripheral can update connection parameters with l2cap'
+                )
+            l2cap_result = (
+                await self.l2cap_channel_manager.update_connection_parameters(
+                    connection,
+                    connection_interval_min,
+                    connection_interval_max,
+                    max_latency,
+                    supervision_timeout,
+                )
+            )
+            if l2cap_result != l2cap.L2CAP_CONNECTION_PARAMETERS_ACCEPTED_RESULT:
+                raise ConnectionParameterUpdateError(l2cap_result)
+
         result = await self.send_command(
             HCI_LE_Connection_Update_Command(
                 connection_handle=connection.handle,
@@ -2124,7 +2146,7 @@
                 supervision_timeout=supervision_timeout,
                 min_ce_length=min_ce_length,
                 max_ce_length=max_ce_length,
-            )
+            )  # type: ignore[call-arg]
         )
         if result.status != HCI_Command_Status_Event.PENDING:
             raise HCI_StatusError(result)
diff --git a/bumble/l2cap.py b/bumble/l2cap.py
index 270d909..fea8a1d 100644
--- a/bumble/l2cap.py
+++ b/bumble/l2cap.py
@@ -1387,6 +1387,7 @@
     le_coc_requests: Dict[int, L2CAP_LE_Credit_Based_Connection_Request]
     fixed_channels: Dict[int, Optional[Callable[[int, bytes], Any]]]
     _host: Optional[Host]
+    connection_parameters_update_response: Optional[asyncio.Future[int]]
 
     def __init__(
         self,
@@ -1408,6 +1409,7 @@
         self.le_coc_requests = {}  # LE CoC connection requests, by identifier
         self.extended_features = extended_features
         self.connectionless_mtu = connectionless_mtu
+        self.connection_parameters_update_response = None
 
     @property
     def host(self) -> Host:
@@ -1865,11 +1867,45 @@
                 ),
             )
 
+    async def update_connection_parameters(
+        self,
+        connection: Connection,
+        interval_min: int,
+        interval_max: int,
+        latency: int,
+        timeout: int,
+    ) -> int:
+        # Check that there isn't already a request pending
+        if self.connection_parameters_update_response:
+            raise InvalidStateError('request already pending')
+        self.connection_parameters_update_response = (
+            asyncio.get_running_loop().create_future()
+        )
+        self.send_control_frame(
+            connection,
+            L2CAP_LE_SIGNALING_CID,
+            L2CAP_Connection_Parameter_Update_Request(
+                interval_min=interval_min,
+                interval_max=interval_max,
+                latency=latency,
+                timeout=timeout,
+            ),
+        )
+        return await self.connection_parameters_update_response
+
     def on_l2cap_connection_parameter_update_response(
         self, connection: Connection, cid: int, response
     ) -> None:
-        # TODO: check response
-        pass
+        if self.connection_parameters_update_response:
+            self.connection_parameters_update_response.set_result(response.result)
+            self.connection_parameters_update_response = None
+        else:
+            logger.warning(
+                color(
+                    'received l2cap_connection_parameter_update_response without a pending request',
+                    'red',
+                )
+            )
 
     def on_l2cap_le_credit_based_connection_request(
         self, connection: Connection, cid: int, request
diff --git a/bumble/transport/common.py b/bumble/transport/common.py
index 5d5bdf1..c030308 100644
--- a/bumble/transport/common.py
+++ b/bumble/transport/common.py
@@ -63,6 +63,8 @@
 
 
 class TransportSource(Protocol):
+    terminated: asyncio.Future[None]
+
     def set_packet_sink(self, sink: TransportSink) -> None:
         ...
 
@@ -430,6 +432,7 @@
         def __init__(self, source: TransportSource, snooper: Snooper):
             self.source = source
             self.snooper = snooper
+            self.terminated = source.terminated
 
         def set_packet_sink(self, sink: TransportSink) -> None:
             self.sink = sink
diff --git a/bumble/vendor/zephyr/__init__.py b/bumble/vendor/zephyr/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/bumble/vendor/zephyr/__init__.py
diff --git a/bumble/vendor/zephyr/hci.py b/bumble/vendor/zephyr/hci.py
new file mode 100644
index 0000000..9ffb3c3
--- /dev/null
+++ b/bumble/vendor/zephyr/hci.py
@@ -0,0 +1,88 @@
+# Copyright 2021-2023 Google LLC
+#
+# 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
+#
+#      https://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.
+
+# -----------------------------------------------------------------------------
+# Imports
+# -----------------------------------------------------------------------------
+from bumble.hci import (
+    hci_vendor_command_op_code,
+    HCI_Command,
+    STATUS_SPEC,
+)
+
+
+# -----------------------------------------------------------------------------
+# Constants
+# -----------------------------------------------------------------------------
+
+# Zephyr RTOS Vendor Specific Commands and Events.
+# Only a subset of the commands are implemented here currently.
+#
+# pylint: disable-next=line-too-long
+# See https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
+HCI_WRITE_TX_POWER_LEVEL_COMMAND = hci_vendor_command_op_code(0x000E)
+HCI_READ_TX_POWER_LEVEL_COMMAND = hci_vendor_command_op_code(0x000F)
+
+HCI_Command.register_commands(globals())
+
+
+# -----------------------------------------------------------------------------
+class TX_Power_Level_Command:
+    '''
+    Base class for read and write TX power level HCI commands
+    '''
+
+    TX_POWER_HANDLE_TYPE_ADV = 0x00
+    TX_POWER_HANDLE_TYPE_SCAN = 0x01
+    TX_POWER_HANDLE_TYPE_CONN = 0x02
+
+
+# -----------------------------------------------------------------------------
+@HCI_Command.command(
+    fields=[('handle_type', 1), ('connection_handle', 2), ('tx_power_level', -1)],
+    return_parameters_fields=[
+        ('status', STATUS_SPEC),
+        ('handle_type', 1),
+        ('connection_handle', 2),
+        ('selected_tx_power_level', -1),
+    ],
+)
+class HCI_Write_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
+    '''
+    Write TX power level. See BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL in
+    https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
+
+    Power level is in dB. Connection handle for TX_POWER_HANDLE_TYPE_ADV and
+    TX_POWER_HANDLE_TYPE_SCAN should be zero.
+    '''
+
+
+# -----------------------------------------------------------------------------
+@HCI_Command.command(
+    fields=[('handle_type', 1), ('connection_handle', 2)],
+    return_parameters_fields=[
+        ('status', STATUS_SPEC),
+        ('handle_type', 1),
+        ('connection_handle', 2),
+        ('tx_power_level', -1),
+    ],
+)
+class HCI_Read_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
+    '''
+    Read TX power level. See BT_HCI_OP_VS_READ_TX_POWER_LEVEL in
+    https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
+
+    Power level is in dB. Connection handle for TX_POWER_HANDLE_TYPE_ADV and
+    TX_POWER_HANDLE_TYPE_SCAN should be zero.
+    '''
diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml
index 0cf65f1..82a6f41 100644
--- a/docs/mkdocs/mkdocs.yml
+++ b/docs/mkdocs/mkdocs.yml
@@ -64,6 +64,7 @@
     - Linux: platforms/linux.md
     - Windows: platforms/windows.md
     - Android: platforms/android.md
+    - Zephyr: platforms/zephyr.md
   - Examples:
     - Overview: examples/index.md
 
diff --git a/docs/mkdocs/src/platforms/index.md b/docs/mkdocs/src/platforms/index.md
index a93e947..858785f 100644
--- a/docs/mkdocs/src/platforms/index.md
+++ b/docs/mkdocs/src/platforms/index.md
@@ -9,3 +9,4 @@
   * :material-linux: Linux - see the [Linux platform page](linux.md)
   * :material-microsoft-windows: Windows - see the [Windows platform page](windows.md)
   * :material-android: Android - see the [Android platform page](android.md)
+  * :material-memory: Zephyr - see the [Zephyr platform page](zephyr.md)
diff --git a/docs/mkdocs/src/platforms/zephyr.md b/docs/mkdocs/src/platforms/zephyr.md
new file mode 100644
index 0000000..0e68247
--- /dev/null
+++ b/docs/mkdocs/src/platforms/zephyr.md
@@ -0,0 +1,51 @@
+:material-memory: ZEPHYR PLATFORM
+=================================
+
+Set TX Power on nRF52840
+------------------------
+
+The Nordic nRF52840 supports Zephyr's vendor specific HCI command for setting TX
+power during advertising, connection, or scanning. With the example [HCI
+USB](https://docs.zephyrproject.org/latest/samples/bluetooth/hci_usb/README.html)
+application, an [nRF52840
+dongle](https://www.nordicsemi.com/Products/Development-
+hardware/nRF52840-Dongle) can be used as a Bumble controller.
+
+To add dynamic TX power support to the HCI USB application, add the following to
+`zephyr/samples/bluetooth/hci_usb/prj.conf` and build.
+
+```
+CONFIG_BT_CTLR_ADVANCED_FEATURES=y
+CONFIG_BT_CTLR_CONN_RSSI=y
+CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y
+```
+
+Alternatively, a prebuilt firmware application can be downloaded here:
+[hci_usb.zip](../downloads/zephyr/hci_usb.zip).
+
+Put the nRF52840 dongle into bootloader mode by pressing the RESET button. The
+LED should pulse red. Load the firmware application with the `nrfutil` tool:
+
+```
+nrfutil dfu usb-serial -pkg hci_usb.zip -p /dev/ttyACM0
+```
+
+The vendor specific HCI commands to read and write TX power are defined in
+`bumble/vendor/zephyr/hci.py` and may be used as such:
+
+```python
+from bumble.vendor.zephyr.hci import HCI_Write_Tx_Power_Level_Command
+
+# set advertising power to -4 dB
+response = await host.send_command(
+    HCI_Write_Tx_Power_Level_Command(
+        handle_type=HCI_Write_Tx_Power_Level_Command.TX_POWER_HANDLE_TYPE_ADV,
+        connection_handle=0,
+        tx_power_level=-4,
+    )
+)
+
+if response.return_parameters.status == HCI_SUCCESS:
+    print(f"TX power set to {response.return_parameters.selected_tx_power_level}")
+
+```