Skip to content

Commit

Permalink
LE read remote features
Browse files Browse the repository at this point in the history
  • Loading branch information
zxzxwu committed Jan 2, 2024
1 parent 09e5ea5 commit 113b072
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 18 deletions.
4 changes: 2 additions & 2 deletions apps/controller_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
from bumble.core import name_or_number
from bumble.hci import (
map_null_terminated_utf8_string,
LeFeatureMask,
HCI_SUCCESS,
HCI_LE_SUPPORTED_FEATURES_NAMES,
HCI_VERSION_NAMES,
LMP_VERSION_NAMES,
HCI_Command,
Expand Down Expand Up @@ -137,7 +137,7 @@ async def get_le_info(host: Host) -> None:

print(color('LE Features:', 'yellow'))
for feature in host.supported_le_features:
print(' ', name_or_number(HCI_LE_SUPPORTED_FEATURES_NAMES, feature))
print(LeFeatureMask(feature).name)


# -----------------------------------------------------------------------------
Expand Down
14 changes: 13 additions & 1 deletion bumble/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,18 @@ def on_hci_le_read_remote_features_command(self, command):
See Bluetooth spec Vol 4, Part E - 7.8.21 LE Read Remote Features Command
'''

handle = command.connection_handle

if not self.find_connection_by_handle(handle):
self.send_hci_packet(
HCI_Command_Status_Event(
status=HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
return

# First, say that the command is pending
self.send_hci_packet(
HCI_Command_Status_Event(
Expand All @@ -1117,7 +1129,7 @@ def on_hci_le_read_remote_features_command(self, command):
self.send_hci_packet(
HCI_LE_Read_Remote_Features_Complete_Event(
status=HCI_SUCCESS,
connection_handle=0,
connection_handle=handle,
le_features=bytes.fromhex('dd40000000000000'),
)
)
Expand Down
46 changes: 34 additions & 12 deletions bumble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
HCI_CENTRAL_ROLE,
HCI_COMMAND_STATUS_PENDING,
HCI_CONNECTED_ISOCHRONOUS_STREAM_LE_SUPPORTED_FEATURE,
HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR,
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
HCI_DISPLAY_ONLY_IO_CAPABILITY,
Expand All @@ -60,12 +59,9 @@
HCI_LE_1M_PHY,
HCI_LE_1M_PHY_BIT,
HCI_LE_2M_PHY,
HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE,
HCI_LE_CLEAR_RESOLVING_LIST_COMMAND,
HCI_LE_CODED_PHY,
HCI_LE_CODED_PHY_BIT,
HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE,
HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE,
HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND,
HCI_LE_RAND_COMMAND,
HCI_LE_READ_PHY_COMMAND,
Expand Down Expand Up @@ -107,6 +103,7 @@
HCI_LE_Extended_Create_Connection_Command,
HCI_LE_Rand_Command,
HCI_LE_Read_PHY_Command,
HCI_LE_Read_Remote_Features_Command,
HCI_LE_Reject_CIS_Request_Command,
HCI_LE_Remove_Advertising_Set_Command,
HCI_LE_Set_Address_Resolution_Enable_Command,
Expand Down Expand Up @@ -152,6 +149,7 @@
HCI_Write_Secure_Connections_Host_Support_Command,
HCI_Write_Simple_Pairing_Mode_Command,
OwnAddressType,
LeFeatureMask,
phy_list_to_bits,
)
from .host import Host
Expand Down Expand Up @@ -682,6 +680,7 @@ class Connection(CompositeEventEmitter):
self_address: Address
peer_address: Address
peer_resolvable_address: Optional[Address]
peer_le_features: Optional[LeFeatureMask]
role: int
encryption: int
authenticated: bool
Expand Down Expand Up @@ -758,6 +757,7 @@ def __init__(
) # By default, use the device's shared server
self.pairing_peer_io_capability = None
self.pairing_peer_authentication_requirements = None
self.peer_le_features = None

# [Classic only]
@classmethod
Expand Down Expand Up @@ -906,6 +906,30 @@ async def get_phy(self):
async def request_remote_name(self):
return await self.device.request_remote_name(self)

async def read_remote_feature(self) -> None:
"""Reads remote supported features.
The size of remote features is very different between BR/EDR and LE, so it only performs the exchange here. Results are stored in the instance.
"""
if self.transport == BT_BR_EDR_TRANSPORT:
raise NotImplementedError("TODO")
else:
with closing(EventWatcher()) as watcher:
read_feature_future = asyncio.get_running_loop().create_future()

def on_le_remote_features(handle: int, features: int):
if handle == self.handle:
self.peer_le_features = LeFeatureMask(features)
read_feature_future.set_result(None)

watcher.on(
self.device.host, 'le_remote_features', on_le_remote_features
)
await self.device.send_command(
HCI_LE_Read_Remote_Features_Command(connection_handle=self.handle),
)
await read_feature_future

async def __aenter__(self):
return self

Expand Down Expand Up @@ -1538,9 +1562,7 @@ async def power_on(self) -> None:
if self.cis_enabled:
await self.send_command(
HCI_LE_Set_Host_Feature_Command(
bit_number=(
HCI_CONNECTED_ISOCHRONOUS_STREAM_LE_SUPPORTED_FEATURE
),
bit_number=LeFeatureMask.CONNECTED_ISOCHRONOUS_STREAM,
bit_value=1,
)
)
Expand Down Expand Up @@ -1604,8 +1626,8 @@ def supports_le_phy(self, phy):
return True

feature_map = {
HCI_LE_2M_PHY: HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE,
HCI_LE_CODED_PHY: HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE,
HCI_LE_2M_PHY: LeFeatureMask.LE_2M_PHY,
HCI_LE_CODED_PHY: LeFeatureMask.LE_CODED_PHY,
}
if phy not in feature_map:
raise ValueError('invalid PHY')
Expand Down Expand Up @@ -1921,7 +1943,7 @@ async def start_scanning(

# Enable scanning
if not legacy and self.supports_le_feature(
HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE
LeFeatureMask.LE_EXTENDED_ADVERTISING
):
# Set the scanning parameters
scan_type = (
Expand All @@ -1939,7 +1961,7 @@ async def start_scanning(
scanning_phys_bits |= 1 << HCI_LE_1M_PHY_BIT
scanning_phy_count += 1
if HCI_LE_CODED_PHY in scanning_phys:
if self.supports_le_feature(HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE):
if self.supports_le_feature(LeFeatureMask.LE_CODED_PHY):
scanning_phys_bits |= 1 << HCI_LE_CODED_PHY_BIT
scanning_phy_count += 1

Expand Down Expand Up @@ -2000,7 +2022,7 @@ async def start_scanning(

async def stop_scanning(self) -> None:
# Disable scanning
if self.supports_le_feature(HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE):
if self.supports_le_feature(LeFeatureMask.LE_EXTENDED_ADVERTISING):
await self.send_command(
HCI_LE_Set_Extended_Scan_Enable_Command(
enable=0, filter_duplicates=0, duration=0, period=0
Expand Down
47 changes: 47 additions & 0 deletions bumble/hci.py
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,53 @@ class PhyBit(enum.IntFlag):

# LE Supported Features
# See Bluetooth spec @ Vol 6, Part B, 4.6 FEATURE SUPPORT
class LeFeatureMask(enum.IntFlag):
LE_ENCRYPTION = 1 << 0
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1 << 1
EXTENDED_REJECT_INDICATION = 1 << 2
PERIPHERAL_INITIATED_FEATURE_EXCHANGE = 1 << 3
LE_PING = 1 << 4
LE_DATA_PACKET_LENGTH_EXTENSION = 1 << 5
LL_PRIVACY = 1 << 6
EXTENDED_SCANNER_FILTER_POLICIES = 1 << 7
LE_2M_PHY = 1 << 8
STABLE_MODULATION_INDEX_TRANSMITTER = 1 << 9
STABLE_MODULATION_INDEX_RECEIVER = 1 << 10
LE_CODED_PHY = 1 << 11
LE_EXTENDED_ADVERTISING = 1 << 12
LE_PERIODIC_ADVERTISING = 1 << 13
CHANNEL_SELECTION_ALGORITHM_2 = 1 << 14
LE_POWER_CLASS_1 = 1 << 15
MINIMUM_NUMBER_OF_USED_CHANNELS_PROCEDURE = 1 << 16
CONNECTION_CTE_REQUEST = 1 << 17
CONNECTION_CTE_RESPONSE = 1 << 18
CONNECTIONLESS_CTE_TRANSMITTER = 1 << 19
CONNECTIONLESS_CTR_RECEIVER = 1 << 20
ANTENNA_SWITCHING_DURING_CTE_TRANSMISSION = 1 << 21
ANTENNA_SWITCHING_DURING_CTE_RECEPTION = 1 << 22
RECEIVING_CONSTANT_TONE_EXTENSIONS = 1 << 23
PERIODIC_ADVERTISING_SYNC_TRANSFER_SENDER = 1 << 24
PERIODIC_ADVERTISING_SYNC_TRANSFER_RECIPIENT = 1 << 25
SLEEP_CLOCK_ACCURACY_UPDATES = 1 << 26
REMOTE_PUBLIC_KEY_VALIDATION = 1 << 27
CONNECTED_ISOCHRONOUS_STREAM_CENTRAL = 1 << 28
CONNECTED_ISOCHRONOUS_STREAM_PERIPHERAL = 1 << 29
ISOCHRONOUS_BROADCASTER = 1 << 30
SYNCHRONIZED_RECEIVER = 1 << 31
CONNECTED_ISOCHRONOUS_STREAM = 1 << 32
LE_POWER_CONTROL_REQUEST = 1 << 33
LE_POWER_CONTROL_REQUEST_DUP = 1 << 34
LE_PATH_LOSS_MONITORING = 1 << 35
PERIODIC_ADVERTISING_ADI_SUPPORT = 1 << 36
CONNECTION_SUBRATING = 1 << 37
CONNECTION_SUBRATING_HOST_SUPPORT = 1 << 38
CHANNEL_CLASSIFICATION = 1 << 39
ADVERTISING_CODING_SELECTION = 1 << 40
ADVERTISING_CODING_SELECTION_HOST_SUPPORT = 1 << 41
PERIODIC_ADVERTISING_WITH_RESPONSES_ADVERTISER = 1 << 43
PERIODIC_ADVERTISING_WITH_RESPONSES_SCANNER = 1 << 44

# Legacy types
HCI_LE_ENCRYPTION_LE_SUPPORTED_FEATURE = 0
HCI_CONNECTION_PARAMETERS_REQUEST_PROCEDURE_LE_SUPPORTED_FEATURE = 1
HCI_EXTENDED_REJECT_INDICATION_LE_SUPPORTED_FEATURE = 2
Expand Down
32 changes: 29 additions & 3 deletions bumble/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,17 @@
import logging
import struct

from typing import Any, Awaitable, Callable, Deque, Dict, Optional, cast, TYPE_CHECKING
from typing import (
Any,
Awaitable,
Callable,
Deque,
Dict,
Optional,
Union,
cast,
TYPE_CHECKING,
)

from bumble.colors import color
from bumble.l2cap import L2CAP_PDU
Expand Down Expand Up @@ -70,6 +80,7 @@
HCI_Reset_Command,
HCI_Set_Event_Mask_Command,
HCI_SynchronousDataPacket,
LeFeatureMask,
)
from .core import (
BT_BR_EDR_TRANSPORT,
Expand Down Expand Up @@ -487,8 +498,11 @@ def supported_commands(self):

return commands

def supports_le_feature(self, feature):
return (self.local_le_features & (1 << feature)) != 0
def supports_le_feature(self, feature: Union[LeFeatureMask, int]) -> bool:
if isinstance(feature, LeFeatureMask):
return (self.local_le_features & feature) != 0
else:
return (self.local_le_features & (1 << feature)) != 0

@property
def supported_le_features(self):
Expand Down Expand Up @@ -1033,3 +1047,15 @@ def on_hci_remote_host_supported_features_notification_event(self, event):
event.bd_addr,
event.host_supported_features,
)

def on_hci_le_read_remote_features_complete_event(self, event):
if event.status != HCI_SUCCESS:
self.emit(
'le_remote_features_failure', event.connection_handle, event.status
)
else:
self.emit(
'le_remote_features',
event.connection_handle,
int.from_bytes(event.le_features, 'little'),
)
13 changes: 13 additions & 0 deletions tests/device_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
GATT_APPEARANCE_CHARACTERISTIC,
)

from .test_utils import TwoDevices

# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -412,6 +414,17 @@ async def test_extended_advertising_disconnection(auto_restart):
device.start_extended_advertising.assert_not_called()


# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_read_remote_supported_feature():
devices = TwoDevices()
await devices.setup_connection()

await devices.connections[0].read_remote_feature()

assert devices.connections[0].peer_le_features is not None


# -----------------------------------------------------------------------------
def test_gatt_services_with_gas():
device = Device(host=Host(None, None))
Expand Down

0 comments on commit 113b072

Please sign in to comment.