forked from auracaster/bumble_mirror
Compare commits
15 Commits
gbg/androi
...
gbg/androi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d35643524e | ||
|
|
085f163c92 | ||
|
|
88b4cbdf1a | ||
|
|
d6afbc6f4e | ||
|
|
fc90de3e7b | ||
|
|
847c2ef114 | ||
|
|
a0bf0c1f4d | ||
|
|
843466c822 | ||
|
|
3adcc8be09 | ||
|
|
c853d56302 | ||
|
|
dc97be5b35 | ||
|
|
73dbdfff9f | ||
|
|
dff14e1258 | ||
|
|
10a3833893 | ||
|
|
7eb493990f |
112
bumble/device.py
112
bumble/device.py
@@ -1432,7 +1432,7 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.host.reset()
|
await self.host.reset()
|
||||||
|
|
||||||
# Try to get the public address from the controller
|
# Try to get the public address from the controller
|
||||||
response = await self.send_command(HCI_Read_BD_ADDR_Command()) # type: ignore[call-arg]
|
response = await self.send_command(HCI_Read_BD_ADDR_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
color(f'BD_ADDR: {response.return_parameters.bd_addr}', 'yellow')
|
color(f'BD_ADDR: {response.return_parameters.bd_addr}', 'yellow')
|
||||||
@@ -1455,7 +1455,7 @@ class Device(CompositeEventEmitter):
|
|||||||
HCI_Write_LE_Host_Support_Command(
|
HCI_Write_LE_Host_Support_Command(
|
||||||
le_supported_host=int(self.le_enabled),
|
le_supported_host=int(self.le_enabled),
|
||||||
simultaneous_le_host=int(self.le_simultaneous_enabled),
|
simultaneous_le_host=int(self.le_simultaneous_enabled),
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.le_enabled:
|
if self.le_enabled:
|
||||||
@@ -1465,7 +1465,7 @@ class Device(CompositeEventEmitter):
|
|||||||
if self.host.supports_command(HCI_LE_RAND_COMMAND):
|
if self.host.supports_command(HCI_LE_RAND_COMMAND):
|
||||||
# Get 8 random bytes
|
# Get 8 random bytes
|
||||||
response = await self.send_command(
|
response = await self.send_command(
|
||||||
HCI_LE_Rand_Command(), check_result=True # type: ignore[call-arg]
|
HCI_LE_Rand_Command(), check_result=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure the address bytes can be a static random address
|
# Ensure the address bytes can be a static random address
|
||||||
@@ -1486,7 +1486,7 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Random_Address_Command(
|
HCI_LE_Set_Random_Address_Command(
|
||||||
random_address=self.random_address
|
random_address=self.random_address
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1499,12 +1499,12 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Address_Resolution_Enable_Command(
|
HCI_LE_Set_Address_Resolution_Enable_Command(
|
||||||
address_resolution_enable=1
|
address_resolution_enable=1
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.cis_enabled:
|
if self.cis_enabled:
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Host_Feature_Command( # type: ignore[call-arg]
|
HCI_LE_Set_Host_Feature_Command(
|
||||||
bit_number=(
|
bit_number=(
|
||||||
HCI_CONNECTED_ISOCHRONOUS_STREAM_LE_SUPPORTED_FEATURE
|
HCI_CONNECTED_ISOCHRONOUS_STREAM_LE_SUPPORTED_FEATURE
|
||||||
),
|
),
|
||||||
@@ -1514,20 +1514,20 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
if self.classic_enabled:
|
if self.classic_enabled:
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Write_Local_Name_Command(local_name=self.name.encode('utf8')) # type: ignore[call-arg]
|
HCI_Write_Local_Name_Command(local_name=self.name.encode('utf8'))
|
||||||
)
|
)
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Write_Class_Of_Device_Command(class_of_device=self.class_of_device) # type: ignore[call-arg]
|
HCI_Write_Class_Of_Device_Command(class_of_device=self.class_of_device)
|
||||||
)
|
)
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Write_Simple_Pairing_Mode_Command(
|
HCI_Write_Simple_Pairing_Mode_Command(
|
||||||
simple_pairing_mode=int(self.classic_ssp_enabled)
|
simple_pairing_mode=int(self.classic_ssp_enabled)
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Write_Secure_Connections_Host_Support_Command(
|
HCI_Write_Secure_Connections_Host_Support_Command(
|
||||||
secure_connections_host_support=int(self.classic_sc_enabled)
|
secure_connections_host_support=int(self.classic_sc_enabled)
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
await self.set_connectable(self.connectable)
|
await self.set_connectable(self.connectable)
|
||||||
await self.set_discoverable(self.discoverable)
|
await self.set_discoverable(self.discoverable)
|
||||||
@@ -1551,7 +1551,7 @@ class Device(CompositeEventEmitter):
|
|||||||
self.address_resolver = smp.AddressResolver(resolving_keys)
|
self.address_resolver = smp.AddressResolver(resolving_keys)
|
||||||
|
|
||||||
if self.address_resolution_offload:
|
if self.address_resolution_offload:
|
||||||
await self.send_command(HCI_LE_Clear_Resolving_List_Command()) # type: ignore[call-arg]
|
await self.send_command(HCI_LE_Clear_Resolving_List_Command())
|
||||||
|
|
||||||
for irk, address in resolving_keys:
|
for irk, address in resolving_keys:
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
@@ -1560,7 +1560,7 @@ class Device(CompositeEventEmitter):
|
|||||||
peer_identity_address=address,
|
peer_identity_address=address,
|
||||||
peer_irk=irk,
|
peer_irk=irk,
|
||||||
local_irk=self.irk,
|
local_irk=self.irk,
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def supports_le_feature(self, feature):
|
def supports_le_feature(self, feature):
|
||||||
@@ -1595,7 +1595,7 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Advertising_Data_Command(
|
HCI_LE_Set_Advertising_Data_Command(
|
||||||
advertising_data=self.advertising_data
|
advertising_data=self.advertising_data
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1604,7 +1604,7 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Scan_Response_Data_Command(
|
HCI_LE_Set_Scan_Response_Data_Command(
|
||||||
scan_response_data=self.scan_response_data
|
scan_response_data=self.scan_response_data
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1630,13 +1630,13 @@ class Device(CompositeEventEmitter):
|
|||||||
peer_address=peer_address,
|
peer_address=peer_address,
|
||||||
advertising_channel_map=7,
|
advertising_channel_map=7,
|
||||||
advertising_filter_policy=0,
|
advertising_filter_policy=0,
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enable advertising
|
# Enable advertising
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Advertising_Enable_Command(advertising_enable=1), # type: ignore[call-arg]
|
HCI_LE_Set_Advertising_Enable_Command(advertising_enable=1),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1649,7 +1649,7 @@ class Device(CompositeEventEmitter):
|
|||||||
# Disable advertising
|
# Disable advertising
|
||||||
if self.advertising:
|
if self.advertising:
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Advertising_Enable_Command(advertising_enable=0), # type: ignore[call-arg]
|
HCI_LE_Set_Advertising_Enable_Command(advertising_enable=0),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1716,7 +1716,7 @@ class Device(CompositeEventEmitter):
|
|||||||
secondary_advertising_phy=1, # LE 1M
|
secondary_advertising_phy=1, # LE 1M
|
||||||
advertising_sid=0,
|
advertising_sid=0,
|
||||||
scan_request_notification_enable=0,
|
scan_request_notification_enable=0,
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1728,7 +1728,7 @@ class Device(CompositeEventEmitter):
|
|||||||
operation=HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
|
operation=HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
|
||||||
fragment_preference=0x01, # Should not fragment
|
fragment_preference=0x01, # Should not fragment
|
||||||
advertising_data=advertising_data,
|
advertising_data=advertising_data,
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1740,7 +1740,7 @@ class Device(CompositeEventEmitter):
|
|||||||
operation=HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
|
operation=HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
|
||||||
fragment_preference=0x01, # Should not fragment
|
fragment_preference=0x01, # Should not fragment
|
||||||
scan_response_data=scan_response,
|
scan_response_data=scan_response,
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1752,7 +1752,7 @@ class Device(CompositeEventEmitter):
|
|||||||
HCI_LE_Set_Advertising_Set_Random_Address_Command(
|
HCI_LE_Set_Advertising_Set_Random_Address_Command(
|
||||||
advertising_handle=adv_handle,
|
advertising_handle=adv_handle,
|
||||||
random_address=self.random_address,
|
random_address=self.random_address,
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1763,13 +1763,13 @@ class Device(CompositeEventEmitter):
|
|||||||
advertising_handles=[adv_handle],
|
advertising_handles=[adv_handle],
|
||||||
durations=[0], # Forever
|
durations=[0], # Forever
|
||||||
max_extended_advertising_events=[0], # Infinite
|
max_extended_advertising_events=[0], # Infinite
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
except HCI_Error as error:
|
except HCI_Error as error:
|
||||||
# When any step fails, cleanup the advertising handle.
|
# When any step fails, cleanup the advertising handle.
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Remove_Advertising_Set_Command(advertising_handle=adv_handle), # type: ignore[call-arg]
|
HCI_LE_Remove_Advertising_Set_Command(advertising_handle=adv_handle),
|
||||||
check_result=False,
|
check_result=False,
|
||||||
)
|
)
|
||||||
raise error
|
raise error
|
||||||
@@ -1791,12 +1791,12 @@ class Device(CompositeEventEmitter):
|
|||||||
advertising_handles=[adv_handle],
|
advertising_handles=[adv_handle],
|
||||||
durations=[0],
|
durations=[0],
|
||||||
max_extended_advertising_events=[0],
|
max_extended_advertising_events=[0],
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
# Remove advertising set
|
# Remove advertising set
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Remove_Advertising_Set_Command(advertising_handle=adv_handle), # type: ignore[call-arg]
|
HCI_LE_Remove_Advertising_Set_Command(advertising_handle=adv_handle),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
self.extended_advertising_handles.remove(adv_handle)
|
self.extended_advertising_handles.remove(adv_handle)
|
||||||
@@ -1864,7 +1864,7 @@ class Device(CompositeEventEmitter):
|
|||||||
scan_types=[scan_type] * scanning_phy_count,
|
scan_types=[scan_type] * scanning_phy_count,
|
||||||
scan_intervals=[int(scan_window / 0.625)] * scanning_phy_count,
|
scan_intervals=[int(scan_window / 0.625)] * scanning_phy_count,
|
||||||
scan_windows=[int(scan_window / 0.625)] * scanning_phy_count,
|
scan_windows=[int(scan_window / 0.625)] * scanning_phy_count,
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1875,7 +1875,7 @@ class Device(CompositeEventEmitter):
|
|||||||
filter_duplicates=1 if filter_duplicates else 0,
|
filter_duplicates=1 if filter_duplicates else 0,
|
||||||
duration=0, # TODO allow other values
|
duration=0, # TODO allow other values
|
||||||
period=0, # TODO allow other values
|
period=0, # TODO allow other values
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -1893,7 +1893,7 @@ class Device(CompositeEventEmitter):
|
|||||||
le_scan_window=int(scan_window / 0.625),
|
le_scan_window=int(scan_window / 0.625),
|
||||||
own_address_type=own_address_type,
|
own_address_type=own_address_type,
|
||||||
scanning_filter_policy=HCI_LE_Set_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY,
|
scanning_filter_policy=HCI_LE_Set_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY,
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1901,7 +1901,7 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Scan_Enable_Command(
|
HCI_LE_Set_Scan_Enable_Command(
|
||||||
le_scan_enable=1, filter_duplicates=1 if filter_duplicates else 0
|
le_scan_enable=1, filter_duplicates=1 if filter_duplicates else 0
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1914,12 +1914,12 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Extended_Scan_Enable_Command(
|
HCI_LE_Set_Extended_Scan_Enable_Command(
|
||||||
enable=0, filter_duplicates=0, duration=0, period=0
|
enable=0, filter_duplicates=0, duration=0, period=0
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Scan_Enable_Command(le_scan_enable=0, filter_duplicates=0), # type: ignore[call-arg]
|
HCI_LE_Set_Scan_Enable_Command(le_scan_enable=0, filter_duplicates=0),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1939,7 +1939,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
async def start_discovery(self, auto_restart: bool = True) -> None:
|
async def start_discovery(self, auto_restart: bool = True) -> None:
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE), # type: ignore[call-arg]
|
HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1948,7 +1948,7 @@ class Device(CompositeEventEmitter):
|
|||||||
lap=HCI_GENERAL_INQUIRY_LAP,
|
lap=HCI_GENERAL_INQUIRY_LAP,
|
||||||
inquiry_length=DEVICE_DEFAULT_INQUIRY_LENGTH,
|
inquiry_length=DEVICE_DEFAULT_INQUIRY_LENGTH,
|
||||||
num_responses=0, # Unlimited number of responses.
|
num_responses=0, # Unlimited number of responses.
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
if response.status != HCI_Command_Status_Event.PENDING:
|
if response.status != HCI_Command_Status_Event.PENDING:
|
||||||
self.discovering = False
|
self.discovering = False
|
||||||
@@ -1959,7 +1959,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
async def stop_discovery(self) -> None:
|
async def stop_discovery(self) -> None:
|
||||||
if self.discovering:
|
if self.discovering:
|
||||||
await self.send_command(HCI_Inquiry_Cancel_Command(), check_result=True) # type: ignore[call-arg]
|
await self.send_command(HCI_Inquiry_Cancel_Command(), check_result=True)
|
||||||
self.auto_restart_inquiry = True
|
self.auto_restart_inquiry = True
|
||||||
self.discovering = False
|
self.discovering = False
|
||||||
|
|
||||||
@@ -2007,7 +2007,7 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Write_Extended_Inquiry_Response_Command(
|
HCI_Write_Extended_Inquiry_Response_Command(
|
||||||
fec_required=0, extended_inquiry_response=self.inquiry_response
|
fec_required=0, extended_inquiry_response=self.inquiry_response
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
await self.set_scan_enable(
|
await self.set_scan_enable(
|
||||||
@@ -2196,7 +2196,7 @@ class Device(CompositeEventEmitter):
|
|||||||
supervision_timeouts=supervision_timeouts,
|
supervision_timeouts=supervision_timeouts,
|
||||||
min_ce_lengths=min_ce_lengths,
|
min_ce_lengths=min_ce_lengths,
|
||||||
max_ce_lengths=max_ce_lengths,
|
max_ce_lengths=max_ce_lengths,
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if HCI_LE_1M_PHY not in connection_parameters_preferences:
|
if HCI_LE_1M_PHY not in connection_parameters_preferences:
|
||||||
@@ -2225,7 +2225,7 @@ class Device(CompositeEventEmitter):
|
|||||||
supervision_timeout=int(prefs.supervision_timeout / 10),
|
supervision_timeout=int(prefs.supervision_timeout / 10),
|
||||||
min_ce_length=int(prefs.min_ce_length / 0.625),
|
min_ce_length=int(prefs.min_ce_length / 0.625),
|
||||||
max_ce_length=int(prefs.max_ce_length / 0.625),
|
max_ce_length=int(prefs.max_ce_length / 0.625),
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Save pending connection
|
# Save pending connection
|
||||||
@@ -2242,7 +2242,7 @@ class Device(CompositeEventEmitter):
|
|||||||
clock_offset=0x0000,
|
clock_offset=0x0000,
|
||||||
allow_role_switch=0x01,
|
allow_role_switch=0x01,
|
||||||
reserved=0,
|
reserved=0,
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.status != HCI_Command_Status_Event.PENDING:
|
if result.status != HCI_Command_Status_Event.PENDING:
|
||||||
@@ -2261,10 +2261,10 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
if transport == BT_LE_TRANSPORT:
|
if transport == BT_LE_TRANSPORT:
|
||||||
await self.send_command(HCI_LE_Create_Connection_Cancel_Command()) # type: ignore[call-arg]
|
await self.send_command(HCI_LE_Create_Connection_Cancel_Command())
|
||||||
else:
|
else:
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Create_Connection_Cancel_Command(bd_addr=peer_address) # type: ignore[call-arg]
|
HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -2378,7 +2378,7 @@ class Device(CompositeEventEmitter):
|
|||||||
try:
|
try:
|
||||||
# Accept connection request
|
# Accept connection request
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Accept_Connection_Request_Command(bd_addr=peer_address, role=role) # type: ignore[call-arg]
|
HCI_Accept_Connection_Request_Command(bd_addr=peer_address, role=role)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Wait for connection complete
|
# Wait for connection complete
|
||||||
@@ -2445,7 +2445,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
# Request a disconnection
|
# Request a disconnection
|
||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_Disconnect_Command(connection_handle=connection.handle, reason=reason) # type: ignore[call-arg]
|
HCI_Disconnect_Command(connection_handle=connection.handle, reason=reason)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -2476,7 +2476,7 @@ class Device(CompositeEventEmitter):
|
|||||||
connection_handle=connection.handle,
|
connection_handle=connection.handle,
|
||||||
tx_octets=tx_octets,
|
tx_octets=tx_octets,
|
||||||
tx_time=tx_time,
|
tx_time=tx_time,
|
||||||
), # type: ignore[call-arg]
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2522,7 +2522,7 @@ class Device(CompositeEventEmitter):
|
|||||||
supervision_timeout=supervision_timeout,
|
supervision_timeout=supervision_timeout,
|
||||||
min_ce_length=min_ce_length,
|
min_ce_length=min_ce_length,
|
||||||
max_ce_length=max_ce_length,
|
max_ce_length=max_ce_length,
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
if result.status != HCI_Command_Status_Event.PENDING:
|
if result.status != HCI_Command_Status_Event.PENDING:
|
||||||
raise HCI_StatusError(result)
|
raise HCI_StatusError(result)
|
||||||
@@ -2850,7 +2850,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role) # type: ignore[call-arg]
|
HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role)
|
||||||
)
|
)
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -2892,7 +2892,7 @@ class Device(CompositeEventEmitter):
|
|||||||
page_scan_repetition_mode=HCI_Remote_Name_Request_Command.R2,
|
page_scan_repetition_mode=HCI_Remote_Name_Request_Command.R2,
|
||||||
reserved=0,
|
reserved=0,
|
||||||
clock_offset=0, # TODO investigate non-0 values
|
clock_offset=0, # TODO investigate non-0 values
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
@@ -2938,7 +2938,7 @@ class Device(CompositeEventEmitter):
|
|||||||
num_cis = len(cis_id)
|
num_cis = len(cis_id)
|
||||||
|
|
||||||
response = await self.send_command(
|
response = await self.send_command(
|
||||||
HCI_LE_Set_CIG_Parameters_Command( # type: ignore[call-arg]
|
HCI_LE_Set_CIG_Parameters_Command(
|
||||||
cig_id=cig_id,
|
cig_id=cig_id,
|
||||||
sdu_interval_c_to_p=sdu_interval[0],
|
sdu_interval_c_to_p=sdu_interval[0],
|
||||||
sdu_interval_p_to_c=sdu_interval[1],
|
sdu_interval_p_to_c=sdu_interval[1],
|
||||||
@@ -2982,7 +2982,7 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_LE_Create_CIS_Command( # type: ignore[call-arg]
|
HCI_LE_Create_CIS_Command(
|
||||||
cis_connection_handle=[p[0] for p in cis_acl_pairs],
|
cis_connection_handle=[p[0] for p in cis_acl_pairs],
|
||||||
acl_connection_handle=[p[1] for p in cis_acl_pairs],
|
acl_connection_handle=[p[1] for p in cis_acl_pairs],
|
||||||
),
|
),
|
||||||
@@ -3015,9 +3015,7 @@ class Device(CompositeEventEmitter):
|
|||||||
@experimental('Only for testing.')
|
@experimental('Only for testing.')
|
||||||
async def accept_cis_request(self, handle: int) -> CisLink:
|
async def accept_cis_request(self, handle: int) -> CisLink:
|
||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_LE_Accept_CIS_Request_Command( # type: ignore[call-arg]
|
HCI_LE_Accept_CIS_Request_Command(connection_handle=handle),
|
||||||
connection_handle=handle
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -3045,9 +3043,7 @@ class Device(CompositeEventEmitter):
|
|||||||
reason: int = HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
reason: int = HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||||
) -> None:
|
) -> None:
|
||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_LE_Reject_CIS_Request_Command( # type: ignore[call-arg]
|
HCI_LE_Reject_CIS_Request_Command(connection_handle=handle, reason=reason),
|
||||||
connection_handle=handle, reason=reason
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -3439,7 +3435,7 @@ class Device(CompositeEventEmitter):
|
|||||||
try:
|
try:
|
||||||
if await connection.abort_on('disconnection', method()):
|
if await connection.abort_on('disconnection', method()):
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
HCI_User_Confirmation_Request_Reply_Command( # type: ignore[call-arg]
|
HCI_User_Confirmation_Request_Reply_Command(
|
||||||
bd_addr=connection.peer_address
|
bd_addr=connection.peer_address
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -3448,7 +3444,7 @@ class Device(CompositeEventEmitter):
|
|||||||
logger.warning(f'exception while confirming: {error}')
|
logger.warning(f'exception while confirming: {error}')
|
||||||
|
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
HCI_User_Confirmation_Request_Negative_Reply_Command( # type: ignore[call-arg]
|
HCI_User_Confirmation_Request_Negative_Reply_Command(
|
||||||
bd_addr=connection.peer_address
|
bd_addr=connection.peer_address
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -3469,7 +3465,7 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
if number is not None:
|
if number is not None:
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
HCI_User_Passkey_Request_Reply_Command( # type: ignore[call-arg]
|
HCI_User_Passkey_Request_Reply_Command(
|
||||||
bd_addr=connection.peer_address, numeric_value=number
|
bd_addr=connection.peer_address, numeric_value=number
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -3478,7 +3474,7 @@ class Device(CompositeEventEmitter):
|
|||||||
logger.warning(f'exception while asking for pass-key: {error}')
|
logger.warning(f'exception while asking for pass-key: {error}')
|
||||||
|
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
HCI_User_Passkey_Request_Negative_Reply_Command( # type: ignore[call-arg]
|
HCI_User_Passkey_Request_Negative_Reply_Command(
|
||||||
bd_addr=connection.peer_address
|
bd_addr=connection.peer_address
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -561,6 +561,12 @@ HCI_LE_TRANSMITTER_TEST_V4_COMMAND = hci_c
|
|||||||
HCI_LE_SET_DATA_RELATED_ADDRESS_CHANGES_COMMAND = hci_command_op_code(0x08, 0x007C)
|
HCI_LE_SET_DATA_RELATED_ADDRESS_CHANGES_COMMAND = hci_command_op_code(0x08, 0x007C)
|
||||||
HCI_LE_SET_DEFAULT_SUBRATE_COMMAND = hci_command_op_code(0x08, 0x007D)
|
HCI_LE_SET_DEFAULT_SUBRATE_COMMAND = hci_command_op_code(0x08, 0x007D)
|
||||||
HCI_LE_SUBRATE_REQUEST_COMMAND = hci_command_op_code(0x08, 0x007E)
|
HCI_LE_SUBRATE_REQUEST_COMMAND = hci_command_op_code(0x08, 0x007E)
|
||||||
|
HCI_LE_SET_EXTENDED_ADVERTISING_PARAMETERS_V2_COMMAND = hci_command_op_code(0x08, 0x007F)
|
||||||
|
HCI_LE_SET_PERIODIC_ADVERTISING_SUBEVENT_DATA_COMMAND = hci_command_op_code(0x08, 0x0082)
|
||||||
|
HCI_LE_SET_PERIODIC_ADVERTISING_RESPONSE_DATA_COMMAND = hci_command_op_code(0x08, 0x0083)
|
||||||
|
HCI_LE_SET_PERIODIC_SYNC_SUBEVENT_COMMAND = hci_command_op_code(0x08, 0x0084)
|
||||||
|
HCI_LE_EXTENDED_CREATE_CONNECTION_V2_COMMAND = hci_command_op_code(0x08, 0x0085)
|
||||||
|
HCI_LE_SET_PERIODIC_ADVERTISING_PARAMETERS_V2_COMMAND = hci_command_op_code(0x08, 0x0086)
|
||||||
|
|
||||||
|
|
||||||
# HCI Error Codes
|
# HCI Error Codes
|
||||||
@@ -1317,16 +1323,28 @@ HCI_SUPPORTED_COMMANDS_FLAGS = (
|
|||||||
(
|
(
|
||||||
HCI_LE_SET_DEFAULT_SUBRATE_COMMAND,
|
HCI_LE_SET_DEFAULT_SUBRATE_COMMAND,
|
||||||
HCI_LE_SUBRATE_REQUEST_COMMAND,
|
HCI_LE_SUBRATE_REQUEST_COMMAND,
|
||||||
|
HCI_LE_SET_EXTENDED_ADVERTISING_PARAMETERS_V2_COMMAND,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
HCI_LE_SET_PERIODIC_ADVERTISING_SUBEVENT_DATA_COMMAND,
|
||||||
|
HCI_LE_SET_PERIODIC_ADVERTISING_RESPONSE_DATA_COMMAND,
|
||||||
|
HCI_LE_SET_PERIODIC_SYNC_SUBEVENT_COMMAND
|
||||||
|
),
|
||||||
|
# Octet 47
|
||||||
|
(
|
||||||
|
HCI_LE_EXTENDED_CREATE_CONNECTION_V2_COMMAND,
|
||||||
|
HCI_LE_SET_PERIODIC_ADVERTISING_PARAMETERS_V2_COMMAND,
|
||||||
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# LE Supported Features
|
# LE Supported Features
|
||||||
|
# See Bluetooth spec @ Vol 6, Part B, 4.6 FEATURE SUPPORT
|
||||||
HCI_LE_ENCRYPTION_LE_SUPPORTED_FEATURE = 0
|
HCI_LE_ENCRYPTION_LE_SUPPORTED_FEATURE = 0
|
||||||
HCI_CONNECTION_PARAMETERS_REQUEST_PROCEDURE_LE_SUPPORTED_FEATURE = 1
|
HCI_CONNECTION_PARAMETERS_REQUEST_PROCEDURE_LE_SUPPORTED_FEATURE = 1
|
||||||
HCI_EXTENDED_REJECT_INDICATION_LE_SUPPORTED_FEATURE = 2
|
HCI_EXTENDED_REJECT_INDICATION_LE_SUPPORTED_FEATURE = 2
|
||||||
@@ -1367,6 +1385,10 @@ HCI_PERIODIC_ADVERTISING_ADI_SUPPORT_LE_SUPPORTED_FEATURE = 36
|
|||||||
HCI_CONNECTION_SUBRATING_LE_SUPPORTED_FEATURE = 37
|
HCI_CONNECTION_SUBRATING_LE_SUPPORTED_FEATURE = 37
|
||||||
HCI_CONNECTION_SUBRATING_HOST_SUPPORT_LE_SUPPORTED_FEATURE = 38
|
HCI_CONNECTION_SUBRATING_HOST_SUPPORT_LE_SUPPORTED_FEATURE = 38
|
||||||
HCI_CHANNEL_CLASSIFICATION_LE_SUPPORTED_FEATURE = 39
|
HCI_CHANNEL_CLASSIFICATION_LE_SUPPORTED_FEATURE = 39
|
||||||
|
HCI_ADVERTISING_CODING_SELECTION_LE_SUPPORTED_FEATURE = 40
|
||||||
|
HCI_ADVERTISING_CODING_SELECTION_HOST_SUPPORT_LE_SUPPORTED_FEATURE = 41
|
||||||
|
HCI_PERIODIC_ADVERTISING_WITH_RESPONSES_ADVERTISER_LE_SUPPORTED_FEATURE = 43
|
||||||
|
HCI_PERIODIC_ADVERTISING_WITH_RESPONSES_SCANNER_LE_SUPPORTED_FEATURE = 44
|
||||||
|
|
||||||
HCI_LE_SUPPORTED_FEATURES_NAMES = {
|
HCI_LE_SUPPORTED_FEATURES_NAMES = {
|
||||||
flag: feature_name for (feature_name, flag) in globals().items()
|
flag: feature_name for (feature_name, flag) in globals().items()
|
||||||
@@ -1629,7 +1651,7 @@ class HCI_Object:
|
|||||||
field_bytes = bytes(field_value)
|
field_bytes = bytes(field_value)
|
||||||
elif field_type == 'v':
|
elif field_type == 'v':
|
||||||
# Variable-length bytes field, with 1-byte length at the beginning
|
# Variable-length bytes field, with 1-byte length at the beginning
|
||||||
field_bytes = bytes(field_bytes)
|
field_bytes = bytes(field_value)
|
||||||
field_length = len(field_bytes)
|
field_length = len(field_bytes)
|
||||||
field_bytes = bytes([field_length]) + field_bytes
|
field_bytes = bytes([field_length]) + field_bytes
|
||||||
elif isinstance(field_value, (bytes, bytearray)) or hasattr(
|
elif isinstance(field_value, (bytes, bytearray)) or hasattr(
|
||||||
@@ -1986,6 +2008,9 @@ class HCI_Packet:
|
|||||||
if packet_type == HCI_EVENT_PACKET:
|
if packet_type == HCI_EVENT_PACKET:
|
||||||
return HCI_Event.from_bytes(packet)
|
return HCI_Event.from_bytes(packet)
|
||||||
|
|
||||||
|
if packet_type == HCI_ISO_DATA_PACKET:
|
||||||
|
return HCI_IsoDataPacket.from_bytes(packet)
|
||||||
|
|
||||||
return HCI_CustomPacket(packet)
|
return HCI_CustomPacket(packet)
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
@@ -2018,6 +2043,7 @@ class HCI_Command(HCI_Packet):
|
|||||||
hci_packet_type = HCI_COMMAND_PACKET
|
hci_packet_type = HCI_COMMAND_PACKET
|
||||||
command_names: Dict[int, str] = {}
|
command_names: Dict[int, str] = {}
|
||||||
command_classes: Dict[int, Type[HCI_Command]] = {}
|
command_classes: Dict[int, Type[HCI_Command]] = {}
|
||||||
|
op_code: int
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def command(fields=(), return_parameters_fields=()):
|
def command(fields=(), return_parameters_fields=()):
|
||||||
@@ -2103,7 +2129,11 @@ class HCI_Command(HCI_Packet):
|
|||||||
return_parameters.fields = cls.return_parameters_fields
|
return_parameters.fields = cls.return_parameters_fields
|
||||||
return return_parameters
|
return return_parameters
|
||||||
|
|
||||||
def __init__(self, op_code, parameters=None, **kwargs):
|
def __init__(self, op_code=-1, parameters=None, **kwargs):
|
||||||
|
# Since the legacy implementation relies on an __init__ injector, typing always
|
||||||
|
# complains that positional argument op_code is not passed, so here sets a
|
||||||
|
# default value to allow building derived HCI_Command without op_code.
|
||||||
|
assert op_code != -1
|
||||||
super().__init__(HCI_Command.command_name(op_code))
|
super().__init__(HCI_Command.command_name(op_code))
|
||||||
if (fields := getattr(self, 'fields', None)) and kwargs:
|
if (fields := getattr(self, 'fields', None)) and kwargs:
|
||||||
HCI_Object.init_from_fields(self, fields, kwargs)
|
HCI_Object.init_from_fields(self, fields, kwargs)
|
||||||
@@ -6093,7 +6123,7 @@ class HCI_IsoDataPacket(HCI_Packet):
|
|||||||
if ts_flag:
|
if ts_flag:
|
||||||
if not should_include_sdu_info:
|
if not should_include_sdu_info:
|
||||||
logger.warn(f'Timestamp included when pb_flag={bin(pb_flag)}')
|
logger.warn(f'Timestamp included when pb_flag={bin(pb_flag)}')
|
||||||
time_stamp, _ = struct.unpack_from('<I', packet, pos)
|
time_stamp, *_ = struct.unpack_from('<I', packet, pos)
|
||||||
pos += 4
|
pos += 4
|
||||||
|
|
||||||
if should_include_sdu_info:
|
if should_include_sdu_info:
|
||||||
@@ -6160,7 +6190,7 @@ class HCI_IsoDataPacket(HCI_Packet):
|
|||||||
self.packet_sequence_number,
|
self.packet_sequence_number,
|
||||||
self.iso_sdu_length | self.packet_status_flag << 14,
|
self.iso_sdu_length | self.packet_status_flag << 14,
|
||||||
]
|
]
|
||||||
return struct.pack(fmt, args) + self.iso_sdu_fragment
|
return struct.pack(fmt, *args) + self.iso_sdu_fragment
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1926,7 +1926,7 @@ class ChannelManager:
|
|||||||
supervision_timeout=request.timeout,
|
supervision_timeout=request.timeout,
|
||||||
min_ce_length=0,
|
min_ce_length=0,
|
||||||
max_ce_length=0,
|
max_ce_length=0,
|
||||||
) # type: ignore[call-arg]
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.send_control_frame(
|
self.send_control_frame(
|
||||||
|
|||||||
496
bumble/profiles/bap.py
Normal file
496
bumble/profiles/bap.py
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
import dataclasses
|
||||||
|
import enum
|
||||||
|
import struct
|
||||||
|
import functools
|
||||||
|
from typing import Optional, List, Union
|
||||||
|
|
||||||
|
from bumble import hci
|
||||||
|
from bumble import gatt
|
||||||
|
from bumble import gatt_client
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Constants
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class AudioLocation(enum.IntFlag):
|
||||||
|
'''Bluetooth Assigned Numbers, Section 6.12.1 - Audio Location'''
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
NOT_ALLOWED = 0x00000000
|
||||||
|
FRONT_LEFT = 0x00000001
|
||||||
|
FRONT_RIGHT = 0x00000002
|
||||||
|
FRONT_CENTER = 0x00000004
|
||||||
|
LOW_FREQUENCY_EFFECTS_1 = 0x00000008
|
||||||
|
BACK_LEFT = 0x00000010
|
||||||
|
BACK_RIGHT = 0x00000020
|
||||||
|
FRONT_LEFT_OF_CENTER = 0x00000040
|
||||||
|
FRONT_RIGHT_OF_CENTER = 0x00000080
|
||||||
|
BACK_CENTER = 0x00000100
|
||||||
|
LOW_FREQUENCY_EFFECTS_2 = 0x00000200
|
||||||
|
SIDE_LEFT = 0x00000400
|
||||||
|
SIDE_RIGHT = 0x00000800
|
||||||
|
TOP_FRONT_LEFT = 0x00001000
|
||||||
|
TOP_FRONT_RIGHT = 0x00002000
|
||||||
|
TOP_FRONT_CENTER = 0x00004000
|
||||||
|
TOP_CENTER = 0x00008000
|
||||||
|
TOP_BACK_LEFT = 0x00010000
|
||||||
|
TOP_BACK_RIGHT = 0x00020000
|
||||||
|
TOP_SIDE_LEFT = 0x00040000
|
||||||
|
TOP_SIDE_RIGHT = 0x00080000
|
||||||
|
TOP_BACK_CENTER = 0x00100000
|
||||||
|
BOTTOM_FRONT_CENTER = 0x00200000
|
||||||
|
BOTTOM_FRONT_LEFT = 0x00400000
|
||||||
|
BOTTOM_FRONT_RIGHT = 0x00800000
|
||||||
|
FRONT_LEFT_WIDE = 0x01000000
|
||||||
|
FRONT_RIGHT_WIDE = 0x02000000
|
||||||
|
LEFT_SURROUND = 0x04000000
|
||||||
|
RIGHT_SURROUND = 0x08000000
|
||||||
|
|
||||||
|
|
||||||
|
class AudioInputType(enum.IntEnum):
|
||||||
|
'''Bluetooth Assigned Numbers, Section 6.12.2 - Audio Input Type'''
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
UNSPECIFIED = 0x00
|
||||||
|
BLUETOOTH = 0x01
|
||||||
|
MICROPHONE = 0x02
|
||||||
|
ANALOG = 0x03
|
||||||
|
DIGITAL = 0x04
|
||||||
|
RADIO = 0x05
|
||||||
|
STREAMING = 0x06
|
||||||
|
AMBIENT = 0x07
|
||||||
|
|
||||||
|
|
||||||
|
class ContextType(enum.IntFlag):
|
||||||
|
'''Bluetooth Assigned Numbers, Section 6.12.3 - Context Type'''
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
PROHIBITED = 0x0000
|
||||||
|
CONVERSATIONAL = 0x0002
|
||||||
|
MEDIA = 0x0004
|
||||||
|
GAME = 0x0008
|
||||||
|
INSTRUCTIONAL = 0x0010
|
||||||
|
VOICE_ASSISTANTS = 0x0020
|
||||||
|
LIVE = 0x0040
|
||||||
|
SOUND_EFFECTS = 0x0080
|
||||||
|
NOTIFICATIONS = 0x0100
|
||||||
|
RINGTONE = 0x0200
|
||||||
|
ALERTS = 0x0400
|
||||||
|
EMERGENCY_ALARM = 0x0800
|
||||||
|
|
||||||
|
|
||||||
|
class SamplingFrequency(enum.IntEnum):
|
||||||
|
'''Bluetooth Assigned Numbers, Section 6.12.5.1 - Sampling Frequency'''
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
FREQ_8000 = 0x01
|
||||||
|
FREQ_11025 = 0x02
|
||||||
|
FREQ_16000 = 0x03
|
||||||
|
FREQ_22050 = 0x04
|
||||||
|
FREQ_24000 = 0x05
|
||||||
|
FREQ_32000 = 0x06
|
||||||
|
FREQ_44100 = 0x07
|
||||||
|
FREQ_48000 = 0x08
|
||||||
|
FREQ_88200 = 0x09
|
||||||
|
FREQ_96000 = 0x0A
|
||||||
|
FREQ_176400 = 0x0B
|
||||||
|
FREQ_192000 = 0x0C
|
||||||
|
FREQ_384000 = 0x0D
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_hz(cls, frequency: int) -> SamplingFrequency:
|
||||||
|
return {
|
||||||
|
8000: SamplingFrequency.FREQ_8000,
|
||||||
|
11025: SamplingFrequency.FREQ_11025,
|
||||||
|
16000: SamplingFrequency.FREQ_16000,
|
||||||
|
22050: SamplingFrequency.FREQ_22050,
|
||||||
|
24000: SamplingFrequency.FREQ_24000,
|
||||||
|
32000: SamplingFrequency.FREQ_32000,
|
||||||
|
44100: SamplingFrequency.FREQ_44100,
|
||||||
|
48000: SamplingFrequency.FREQ_48000,
|
||||||
|
88200: SamplingFrequency.FREQ_88200,
|
||||||
|
96000: SamplingFrequency.FREQ_96000,
|
||||||
|
176400: SamplingFrequency.FREQ_176400,
|
||||||
|
192000: SamplingFrequency.FREQ_192000,
|
||||||
|
384000: SamplingFrequency.FREQ_384000,
|
||||||
|
}[frequency]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hz(self) -> int:
|
||||||
|
return {
|
||||||
|
SamplingFrequency.FREQ_8000: 8000,
|
||||||
|
SamplingFrequency.FREQ_11025: 11025,
|
||||||
|
SamplingFrequency.FREQ_16000: 16000,
|
||||||
|
SamplingFrequency.FREQ_22050: 22050,
|
||||||
|
SamplingFrequency.FREQ_24000: 24000,
|
||||||
|
SamplingFrequency.FREQ_32000: 32000,
|
||||||
|
SamplingFrequency.FREQ_44100: 44100,
|
||||||
|
SamplingFrequency.FREQ_48000: 48000,
|
||||||
|
SamplingFrequency.FREQ_88200: 88200,
|
||||||
|
SamplingFrequency.FREQ_96000: 96000,
|
||||||
|
SamplingFrequency.FREQ_176400: 176400,
|
||||||
|
SamplingFrequency.FREQ_192000: 192000,
|
||||||
|
SamplingFrequency.FREQ_384000: 384000,
|
||||||
|
}[self]
|
||||||
|
|
||||||
|
|
||||||
|
class SupportedSamplingFrequency(enum.IntFlag):
|
||||||
|
'''Bluetooth Assigned Numbers, Section 6.12.4.1 - Sample Frequency'''
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
FREQ_8000 = 1 << (SamplingFrequency.FREQ_8000 - 1)
|
||||||
|
FREQ_11025 = 1 << (SamplingFrequency.FREQ_11025 - 1)
|
||||||
|
FREQ_16000 = 1 << (SamplingFrequency.FREQ_16000 - 1)
|
||||||
|
FREQ_22050 = 1 << (SamplingFrequency.FREQ_22050 - 1)
|
||||||
|
FREQ_24000 = 1 << (SamplingFrequency.FREQ_24000 - 1)
|
||||||
|
FREQ_32000 = 1 << (SamplingFrequency.FREQ_32000 - 1)
|
||||||
|
FREQ_44100 = 1 << (SamplingFrequency.FREQ_44100 - 1)
|
||||||
|
FREQ_48000 = 1 << (SamplingFrequency.FREQ_48000 - 1)
|
||||||
|
FREQ_88200 = 1 << (SamplingFrequency.FREQ_88200 - 1)
|
||||||
|
FREQ_96000 = 1 << (SamplingFrequency.FREQ_96000 - 1)
|
||||||
|
FREQ_176400 = 1 << (SamplingFrequency.FREQ_176400 - 1)
|
||||||
|
FREQ_192000 = 1 << (SamplingFrequency.FREQ_192000 - 1)
|
||||||
|
FREQ_384000 = 1 << (SamplingFrequency.FREQ_384000 - 1)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_hz(cls, frequencies: Sequence[int]) -> SupportedSamplingFrequency:
|
||||||
|
MAPPING = {
|
||||||
|
8000: SupportedSamplingFrequency.FREQ_8000,
|
||||||
|
11025: SupportedSamplingFrequency.FREQ_11025,
|
||||||
|
16000: SupportedSamplingFrequency.FREQ_16000,
|
||||||
|
22050: SupportedSamplingFrequency.FREQ_22050,
|
||||||
|
24000: SupportedSamplingFrequency.FREQ_24000,
|
||||||
|
32000: SupportedSamplingFrequency.FREQ_32000,
|
||||||
|
44100: SupportedSamplingFrequency.FREQ_44100,
|
||||||
|
48000: SupportedSamplingFrequency.FREQ_48000,
|
||||||
|
88200: SupportedSamplingFrequency.FREQ_88200,
|
||||||
|
96000: SupportedSamplingFrequency.FREQ_96000,
|
||||||
|
176400: SupportedSamplingFrequency.FREQ_176400,
|
||||||
|
192000: SupportedSamplingFrequency.FREQ_192000,
|
||||||
|
384000: SupportedSamplingFrequency.FREQ_384000,
|
||||||
|
}
|
||||||
|
|
||||||
|
return functools.reduce(
|
||||||
|
lambda x, y: x | MAPPING[y],
|
||||||
|
frequencies,
|
||||||
|
cls(0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FrameDuration(enum.IntEnum):
|
||||||
|
'''Bluetooth Assigned Numbers, Section 6.12.5.2 - Frame Duration'''
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
DURATION_7500_US = 0x00
|
||||||
|
DURATION_10000_US = 0x01
|
||||||
|
|
||||||
|
|
||||||
|
class SupportedFrameDuration(enum.IntFlag):
|
||||||
|
'''Bluetooth Assigned Numbers, Section 6.12.4.2 - Frame Duration'''
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
DURATION_7500_US_SUPPORTED = 0b0001
|
||||||
|
DURATION_10000_US_SUPPORTED = 0b0010
|
||||||
|
DURATION_7500_US_PREFERRED = 0b0001
|
||||||
|
DURATION_10000_US_PREFERRED = 0b0010
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Utils
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def bits_to_channel_counts(data: int) -> List[int]:
|
||||||
|
pos = 0
|
||||||
|
counts = []
|
||||||
|
while data != 0:
|
||||||
|
# Bit 0 = count 1
|
||||||
|
# Bit 1 = count 2, and so on
|
||||||
|
pos += 1
|
||||||
|
if data & 1:
|
||||||
|
counts.append(pos)
|
||||||
|
data >>= 1
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
def channel_counts_to_bits(counts: Sequence[int]) -> int:
|
||||||
|
return sum(set([1 << (count - 1) for count in counts]))
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Structures
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CodecSpecificCapabilities:
|
||||||
|
'''See:
|
||||||
|
* Bluetooth Assigned Numbers, 6.12.4 - Codec Specific Capabilities LTV Structures
|
||||||
|
* Basic Audio Profile, 4.3.1 - Codec_Specific_Capabilities LTV requirements
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Type(enum.IntEnum):
|
||||||
|
# fmt: off
|
||||||
|
SAMPLING_FREQUENCY = 0x01
|
||||||
|
FRAME_DURATION = 0x02
|
||||||
|
AUDIO_CHANNEL_COUNT = 0x03
|
||||||
|
OCTETS_PER_FRAME = 0x04
|
||||||
|
CODEC_FRAMES_PER_SDU = 0x05
|
||||||
|
|
||||||
|
supported_sampling_frequencies: SupportedSamplingFrequency
|
||||||
|
supported_frame_durations: SupportedFrameDuration
|
||||||
|
supported_audio_channel_counts: Sequence[int]
|
||||||
|
min_octets_per_codec_frame: int
|
||||||
|
max_octets_per_codec_frame: int
|
||||||
|
supported_max_codec_frames_per_sdu: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> CodecSpecificCapabilities:
|
||||||
|
offset = 0
|
||||||
|
# Allowed default values.
|
||||||
|
supported_audio_channel_counts = [1]
|
||||||
|
supported_max_codec_frames_per_sdu = 1
|
||||||
|
while offset < len(data):
|
||||||
|
length, type = struct.unpack_from('BB', data, offset)
|
||||||
|
offset += 2
|
||||||
|
value = int.from_bytes(data[offset : offset + length - 1], 'little')
|
||||||
|
offset += length - 1
|
||||||
|
|
||||||
|
if type == CodecSpecificCapabilities.Type.SAMPLING_FREQUENCY:
|
||||||
|
supported_sampling_frequencies = SupportedSamplingFrequency(value)
|
||||||
|
elif type == CodecSpecificCapabilities.Type.FRAME_DURATION:
|
||||||
|
supported_frame_durations = SupportedFrameDuration(value)
|
||||||
|
elif type == CodecSpecificCapabilities.Type.AUDIO_CHANNEL_COUNT:
|
||||||
|
supported_audio_channel_counts = bits_to_channel_counts(value)
|
||||||
|
elif type == CodecSpecificCapabilities.Type.OCTETS_PER_FRAME:
|
||||||
|
min_octets_per_sample = value & 0xFFFF
|
||||||
|
max_octets_per_sample = value >> 16
|
||||||
|
elif type == CodecSpecificCapabilities.Type.CODEC_FRAMES_PER_SDU:
|
||||||
|
supported_max_codec_frames_per_sdu = value
|
||||||
|
|
||||||
|
# It is expected here that if some fields are missing, an error should be raised.
|
||||||
|
return CodecSpecificCapabilities(
|
||||||
|
supported_sampling_frequencies=supported_sampling_frequencies,
|
||||||
|
supported_frame_durations=supported_frame_durations,
|
||||||
|
supported_audio_channel_counts=supported_audio_channel_counts,
|
||||||
|
min_octets_per_codec_frame=min_octets_per_sample,
|
||||||
|
max_octets_per_codec_frame=max_octets_per_sample,
|
||||||
|
supported_max_codec_frames_per_sdu=supported_max_codec_frames_per_sdu,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return struct.pack(
|
||||||
|
'<BBHBBBBBBBBHHBBB',
|
||||||
|
3,
|
||||||
|
CodecSpecificCapabilities.Type.SAMPLING_FREQUENCY,
|
||||||
|
self.supported_sampling_frequencies,
|
||||||
|
2,
|
||||||
|
CodecSpecificCapabilities.Type.FRAME_DURATION,
|
||||||
|
self.supported_frame_durations,
|
||||||
|
2,
|
||||||
|
CodecSpecificCapabilities.Type.AUDIO_CHANNEL_COUNT,
|
||||||
|
channel_counts_to_bits(self.supported_audio_channel_counts),
|
||||||
|
5,
|
||||||
|
CodecSpecificCapabilities.Type.OCTETS_PER_FRAME,
|
||||||
|
self.min_octets_per_codec_frame,
|
||||||
|
self.max_octets_per_codec_frame,
|
||||||
|
2,
|
||||||
|
CodecSpecificCapabilities.Type.CODEC_FRAMES_PER_SDU,
|
||||||
|
self.supported_max_codec_frames_per_sdu,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class PacRecord:
|
||||||
|
coding_format: hci.CodingFormat
|
||||||
|
codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
|
||||||
|
# TODO: Parse Metadata
|
||||||
|
metadata: bytes = b''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> PacRecord:
|
||||||
|
offset, coding_format = hci.CodingFormat.parse_from_bytes(data, 0)
|
||||||
|
codec_specific_capabilities_size = data[offset]
|
||||||
|
|
||||||
|
offset += 1
|
||||||
|
codec_specific_capabilities_bytes = data[
|
||||||
|
offset : offset + codec_specific_capabilities_size
|
||||||
|
]
|
||||||
|
offset += codec_specific_capabilities_size
|
||||||
|
metadata_size = data[offset]
|
||||||
|
metadata = data[offset : offset + metadata_size]
|
||||||
|
|
||||||
|
codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
|
||||||
|
if coding_format.codec_id == hci.CodecID.VENDOR_SPECIFIC:
|
||||||
|
codec_specific_capabilities = codec_specific_capabilities_bytes
|
||||||
|
else:
|
||||||
|
codec_specific_capabilities = CodecSpecificCapabilities.from_bytes(
|
||||||
|
codec_specific_capabilities_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
return PacRecord(
|
||||||
|
coding_format=coding_format,
|
||||||
|
codec_specific_capabilities=codec_specific_capabilities,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
capabilities_bytes = bytes(self.codec_specific_capabilities)
|
||||||
|
return (
|
||||||
|
bytes(self.coding_format)
|
||||||
|
+ bytes([len(capabilities_bytes)])
|
||||||
|
+ capabilities_bytes
|
||||||
|
+ bytes([len(self.metadata)])
|
||||||
|
+ self.metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Server
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class PublishedAudioCapabilitiesService(gatt.TemplateService):
|
||||||
|
UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
|
||||||
|
|
||||||
|
sink_pac: Optional[gatt.Characteristic]
|
||||||
|
sink_audio_locations: Optional[gatt.Characteristic]
|
||||||
|
source_pac: Optional[gatt.Characteristic]
|
||||||
|
source_audio_locations: Optional[gatt.Characteristic]
|
||||||
|
available_audio_contexts: gatt.Characteristic
|
||||||
|
supported_audio_contexts: gatt.Characteristic
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
supported_source_context: ContextType,
|
||||||
|
supported_sink_context: ContextType,
|
||||||
|
available_source_context: ContextType,
|
||||||
|
available_sink_context: ContextType,
|
||||||
|
sink_pac: Sequence[PacRecord] = [],
|
||||||
|
sink_audio_locations: Optional[AudioLocation] = None,
|
||||||
|
source_pac: Sequence[PacRecord] = [],
|
||||||
|
source_audio_locations: Optional[AudioLocation] = None,
|
||||||
|
) -> None:
|
||||||
|
characteristics = []
|
||||||
|
|
||||||
|
self.supported_audio_contexts = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.READ,
|
||||||
|
permissions=gatt.Characteristic.Permissions.READABLE,
|
||||||
|
value=struct.pack('<HH', supported_sink_context, supported_source_context),
|
||||||
|
)
|
||||||
|
characteristics.append(self.supported_audio_contexts)
|
||||||
|
|
||||||
|
self.available_audio_contexts = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.NOTIFY,
|
||||||
|
permissions=gatt.Characteristic.Permissions.READABLE,
|
||||||
|
value=struct.pack('<HH', available_sink_context, available_source_context),
|
||||||
|
)
|
||||||
|
characteristics.append(self.available_audio_contexts)
|
||||||
|
|
||||||
|
if sink_pac:
|
||||||
|
self.sink_pac = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_SINK_PAC_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.READ,
|
||||||
|
permissions=gatt.Characteristic.Permissions.READABLE,
|
||||||
|
value=bytes([len(sink_pac)]) + b''.join(map(bytes, sink_pac)),
|
||||||
|
)
|
||||||
|
characteristics.append(self.sink_pac)
|
||||||
|
|
||||||
|
if sink_audio_locations is not None:
|
||||||
|
self.sink_audio_locations = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.READ,
|
||||||
|
permissions=gatt.Characteristic.Permissions.READABLE,
|
||||||
|
value=struct.pack('<I', sink_audio_locations),
|
||||||
|
)
|
||||||
|
characteristics.append(self.sink_audio_locations)
|
||||||
|
|
||||||
|
if source_pac:
|
||||||
|
self.source_pac = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_SOURCE_PAC_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.READ,
|
||||||
|
permissions=gatt.Characteristic.Permissions.READABLE,
|
||||||
|
value=bytes([len(source_pac)]) + b''.join(map(bytes, source_pac)),
|
||||||
|
)
|
||||||
|
characteristics.append(self.source_pac)
|
||||||
|
|
||||||
|
if source_audio_locations is not None:
|
||||||
|
self.source_audio_locations = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.READ,
|
||||||
|
permissions=gatt.Characteristic.Permissions.READABLE,
|
||||||
|
value=struct.pack('<I', source_audio_locations),
|
||||||
|
)
|
||||||
|
characteristics.append(self.source_audio_locations)
|
||||||
|
|
||||||
|
super().__init__(characteristics)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Client
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
|
||||||
|
SERVICE_CLASS = PublishedAudioCapabilitiesService
|
||||||
|
|
||||||
|
sink_pac: Optional[gatt_client.CharacteristicProxy] = None
|
||||||
|
sink_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
|
||||||
|
source_pac: Optional[gatt_client.CharacteristicProxy] = None
|
||||||
|
source_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
|
||||||
|
available_audio_contexts: gatt_client.CharacteristicProxy
|
||||||
|
supported_audio_contexts: gatt_client.CharacteristicProxy
|
||||||
|
|
||||||
|
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
||||||
|
self.service_proxy = service_proxy
|
||||||
|
|
||||||
|
self.available_audio_contexts = service_proxy.get_characteristics_by_uuid(
|
||||||
|
gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
|
||||||
|
)[0]
|
||||||
|
self.supported_audio_contexts = service_proxy.get_characteristics_by_uuid(
|
||||||
|
gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||||
|
gatt.GATT_SINK_PAC_CHARACTERISTIC
|
||||||
|
):
|
||||||
|
self.sink_pac = characteristics[0]
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||||
|
gatt.GATT_SOURCE_PAC_CHARACTERISTIC
|
||||||
|
):
|
||||||
|
self.source_pac = characteristics[0]
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||||
|
gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC
|
||||||
|
):
|
||||||
|
self.sink_audio_locations = characteristics[0]
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||||
|
gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC
|
||||||
|
):
|
||||||
|
self.source_audio_locations = characteristics[0]
|
||||||
@@ -1090,7 +1090,7 @@ class Session:
|
|||||||
# We can now encrypt the connection with the short term key, so that we can
|
# We can now encrypt the connection with the short term key, so that we can
|
||||||
# distribute the long term and/or other keys over an encrypted connection
|
# distribute the long term and/or other keys over an encrypted connection
|
||||||
self.manager.device.host.send_command_sync(
|
self.manager.device.host.send_command_sync(
|
||||||
HCI_LE_Enable_Encryption_Command( # type: ignore[call-arg]
|
HCI_LE_Enable_Encryption_Command(
|
||||||
connection_handle=self.connection.handle,
|
connection_handle=self.connection.handle,
|
||||||
random_number=bytes(8),
|
random_number=bytes(8),
|
||||||
encrypted_diversifier=0,
|
encrypted_diversifier=0,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ HCI_PACKET_INFO: Dict[int, Tuple[int, int, str]] = {
|
|||||||
hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
|
hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
|
||||||
hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'),
|
hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'),
|
||||||
hci.HCI_EVENT_PACKET: (1, 1, 'B'),
|
hci.HCI_EVENT_PACKET: (1, 1, 'B'),
|
||||||
|
hci.HCI_ISO_DATA_PACKET: (2, 2, 'H'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ async def main() -> None:
|
|||||||
HCI_Enhanced_Setup_Synchronous_Connection_Command(
|
HCI_Enhanced_Setup_Synchronous_Connection_Command(
|
||||||
connection_handle=connections[0].handle,
|
connection_handle=connections[0].handle,
|
||||||
**ESCO_PARAMETERS[DefaultCodecParameters.ESCO_CVSD_S3].asdict(),
|
**ESCO_PARAMETERS[DefaultCodecParameters.ESCO_CVSD_S3].asdict(),
|
||||||
# type: ignore[call-args]
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
134
examples/run_unicast_server.py
Normal file
134
examples/run_unicast_server.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# 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
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from bumble.core import AdvertisingData
|
||||||
|
from bumble.device import Device
|
||||||
|
from bumble.hci import (
|
||||||
|
CodecID,
|
||||||
|
CodingFormat,
|
||||||
|
OwnAddressType,
|
||||||
|
HCI_LE_Set_Extended_Advertising_Parameters_Command,
|
||||||
|
)
|
||||||
|
from bumble.profiles.bap import (
|
||||||
|
CodecSpecificCapabilities,
|
||||||
|
ContextType,
|
||||||
|
AudioLocation,
|
||||||
|
SupportedSamplingFrequency,
|
||||||
|
SupportedFrameDuration,
|
||||||
|
PacRecord,
|
||||||
|
PublishedAudioCapabilitiesService,
|
||||||
|
)
|
||||||
|
|
||||||
|
from bumble.transport import open_transport_or_link
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def main() -> None:
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print('Usage: run_cig_setup.py <config-file>' '<transport-spec-for-device>')
|
||||||
|
return
|
||||||
|
|
||||||
|
print('<<< connecting to HCI...')
|
||||||
|
async with await open_transport_or_link(sys.argv[2]) as hci_transport:
|
||||||
|
print('<<< connected')
|
||||||
|
|
||||||
|
device = Device.from_config_file_with_hci(
|
||||||
|
sys.argv[1], hci_transport.source, hci_transport.sink
|
||||||
|
)
|
||||||
|
device.cis_enabled = True
|
||||||
|
|
||||||
|
await device.power_on()
|
||||||
|
|
||||||
|
device.add_service(
|
||||||
|
PublishedAudioCapabilitiesService(
|
||||||
|
supported_source_context=ContextType.PROHIBITED,
|
||||||
|
available_source_context=ContextType.PROHIBITED,
|
||||||
|
supported_sink_context=ContextType.MEDIA,
|
||||||
|
available_sink_context=ContextType.MEDIA,
|
||||||
|
sink_audio_locations=(
|
||||||
|
AudioLocation.FRONT_LEFT | AudioLocation.FRONT_RIGHT
|
||||||
|
),
|
||||||
|
sink_pac=[
|
||||||
|
# Codec Capability Setting 16_2
|
||||||
|
PacRecord(
|
||||||
|
coding_format=CodingFormat(CodecID.LC3),
|
||||||
|
codec_specific_capabilities=CodecSpecificCapabilities(
|
||||||
|
supported_sampling_frequencies=(
|
||||||
|
SupportedSamplingFrequency.FREQ_16000
|
||||||
|
),
|
||||||
|
supported_frame_durations=(
|
||||||
|
SupportedFrameDuration.DURATION_10000_US_SUPPORTED
|
||||||
|
),
|
||||||
|
supported_audio_channel_counts=[1],
|
||||||
|
min_octets_per_codec_frame=40,
|
||||||
|
max_octets_per_codec_frame=40,
|
||||||
|
supported_max_codec_frames_per_sdu=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# Codec Capability Setting 24_2
|
||||||
|
PacRecord(
|
||||||
|
coding_format=CodingFormat(CodecID.LC3),
|
||||||
|
codec_specific_capabilities=CodecSpecificCapabilities(
|
||||||
|
supported_sampling_frequencies=(
|
||||||
|
SupportedSamplingFrequency.FREQ_24000
|
||||||
|
),
|
||||||
|
supported_frame_durations=(
|
||||||
|
SupportedFrameDuration.DURATION_10000_US_SUPPORTED
|
||||||
|
),
|
||||||
|
supported_audio_channel_counts=[1],
|
||||||
|
min_octets_per_codec_frame=60,
|
||||||
|
max_octets_per_codec_frame=60,
|
||||||
|
supported_max_codec_frames_per_sdu=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
advertising_data = bytes(
|
||||||
|
AdvertisingData(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||||
|
bytes('Bumble LE Audio', 'utf-8'),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||||
|
bytes(PublishedAudioCapabilitiesService.UUID),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await device.start_extended_advertising(
|
||||||
|
advertising_properties=(
|
||||||
|
HCI_LE_Set_Extended_Advertising_Parameters_Command.AdvertisingProperties.CONNECTABLE_ADVERTISING
|
||||||
|
),
|
||||||
|
own_address_type=OwnAddressType.RANDOM,
|
||||||
|
advertising_data=advertising_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hci_transport.source.terminated
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||||
|
asyncio.run(main())
|
||||||
@@ -16,17 +16,77 @@ package com.github.google.bumble.btbench
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import java.io.IOException
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCallback
|
||||||
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
private val Log = Logger.getLogger("btbench.l2cap-client")
|
private val Log = Logger.getLogger("btbench.l2cap-client")
|
||||||
|
|
||||||
class L2capClient(private val viewModel: AppViewModel, val bluetoothAdapter: BluetoothAdapter) {
|
class L2capClient(
|
||||||
|
private val viewModel: AppViewModel,
|
||||||
|
val bluetoothAdapter: BluetoothAdapter,
|
||||||
|
val context: Context
|
||||||
|
) {
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun run() {
|
fun run() {
|
||||||
viewModel.running = true
|
viewModel.running = true
|
||||||
val remoteDevice = bluetoothAdapter.getRemoteDevice(viewModel.peerBluetoothAddress)
|
val addressIsPublic = viewModel.peerBluetoothAddress.endsWith("/P")
|
||||||
|
val address = viewModel.peerBluetoothAddress.take(17)
|
||||||
|
val remoteDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
bluetoothAdapter.getRemoteLeDevice(
|
||||||
|
address,
|
||||||
|
if (addressIsPublic) {
|
||||||
|
BluetoothDevice.ADDRESS_TYPE_PUBLIC
|
||||||
|
} else {
|
||||||
|
BluetoothDevice.ADDRESS_TYPE_RANDOM
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
bluetoothAdapter.getRemoteDevice(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
val gatt = remoteDevice.connectGatt(
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
object : BluetoothGattCallback() {
|
||||||
|
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
||||||
|
Log.info("MTU update: mtu=$mtu status=$status")
|
||||||
|
viewModel.mtu = mtu
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
|
||||||
|
Log.info("PHY update: tx=$txPhy, rx=$rxPhy, status=$status")
|
||||||
|
viewModel.txPhy = txPhy
|
||||||
|
viewModel.rxPhy = rxPhy
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
|
||||||
|
Log.info("PHY: tx=$txPhy, rx=$rxPhy, status=$status")
|
||||||
|
viewModel.txPhy = txPhy
|
||||||
|
viewModel.rxPhy = rxPhy
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnectionStateChange(
|
||||||
|
gatt: BluetoothGatt?, status: Int, newState: Int
|
||||||
|
) {
|
||||||
|
if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) {
|
||||||
|
gatt.setPreferredPhy(
|
||||||
|
BluetoothDevice.PHY_LE_2M_MASK,
|
||||||
|
BluetoothDevice.PHY_LE_2M_MASK,
|
||||||
|
BluetoothDevice.PHY_OPTION_NO_PREFERRED
|
||||||
|
)
|
||||||
|
gatt.readPhy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BluetoothDevice.TRANSPORT_LE,
|
||||||
|
if (viewModel.use2mPhy) BluetoothDevice.PHY_LE_2M_MASK else BluetoothDevice.PHY_LE_1M_MASK
|
||||||
|
)
|
||||||
|
|
||||||
val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm)
|
val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm)
|
||||||
|
|
||||||
val client = SocketClient(viewModel, socket)
|
val client = SocketClient(viewModel, socket)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ private val Log = Logger.getLogger("btbench.l2cap-server")
|
|||||||
class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdapter: BluetoothAdapter) {
|
class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdapter: BluetoothAdapter) {
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun run() {
|
fun run() {
|
||||||
// Advertise to that the peer can find us and connect.
|
// Advertise so that the peer can find us and connect.
|
||||||
val callback = object: AdvertiseCallback() {
|
val callback = object: AdvertiseCallback() {
|
||||||
override fun onStartFailure(errorCode: Int) {
|
override fun onStartFailure(errorCode: Int) {
|
||||||
Log.warning("failed to start advertising: $errorCode")
|
Log.warning("failed to start advertising: $errorCode")
|
||||||
@@ -50,13 +50,12 @@ class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdap
|
|||||||
val advertiseData = AdvertiseData.Builder().build()
|
val advertiseData = AdvertiseData.Builder().build()
|
||||||
val scanData = AdvertiseData.Builder().setIncludeDeviceName(true).build()
|
val scanData = AdvertiseData.Builder().setIncludeDeviceName(true).build()
|
||||||
val advertiser = bluetoothAdapter.bluetoothLeAdvertiser
|
val advertiser = bluetoothAdapter.bluetoothLeAdvertiser
|
||||||
advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback)
|
|
||||||
|
|
||||||
val serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel()
|
val serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel()
|
||||||
viewModel.l2capPsm = serverSocket.psm
|
viewModel.l2capPsm = serverSocket.psm
|
||||||
Log.info("psm = $serverSocket.psm")
|
Log.info("psm = $serverSocket.psm")
|
||||||
|
|
||||||
val server = SocketServer(viewModel, serverSocket)
|
val server = SocketServer(viewModel, serverSocket)
|
||||||
server.run({ advertiser.stopAdvertising(callback) })
|
server.run({ advertiser.stopAdvertising(callback) }, { advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,23 +26,33 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -171,7 +181,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun runL2capClient() {
|
private fun runL2capClient() {
|
||||||
val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it) }
|
val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it, baseContext) }
|
||||||
l2capClient?.run()
|
l2capClient?.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,9 +209,12 @@ fun MainView(
|
|||||||
runL2capServer: () -> Unit
|
runL2capServer: () -> Unit
|
||||||
) {
|
) {
|
||||||
BTBenchTheme {
|
BTBenchTheme {
|
||||||
// A surface container using the 'background' color from the theme
|
val scrollState = rememberScrollState()
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
Text(
|
Text(
|
||||||
@@ -212,28 +225,33 @@ fun MainView(
|
|||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
TextField(label = {
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
TextField(
|
||||||
|
label = {
|
||||||
Text(text = "Peer Bluetooth Address")
|
Text(text = "Peer Bluetooth Address")
|
||||||
},
|
},
|
||||||
value = appViewModel.peerBluetoothAddress,
|
value = appViewModel.peerBluetoothAddress,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||||
keyboardOptions = KeyboardOptions.Default.copy(
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done
|
keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done
|
||||||
),
|
),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
appViewModel.updatePeerBluetoothAddress(it)
|
appViewModel.updatePeerBluetoothAddress(it)
|
||||||
},
|
},
|
||||||
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
keyboardActions = KeyboardActions(onDone = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
focusManager.clearFocus()
|
||||||
|
})
|
||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
TextField(label = {
|
TextField(label = {
|
||||||
Text(text = "L2CAP PSM")
|
Text(text = "L2CAP PSM")
|
||||||
},
|
},
|
||||||
value = appViewModel.l2capPsm.toString(),
|
value = appViewModel.l2capPsm.toString(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||||
keyboardOptions = KeyboardOptions.Default.copy(
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
keyboardType = KeyboardType.Number,
|
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
|
||||||
imeAction = ImeAction.Done
|
|
||||||
),
|
),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
if (it.isNotEmpty()) {
|
if (it.isNotEmpty()) {
|
||||||
@@ -243,7 +261,11 @@ fun MainView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }))
|
keyboardActions = KeyboardActions(onDone = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
focusManager.clearFocus()
|
||||||
|
})
|
||||||
|
)
|
||||||
Divider()
|
Divider()
|
||||||
Slider(
|
Slider(
|
||||||
value = appViewModel.senderPacketCountSlider, onValueChange = {
|
value = appViewModel.senderPacketCountSlider, onValueChange = {
|
||||||
@@ -264,7 +286,19 @@ fun MainView(
|
|||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Become Discoverable", onClick = becomeDiscoverable, true
|
text = "Become Discoverable", onClick = becomeDiscoverable, true
|
||||||
)
|
)
|
||||||
Row() {
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = "2M PHY")
|
||||||
|
Spacer(modifier = Modifier.padding(start = 8.dp))
|
||||||
|
Switch(
|
||||||
|
checked = appViewModel.use2mPhy,
|
||||||
|
onCheckedChange = { appViewModel.use2mPhy = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
Row {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "RFCOMM Client", onClick = runRfcommClient, !appViewModel.running
|
text = "RFCOMM Client", onClick = runRfcommClient, !appViewModel.running
|
||||||
)
|
)
|
||||||
@@ -272,7 +306,7 @@ fun MainView(
|
|||||||
text = "RFCOMM Server", onClick = runRfcommServer, !appViewModel.running
|
text = "RFCOMM Server", onClick = runRfcommServer, !appViewModel.running
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row() {
|
Row {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "L2CAP Client", onClick = runL2capClient, !appViewModel.running
|
text = "L2CAP Client", onClick = runL2capClient, !appViewModel.running
|
||||||
)
|
)
|
||||||
@@ -281,6 +315,12 @@ fun MainView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
|
Text(
|
||||||
|
text = if (appViewModel.mtu != 0) "MTU: ${appViewModel.mtu}" else ""
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (appViewModel.rxPhy != 0 || appViewModel.txPhy != 0) "PHY: tx=${appViewModel.txPhy}, rx=${appViewModel.rxPhy}" else ""
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Packets Sent: ${appViewModel.packetsSent}"
|
text = "Packets Sent: ${appViewModel.packetsSent}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ class AppViewModel : ViewModel() {
|
|||||||
private var preferences: SharedPreferences? = null
|
private var preferences: SharedPreferences? = null
|
||||||
var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS)
|
var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS)
|
||||||
var l2capPsm by mutableStateOf(0)
|
var l2capPsm by mutableStateOf(0)
|
||||||
|
var use2mPhy by mutableStateOf(true)
|
||||||
|
var mtu by mutableStateOf(0)
|
||||||
|
var rxPhy by mutableStateOf(0)
|
||||||
|
var txPhy by mutableStateOf(0)
|
||||||
var senderPacketCountSlider by mutableFloatStateOf(0.0F)
|
var senderPacketCountSlider by mutableFloatStateOf(0.0F)
|
||||||
var senderPacketSizeSlider by mutableFloatStateOf(0.0F)
|
var senderPacketSizeSlider by mutableFloatStateOf(0.0F)
|
||||||
var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT)
|
var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT)
|
||||||
@@ -64,11 +68,12 @@ class AppViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updatePeerBluetoothAddress(peerBluetoothAddress: String) {
|
fun updatePeerBluetoothAddress(peerBluetoothAddress: String) {
|
||||||
this.peerBluetoothAddress = peerBluetoothAddress
|
val address = peerBluetoothAddress.uppercase()
|
||||||
|
this.peerBluetoothAddress = address
|
||||||
|
|
||||||
// Save the address to the preferences
|
// Save the address to the preferences
|
||||||
with(preferences!!.edit()) {
|
with(preferences!!.edit()) {
|
||||||
putString(PEER_BLUETOOTH_ADDRESS_PREF_KEY, peerBluetoothAddress)
|
putString(PEER_BLUETOOTH_ADDRESS_PREF_KEY, address)
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +121,7 @@ class AppViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateSenderPacketSizeSlider() {
|
fun updateSenderPacketSizeSlider() {
|
||||||
if (senderPacketSize <= 1) {
|
if (senderPacketSize <= 16) {
|
||||||
senderPacketSizeSlider = 0.0F
|
senderPacketSizeSlider = 0.0F
|
||||||
} else if (senderPacketSize <= 256) {
|
} else if (senderPacketSize <= 256) {
|
||||||
senderPacketSizeSlider = 0.02F
|
senderPacketSizeSlider = 0.02F
|
||||||
@@ -138,7 +143,7 @@ class AppViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun updateSenderPacketSize() {
|
fun updateSenderPacketSize() {
|
||||||
if (senderPacketSizeSlider < 0.1F) {
|
if (senderPacketSizeSlider < 0.1F) {
|
||||||
senderPacketSize = 1
|
senderPacketSize = 16
|
||||||
} else if (senderPacketSizeSlider < 0.3F) {
|
} else if (senderPacketSizeSlider < 0.3F) {
|
||||||
senderPacketSize = 256
|
senderPacketSize = 256
|
||||||
} else if (senderPacketSizeSlider < 0.5F) {
|
} else if (senderPacketSizeSlider < 0.5F) {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ private val Log = Logger.getLogger("btbench.rfcomm-client")
|
|||||||
class RfcommClient(private val viewModel: AppViewModel, val bluetoothAdapter: BluetoothAdapter) {
|
class RfcommClient(private val viewModel: AppViewModel, val bluetoothAdapter: BluetoothAdapter) {
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun run() {
|
fun run() {
|
||||||
val remoteDevice = bluetoothAdapter.getRemoteDevice(viewModel.peerBluetoothAddress)
|
val address = viewModel.peerBluetoothAddress.take(17)
|
||||||
|
val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
|
||||||
val socket = remoteDevice.createInsecureRfcommSocketToServiceRecord(
|
val socket = remoteDevice.createInsecureRfcommSocketToServiceRecord(
|
||||||
DEFAULT_RFCOMM_UUID
|
DEFAULT_RFCOMM_UUID
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,6 +30,6 @@ class RfcommServer(private val viewModel: AppViewModel, val bluetoothAdapter: Bl
|
|||||||
)
|
)
|
||||||
|
|
||||||
val server = SocketServer(viewModel, serverSocket)
|
val server = SocketServer(viewModel, serverSocket)
|
||||||
server.run({})
|
server.run({}, {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,8 @@ import kotlin.concurrent.thread
|
|||||||
|
|
||||||
private val Log = Logger.getLogger("btbench.socket-client")
|
private val Log = Logger.getLogger("btbench.socket-client")
|
||||||
|
|
||||||
|
private const val DEFAULT_STARTUP_DELAY = 3000
|
||||||
|
|
||||||
class SocketClient(private val viewModel: AppViewModel, private val socket: BluetoothSocket) {
|
class SocketClient(private val viewModel: AppViewModel, private val socket: BluetoothSocket) {
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun run() {
|
fun run() {
|
||||||
@@ -56,6 +58,10 @@ class SocketClient(private val viewModel: AppViewModel, private val socket: Blue
|
|||||||
socketDataSource.receive()
|
socketDataSource.receive()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.info("Startup delay: $DEFAULT_STARTUP_DELAY")
|
||||||
|
Thread.sleep(DEFAULT_STARTUP_DELAY.toLong());
|
||||||
|
Log.info("Starting to send")
|
||||||
|
|
||||||
sender.run()
|
sender.run()
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,13 @@ import kotlin.concurrent.thread
|
|||||||
private val Log = Logger.getLogger("btbench.socket-server")
|
private val Log = Logger.getLogger("btbench.socket-server")
|
||||||
|
|
||||||
class SocketServer(private val viewModel: AppViewModel, private val serverSocket: BluetoothServerSocket) {
|
class SocketServer(private val viewModel: AppViewModel, private val serverSocket: BluetoothServerSocket) {
|
||||||
fun run(onTerminate: () -> Unit) {
|
fun run(onConnected: () -> Unit, onDisconnected: () -> Unit) {
|
||||||
var aborted = false
|
var aborted = false
|
||||||
viewModel.running = true
|
viewModel.running = true
|
||||||
|
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
serverSocket.close()
|
serverSocket.close()
|
||||||
viewModel.running = false
|
viewModel.running = false
|
||||||
onTerminate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
thread(name = "SocketServer") {
|
thread(name = "SocketServer") {
|
||||||
@@ -38,6 +37,7 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
|
|||||||
serverSocket.close()
|
serverSocket.close()
|
||||||
}
|
}
|
||||||
Log.info("waiting for connection...")
|
Log.info("waiting for connection...")
|
||||||
|
onDisconnected()
|
||||||
val socket = try {
|
val socket = try {
|
||||||
serverSocket.accept()
|
serverSocket.accept()
|
||||||
} catch (error: IOException) {
|
} catch (error: IOException) {
|
||||||
@@ -45,7 +45,8 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
|
|||||||
cleanup()
|
cleanup()
|
||||||
return@thread
|
return@thread
|
||||||
}
|
}
|
||||||
Log.info("got connection")
|
Log.info("got connection from ${socket.remoteDevice.address}")
|
||||||
|
onConnected()
|
||||||
|
|
||||||
viewModel.aborter = {
|
viewModel.aborter = {
|
||||||
aborted = true
|
aborted = true
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -71,7 +73,7 @@ class AppViewModel : ViewModel(), HciProxy.Listener {
|
|||||||
this.tcpPort = tcpPort
|
this.tcpPort = tcpPort
|
||||||
|
|
||||||
// Save the port to the preferences
|
// Save the port to the preferences
|
||||||
with (preferences!!.edit()) {
|
with(preferences!!.edit()) {
|
||||||
putString(TCP_PORT_PREF_KEY, tcpPort.toString())
|
putString(TCP_PORT_PREF_KEY, tcpPort.toString())
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
@@ -138,7 +140,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
log.warning("Exception while running HCI Server: $error")
|
log.warning("Exception while running HCI Server: $error")
|
||||||
} catch (error: HalException) {
|
} catch (error: HalException) {
|
||||||
log.warning("HAL exception: ${error.message}")
|
log.warning("HAL exception: ${error.message}")
|
||||||
appViewModel.message = "Cannot bind to HAL (${error.message}). You may need to use the command 'setenforce 0' in a root adb shell."
|
appViewModel.message =
|
||||||
|
"Cannot bind to HAL (${error.message}). You may need to use the command 'setenforce 0' in a root adb shell."
|
||||||
}
|
}
|
||||||
log.info("HCI Proxy thread ended")
|
log.info("HCI Proxy thread ended")
|
||||||
appViewModel.canStart = true
|
appViewModel.canStart = true
|
||||||
@@ -157,9 +160,12 @@ fun ActionButton(text: String, onClick: () -> Unit, enabled: Boolean) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) {
|
fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) {
|
||||||
RemoteHCITheme {
|
RemoteHCITheme {
|
||||||
// A surface container using the 'background' color from the theme
|
val scrollState = rememberScrollState()
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
Text(
|
Text(
|
||||||
@@ -174,13 +180,15 @@ fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) {
|
|||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
TextField(
|
TextField(label = {
|
||||||
label = {
|
|
||||||
Text(text = "TCP Port")
|
Text(text = "TCP Port")
|
||||||
},
|
},
|
||||||
value = appViewModel.tcpPort.toString(),
|
value = appViewModel.tcpPort.toString(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
keyboardType = KeyboardType.Number,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
if (it.isNotEmpty()) {
|
if (it.isNotEmpty()) {
|
||||||
val tcpPort = it.toIntOrNull()
|
val tcpPort = it.toIntOrNull()
|
||||||
@@ -189,10 +197,7 @@ fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }))
|
||||||
onDone = {keyboardController?.hide()}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Divider()
|
Divider()
|
||||||
val connectState = if (appViewModel.hostConnected) "CONNECTED" else "DISCONNECTED"
|
val connectState = if (appViewModel.hostConnected) "CONNECTED" else "DISCONNECTED"
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
8
rust/Cargo.lock
generated
8
rust/Cargo.lock
generated
@@ -1073,9 +1073,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.57"
|
version = "0.10.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -1105,9 +1105,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.92"
|
version = "0.9.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db7e971c2c2bba161b2d2fdf37080177eff520b3bc044787c7f1f5f9e78d869b"
|
checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
|||||||
151
tests/bap_test.py
Normal file
151
tests/bap_test.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# 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
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from bumble import device
|
||||||
|
from bumble.hci import CodecID, CodingFormat
|
||||||
|
from bumble.profiles.bap import (
|
||||||
|
AudioLocation,
|
||||||
|
SupportedFrameDuration,
|
||||||
|
SupportedSamplingFrequency,
|
||||||
|
CodecSpecificCapabilities,
|
||||||
|
ContextType,
|
||||||
|
PacRecord,
|
||||||
|
PublishedAudioCapabilitiesService,
|
||||||
|
PublishedAudioCapabilitiesServiceProxy,
|
||||||
|
)
|
||||||
|
from .test_utils import TwoDevices
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Logging
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_codec_specific_capabilities() -> None:
|
||||||
|
SAMPLE_FREQUENCY = SupportedSamplingFrequency.FREQ_16000
|
||||||
|
FRAME_SURATION = SupportedFrameDuration.DURATION_10000_US_SUPPORTED
|
||||||
|
AUDIO_CHANNEL_COUNTS = [1]
|
||||||
|
cap = CodecSpecificCapabilities(
|
||||||
|
supported_sampling_frequencies=SAMPLE_FREQUENCY,
|
||||||
|
supported_frame_durations=FRAME_SURATION,
|
||||||
|
supported_audio_channel_counts=AUDIO_CHANNEL_COUNTS,
|
||||||
|
min_octets_per_codec_frame=40,
|
||||||
|
max_octets_per_codec_frame=40,
|
||||||
|
supported_max_codec_frames_per_sdu=1,
|
||||||
|
)
|
||||||
|
assert CodecSpecificCapabilities.from_bytes(bytes(cap)) == cap
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_pac_record() -> None:
|
||||||
|
SAMPLE_FREQUENCY = SupportedSamplingFrequency.FREQ_16000
|
||||||
|
FRAME_SURATION = SupportedFrameDuration.DURATION_10000_US_SUPPORTED
|
||||||
|
AUDIO_CHANNEL_COUNTS = [1]
|
||||||
|
cap = CodecSpecificCapabilities(
|
||||||
|
supported_sampling_frequencies=SAMPLE_FREQUENCY,
|
||||||
|
supported_frame_durations=FRAME_SURATION,
|
||||||
|
supported_audio_channel_counts=AUDIO_CHANNEL_COUNTS,
|
||||||
|
min_octets_per_codec_frame=40,
|
||||||
|
max_octets_per_codec_frame=40,
|
||||||
|
supported_max_codec_frames_per_sdu=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
pac_record = PacRecord(
|
||||||
|
coding_format=CodingFormat(CodecID.LC3),
|
||||||
|
codec_specific_capabilities=cap,
|
||||||
|
metadata=b'',
|
||||||
|
)
|
||||||
|
assert PacRecord.from_bytes(bytes(pac_record)) == pac_record
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_vendor_specific_pac_record() -> None:
|
||||||
|
# Vendor-Specific codec, Google, ID=0xFFFF. No capabilities and metadata.
|
||||||
|
RAW_DATA = bytes.fromhex('ffe000ffff0000')
|
||||||
|
assert bytes(PacRecord.from_bytes(RAW_DATA)) == RAW_DATA
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pacs():
|
||||||
|
devices = TwoDevices()
|
||||||
|
devices[0].add_service(
|
||||||
|
PublishedAudioCapabilitiesService(
|
||||||
|
supported_sink_context=ContextType.MEDIA,
|
||||||
|
available_sink_context=ContextType.MEDIA,
|
||||||
|
supported_source_context=0,
|
||||||
|
available_source_context=0,
|
||||||
|
sink_pac=[
|
||||||
|
# Codec Capability Setting 16_2
|
||||||
|
PacRecord(
|
||||||
|
coding_format=CodingFormat(CodecID.LC3),
|
||||||
|
codec_specific_capabilities=CodecSpecificCapabilities(
|
||||||
|
supported_sampling_frequencies=(
|
||||||
|
SupportedSamplingFrequency.FREQ_16000
|
||||||
|
),
|
||||||
|
supported_frame_durations=(
|
||||||
|
SupportedFrameDuration.DURATION_10000_US_SUPPORTED
|
||||||
|
),
|
||||||
|
supported_audio_channel_counts=[1],
|
||||||
|
min_octets_per_codec_frame=40,
|
||||||
|
max_octets_per_codec_frame=40,
|
||||||
|
supported_max_codec_frames_per_sdu=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# Codec Capability Setting 24_2
|
||||||
|
PacRecord(
|
||||||
|
coding_format=CodingFormat(CodecID.LC3),
|
||||||
|
codec_specific_capabilities=CodecSpecificCapabilities(
|
||||||
|
supported_sampling_frequencies=(
|
||||||
|
SupportedSamplingFrequency.FREQ_24000
|
||||||
|
),
|
||||||
|
supported_frame_durations=(
|
||||||
|
SupportedFrameDuration.DURATION_10000_US_SUPPORTED
|
||||||
|
),
|
||||||
|
supported_audio_channel_counts=[1],
|
||||||
|
min_octets_per_codec_frame=60,
|
||||||
|
max_octets_per_codec_frame=60,
|
||||||
|
supported_max_codec_frames_per_sdu=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
sink_audio_locations=AudioLocation.FRONT_LEFT | AudioLocation.FRONT_RIGHT,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await devices.setup_connection()
|
||||||
|
peer = device.Peer(devices.connections[1])
|
||||||
|
pacs_client = await peer.discover_service_and_create_proxy(
|
||||||
|
PublishedAudioCapabilitiesServiceProxy
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def run():
|
||||||
|
await test_pacs()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||||
|
asyncio.run(run())
|
||||||
@@ -32,6 +32,7 @@ from bumble.hci import (
|
|||||||
HCI_CustomPacket,
|
HCI_CustomPacket,
|
||||||
HCI_Disconnect_Command,
|
HCI_Disconnect_Command,
|
||||||
HCI_Event,
|
HCI_Event,
|
||||||
|
HCI_IsoDataPacket,
|
||||||
HCI_LE_Add_Device_To_Filter_Accept_List_Command,
|
HCI_LE_Add_Device_To_Filter_Accept_List_Command,
|
||||||
HCI_LE_Advertising_Report_Event,
|
HCI_LE_Advertising_Report_Event,
|
||||||
HCI_LE_Channel_Selection_Algorithm_Event,
|
HCI_LE_Channel_Selection_Algorithm_Event,
|
||||||
@@ -53,6 +54,7 @@ from bumble.hci import (
|
|||||||
HCI_LE_Set_Random_Address_Command,
|
HCI_LE_Set_Random_Address_Command,
|
||||||
HCI_LE_Set_Scan_Enable_Command,
|
HCI_LE_Set_Scan_Enable_Command,
|
||||||
HCI_LE_Set_Scan_Parameters_Command,
|
HCI_LE_Set_Scan_Parameters_Command,
|
||||||
|
HCI_LE_Setup_ISO_Data_Path_Command,
|
||||||
HCI_Number_Of_Completed_Packets_Event,
|
HCI_Number_Of_Completed_Packets_Event,
|
||||||
HCI_Packet,
|
HCI_Packet,
|
||||||
HCI_PIN_Code_Request_Reply_Command,
|
HCI_PIN_Code_Request_Reply_Command,
|
||||||
@@ -455,6 +457,14 @@ def test_HCI_LE_Setup_ISO_Data_Path_Command():
|
|||||||
assert command.controller_delay == 0
|
assert command.controller_delay == 0
|
||||||
assert command.codec_configuration == b''
|
assert command.codec_configuration == b''
|
||||||
|
|
||||||
|
command = HCI_LE_Setup_ISO_Data_Path_Command(
|
||||||
|
connection_handle=0x0060,
|
||||||
|
data_path_direction=0x00,
|
||||||
|
data_path_id=0x01,
|
||||||
|
codec_id=CodingFormat(CodecID.TRANSPARENT),
|
||||||
|
controller_delay=0x00,
|
||||||
|
codec_configuration=b'',
|
||||||
|
)
|
||||||
basic_check(command)
|
basic_check(command)
|
||||||
|
|
||||||
|
|
||||||
@@ -477,6 +487,29 @@ def test_custom():
|
|||||||
assert packet.payload == data
|
assert packet.payload == data
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_iso_data_packet():
|
||||||
|
data = bytes.fromhex(
|
||||||
|
'05616044002ac9f0a193003c00e83b477b00eba8d41dc018bf1a980f0290afe1e7c37652096697'
|
||||||
|
'52b6a535a8df61e22931ef5a36281bc77ed6a3206d984bcdabee6be831c699cb50e2'
|
||||||
|
)
|
||||||
|
packet = HCI_IsoDataPacket.from_bytes(data)
|
||||||
|
assert packet.connection_handle == 0x0061
|
||||||
|
assert packet.packet_status_flag == 0
|
||||||
|
assert packet.pb_flag == 0x02
|
||||||
|
assert packet.ts_flag == 0x01
|
||||||
|
assert packet.data_total_length == 68
|
||||||
|
assert packet.time_stamp == 2716911914
|
||||||
|
assert packet.packet_sequence_number == 147
|
||||||
|
assert packet.iso_sdu_length == 60
|
||||||
|
assert packet.iso_sdu_fragment == bytes.fromhex(
|
||||||
|
'e83b477b00eba8d41dc018bf1a980f0290afe1e7c3765209669752b6a535a8df61e22931ef5a3'
|
||||||
|
'6281bc77ed6a3206d984bcdabee6be831c699cb50e2'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert packet.to_bytes() == data
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def run_test_events():
|
def run_test_events():
|
||||||
test_HCI_Event()
|
test_HCI_Event()
|
||||||
@@ -515,6 +548,7 @@ def run_test_commands():
|
|||||||
test_HCI_LE_Set_Default_PHY_Command()
|
test_HCI_LE_Set_Default_PHY_Command()
|
||||||
test_HCI_LE_Set_Extended_Scan_Parameters_Command()
|
test_HCI_LE_Set_Extended_Scan_Parameters_Command()
|
||||||
test_HCI_LE_Set_Extended_Advertising_Enable_Command()
|
test_HCI_LE_Set_Extended_Advertising_Enable_Command()
|
||||||
|
test_HCI_LE_Setup_ISO_Data_Path_Command()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -523,3 +557,4 @@ if __name__ == '__main__':
|
|||||||
run_test_commands()
|
run_test_commands()
|
||||||
test_address()
|
test_address()
|
||||||
test_custom()
|
test_custom()
|
||||||
|
test_iso_data_packet()
|
||||||
|
|||||||
Reference in New Issue
Block a user