forked from auracaster/bumble_mirror
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9f91f695a | |||
| b57096abe2 | |||
| 100bea6b41 | |||
| 63819bf9dd | |||
| e3fdab4175 | |||
| bbcd14dbf0 | |||
| 01dc0d574b | |||
| 5e959d638e | |||
| c88b32a406 | |||
| 5a72eefb89 | |||
| 430046944b | |||
| 21d23320eb | |||
| d0990ee04d | |||
| 2d88e853e8 | |||
| a060a70fba | |||
| a06394ad4a | |||
| a1414c2b5b | |||
| b2864dac2d | |||
| b78f895143 | |||
| c4e9726828 | |||
| d4b8e8348a | |||
| 19debaa52e | |||
| 73fe564321 |
+22
-13
@@ -60,7 +60,7 @@ AURACAST_DEFAULT_ATT_MTU = 256
|
|||||||
class BroadcastScanner(pyee.EventEmitter):
|
class BroadcastScanner(pyee.EventEmitter):
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class Broadcast(pyee.EventEmitter):
|
class Broadcast(pyee.EventEmitter):
|
||||||
name: str
|
name: str | None
|
||||||
sync: bumble.device.PeriodicAdvertisingSync
|
sync: bumble.device.PeriodicAdvertisingSync
|
||||||
rssi: int = 0
|
rssi: int = 0
|
||||||
public_broadcast_announcement: Optional[
|
public_broadcast_announcement: Optional[
|
||||||
@@ -135,7 +135,8 @@ class BroadcastScanner(pyee.EventEmitter):
|
|||||||
self.sync.advertiser_address,
|
self.sync.advertiser_address,
|
||||||
color(self.sync.state.name, 'green'),
|
color(self.sync.state.name, 'green'),
|
||||||
)
|
)
|
||||||
print(f' {color("Name", "cyan")}: {self.name}')
|
if self.name is not None:
|
||||||
|
print(f' {color("Name", "cyan")}: {self.name}')
|
||||||
if self.appearance:
|
if self.appearance:
|
||||||
print(f' {color("Appearance", "cyan")}: {str(self.appearance)}')
|
print(f' {color("Appearance", "cyan")}: {str(self.appearance)}')
|
||||||
print(f' {color("RSSI", "cyan")}: {self.rssi}')
|
print(f' {color("RSSI", "cyan")}: {self.rssi}')
|
||||||
@@ -174,7 +175,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|||||||
print(color(' Codec ID:', 'yellow'))
|
print(color(' Codec ID:', 'yellow'))
|
||||||
print(
|
print(
|
||||||
color(' Coding Format: ', 'green'),
|
color(' Coding Format: ', 'green'),
|
||||||
subgroup.codec_id.coding_format.name,
|
subgroup.codec_id.codec_id.name,
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
color(' Company ID: ', 'green'),
|
color(' Company ID: ', 'green'),
|
||||||
@@ -274,13 +275,24 @@ class BroadcastScanner(pyee.EventEmitter):
|
|||||||
await self.device.stop_scanning()
|
await self.device.stop_scanning()
|
||||||
|
|
||||||
def on_advertisement(self, advertisement: bumble.device.Advertisement) -> None:
|
def on_advertisement(self, advertisement: bumble.device.Advertisement) -> None:
|
||||||
if (
|
if not (
|
||||||
broadcast_name := advertisement.data.get(
|
ads := advertisement.data.get_all(
|
||||||
bumble.core.AdvertisingData.BROADCAST_NAME
|
bumble.core.AdvertisingData.SERVICE_DATA_16_BIT_UUID
|
||||||
)
|
)
|
||||||
) is None:
|
) or not (
|
||||||
|
any(
|
||||||
|
ad
|
||||||
|
for ad in ads
|
||||||
|
if isinstance(ad, tuple)
|
||||||
|
and ad[0] == bumble.gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE
|
||||||
|
)
|
||||||
|
):
|
||||||
return
|
return
|
||||||
assert isinstance(broadcast_name, str)
|
|
||||||
|
broadcast_name = advertisement.data.get(
|
||||||
|
bumble.core.AdvertisingData.BROADCAST_NAME
|
||||||
|
)
|
||||||
|
assert isinstance(broadcast_name, str) or broadcast_name is None
|
||||||
|
|
||||||
if broadcast := self.broadcasts.get(advertisement.address):
|
if broadcast := self.broadcasts.get(advertisement.address):
|
||||||
broadcast.update(advertisement)
|
broadcast.update(advertisement)
|
||||||
@@ -291,7 +303,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def on_new_broadcast(
|
async def on_new_broadcast(
|
||||||
self, name: str, advertisement: bumble.device.Advertisement
|
self, name: str | None, advertisement: bumble.device.Advertisement
|
||||||
) -> None:
|
) -> None:
|
||||||
periodic_advertising_sync = await self.device.create_periodic_advertising_sync(
|
periodic_advertising_sync = await self.device.create_periodic_advertising_sync(
|
||||||
advertiser_address=advertisement.address,
|
advertiser_address=advertisement.address,
|
||||||
@@ -299,10 +311,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|||||||
sync_timeout=self.sync_timeout,
|
sync_timeout=self.sync_timeout,
|
||||||
filter_duplicates=self.filter_duplicates,
|
filter_duplicates=self.filter_duplicates,
|
||||||
)
|
)
|
||||||
broadcast = self.Broadcast(
|
broadcast = self.Broadcast(name, periodic_advertising_sync)
|
||||||
name,
|
|
||||||
periodic_advertising_sync,
|
|
||||||
)
|
|
||||||
broadcast.update(advertisement)
|
broadcast.update(advertisement)
|
||||||
self.broadcasts[advertisement.address] = broadcast
|
self.broadcasts[advertisement.address] = broadcast
|
||||||
periodic_advertising_sync.on('loss', lambda: self.on_broadcast_loss(broadcast))
|
periodic_advertising_sync.on('loss', lambda: self.on_broadcast_loss(broadcast))
|
||||||
|
|||||||
+24
-6
@@ -486,7 +486,12 @@ class Speaker:
|
|||||||
|
|
||||||
def on_pdu(pdu: HCI_IsoDataPacket, ase: ascs.AseStateMachine):
|
def on_pdu(pdu: HCI_IsoDataPacket, ase: ascs.AseStateMachine):
|
||||||
codec_config = ase.codec_specific_configuration
|
codec_config = ase.codec_specific_configuration
|
||||||
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
if (
|
||||||
|
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
||||||
|
or codec_config.frame_duration is None
|
||||||
|
or codec_config.audio_channel_allocation is None
|
||||||
|
):
|
||||||
|
return
|
||||||
pcm = decode(
|
pcm = decode(
|
||||||
codec_config.frame_duration.us,
|
codec_config.frame_duration.us,
|
||||||
codec_config.audio_channel_allocation.channel_count,
|
codec_config.audio_channel_allocation.channel_count,
|
||||||
@@ -495,11 +500,17 @@ class Speaker:
|
|||||||
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
||||||
|
|
||||||
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
||||||
|
codec_config = ase.codec_specific_configuration
|
||||||
if ase.state == ascs.AseStateMachine.State.STREAMING:
|
if ase.state == ascs.AseStateMachine.State.STREAMING:
|
||||||
codec_config = ase.codec_specific_configuration
|
|
||||||
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
||||||
assert ase.cis_link
|
|
||||||
if ase.role == ascs.AudioRole.SOURCE:
|
if ase.role == ascs.AudioRole.SOURCE:
|
||||||
|
if (
|
||||||
|
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
||||||
|
or ase.cis_link is None
|
||||||
|
or codec_config.octets_per_codec_frame is None
|
||||||
|
or codec_config.frame_duration is None
|
||||||
|
or codec_config.codec_frames_per_sdu is None
|
||||||
|
):
|
||||||
|
return
|
||||||
ase.cis_link.abort_on(
|
ase.cis_link.abort_on(
|
||||||
'disconnection',
|
'disconnection',
|
||||||
lc3_source_task(
|
lc3_source_task(
|
||||||
@@ -514,10 +525,17 @@ class Speaker:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
if not ase.cis_link:
|
||||||
|
return
|
||||||
ase.cis_link.sink = functools.partial(on_pdu, ase=ase)
|
ase.cis_link.sink = functools.partial(on_pdu, ase=ase)
|
||||||
elif ase.state == ascs.AseStateMachine.State.CODEC_CONFIGURED:
|
elif ase.state == ascs.AseStateMachine.State.CODEC_CONFIGURED:
|
||||||
codec_config = ase.codec_specific_configuration
|
if (
|
||||||
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
||||||
|
or codec_config.sampling_frequency is None
|
||||||
|
or codec_config.frame_duration is None
|
||||||
|
or codec_config.audio_channel_allocation is None
|
||||||
|
):
|
||||||
|
return
|
||||||
if ase.role == ascs.AudioRole.SOURCE:
|
if ase.role == ascs.AudioRole.SOURCE:
|
||||||
setup_encoders(
|
setup_encoders(
|
||||||
codec_config.sampling_frequency.hz,
|
codec_config.sampling_frequency.hz,
|
||||||
|
|||||||
+13
-8
@@ -373,7 +373,9 @@ async def pair(
|
|||||||
shared_data = (
|
shared_data = (
|
||||||
None
|
None
|
||||||
if oob == '-'
|
if oob == '-'
|
||||||
else OobData.from_ad(AdvertisingData.from_bytes(bytes.fromhex(oob)))
|
else OobData.from_ad(
|
||||||
|
AdvertisingData.from_bytes(bytes.fromhex(oob))
|
||||||
|
).shared_data
|
||||||
)
|
)
|
||||||
legacy_context = OobLegacyContext()
|
legacy_context = OobLegacyContext()
|
||||||
oob_contexts = PairingConfig.OobConfig(
|
oob_contexts = PairingConfig.OobConfig(
|
||||||
@@ -381,16 +383,19 @@ async def pair(
|
|||||||
peer_data=shared_data,
|
peer_data=shared_data,
|
||||||
legacy_context=legacy_context,
|
legacy_context=legacy_context,
|
||||||
)
|
)
|
||||||
oob_data = OobData(
|
|
||||||
address=device.random_address,
|
|
||||||
shared_data=shared_data,
|
|
||||||
legacy_context=legacy_context,
|
|
||||||
)
|
|
||||||
print(color('@@@-----------------------------------', 'yellow'))
|
print(color('@@@-----------------------------------', 'yellow'))
|
||||||
print(color('@@@ OOB Data:', 'yellow'))
|
print(color('@@@ OOB Data:', 'yellow'))
|
||||||
print(color(f'@@@ {our_oob_context.share()}', 'yellow'))
|
if shared_data is None:
|
||||||
|
oob_data = OobData(
|
||||||
|
address=device.random_address, shared_data=our_oob_context.share()
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
color(
|
||||||
|
f'@@@ SHARE: {bytes(oob_data.to_ad()).hex()}',
|
||||||
|
'yellow',
|
||||||
|
)
|
||||||
|
)
|
||||||
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
||||||
print(color(f'@@@ HEX: ({bytes(oob_data.to_ad()).hex()})', 'yellow'))
|
|
||||||
print(color('@@@-----------------------------------', 'yellow'))
|
print(color('@@@-----------------------------------', 'yellow'))
|
||||||
else:
|
else:
|
||||||
oob_contexts = None
|
oob_contexts = None
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ if TYPE_CHECKING:
|
|||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
ATT_CID = 0x04
|
ATT_CID = 0x04
|
||||||
|
ATT_PSM = 0x001F
|
||||||
|
|
||||||
ATT_ERROR_RESPONSE = 0x01
|
ATT_ERROR_RESPONSE = 0x01
|
||||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||||
|
|||||||
@@ -1543,6 +1543,41 @@ class Controller:
|
|||||||
}
|
}
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
|
def on_hci_le_set_advertising_set_random_address_command(self, _command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 4, Part E - 7.8.52 LE Set Advertising Set Random Address
|
||||||
|
Command
|
||||||
|
'''
|
||||||
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
|
def on_hci_le_set_extended_advertising_parameters_command(self, _command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 4, Part E - 7.8.53 LE Set Extended Advertising Parameters
|
||||||
|
Command
|
||||||
|
'''
|
||||||
|
return bytes([HCI_SUCCESS, 0])
|
||||||
|
|
||||||
|
def on_hci_le_set_extended_advertising_data_command(self, _command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 4, Part E - 7.8.54 LE Set Extended Advertising Data
|
||||||
|
Command
|
||||||
|
'''
|
||||||
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
|
def on_hci_le_set_extended_scan_response_data_command(self, _command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 4, Part E - 7.8.55 LE Set Extended Scan Response Data
|
||||||
|
Command
|
||||||
|
'''
|
||||||
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
|
def on_hci_le_set_extended_advertising_enable_command(self, _command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 4, Part E - 7.8.56 LE Set Extended Advertising Enable
|
||||||
|
Command
|
||||||
|
'''
|
||||||
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_read_maximum_advertising_data_length_command(self, _command):
|
def on_hci_le_read_maximum_advertising_data_length_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 4, Part E - 7.8.57 LE Read Maximum Advertising Data
|
See Bluetooth spec Vol 4, Part E - 7.8.57 LE Read Maximum Advertising Data
|
||||||
@@ -1557,6 +1592,27 @@ class Controller:
|
|||||||
'''
|
'''
|
||||||
return struct.pack('<BB', HCI_SUCCESS, 0xF0)
|
return struct.pack('<BB', HCI_SUCCESS, 0xF0)
|
||||||
|
|
||||||
|
def on_hci_le_set_periodic_advertising_parameters_command(self, _command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 4, Part E - 7.8.61 LE Set Periodic Advertising Parameters
|
||||||
|
Command
|
||||||
|
'''
|
||||||
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
|
def on_hci_le_set_periodic_advertising_data_command(self, _command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 4, Part E - 7.8.62 LE Set Periodic Advertising Data
|
||||||
|
Command
|
||||||
|
'''
|
||||||
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
|
def on_hci_le_set_periodic_advertising_enable_command(self, _command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 4, Part E - 7.8.63 LE Set Periodic Advertising Enable
|
||||||
|
Command
|
||||||
|
'''
|
||||||
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_read_transmit_power_command(self, _command):
|
def on_hci_le_read_transmit_power_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 4, Part E - 7.8.74 LE Read Transmit Power Command
|
See Bluetooth spec Vol 4, Part E - 7.8.74 LE Read Transmit Power Command
|
||||||
|
|||||||
@@ -1624,6 +1624,9 @@ class AdvertisingData:
|
|||||||
[bytes([len(x[1]) + 1, x[0]]) + x[1] for x in self.ad_structures]
|
[bytes([len(x[1]) + 1, x[0]]) + x[1] for x in self.ad_structures]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_bytes(self) -> bytes:
|
||||||
|
return bytes(self)
|
||||||
|
|
||||||
def to_string(self, separator=', '):
|
def to_string(self, separator=', '):
|
||||||
return separator.join(
|
return separator.join(
|
||||||
[AdvertisingData.ad_data_to_string(x[0], x[1]) for x in self.ad_structures]
|
[AdvertisingData.ad_data_to_string(x[0], x[1]) for x in self.ad_structures]
|
||||||
|
|||||||
+455
-495
File diff suppressed because it is too large
Load Diff
@@ -898,6 +898,12 @@ class Client:
|
|||||||
) and subscriber in subscribers:
|
) and subscriber in subscribers:
|
||||||
subscribers.remove(subscriber)
|
subscribers.remove(subscriber)
|
||||||
|
|
||||||
|
# The characteristic itself is added as subscriber. If it is the
|
||||||
|
# last remaining subscriber, we remove it, such that the clean up
|
||||||
|
# works correctly. Otherwise the CCCD never is set back to 0.
|
||||||
|
if len(subscribers) == 1 and characteristic in subscribers:
|
||||||
|
subscribers.remove(characteristic)
|
||||||
|
|
||||||
# Cleanup if we removed the last one
|
# Cleanup if we removed the last one
|
||||||
if not subscribers:
|
if not subscribers:
|
||||||
del subscriber_set[characteristic.handle]
|
del subscriber_set[characteristic.handle]
|
||||||
|
|||||||
+85
-1
@@ -915,6 +915,8 @@ HCI_SUPPORTED_COMMANDS_MASKS = {
|
|||||||
HCI_READ_CURRENT_IAC_LAP_COMMAND : 1 << (11*8+3),
|
HCI_READ_CURRENT_IAC_LAP_COMMAND : 1 << (11*8+3),
|
||||||
HCI_WRITE_CURRENT_IAC_LAP_COMMAND : 1 << (11*8+4),
|
HCI_WRITE_CURRENT_IAC_LAP_COMMAND : 1 << (11*8+4),
|
||||||
HCI_SET_AFH_HOST_CHANNEL_CLASSIFICATION_COMMAND : 1 << (12*8+1),
|
HCI_SET_AFH_HOST_CHANNEL_CLASSIFICATION_COMMAND : 1 << (12*8+1),
|
||||||
|
HCI_LE_CS_READ_REMOTE_FAE_TABLE_COMMAND : 1 << (12*8+2),
|
||||||
|
HCI_LE_CS_WRITE_CACHED_REMOTE_FAE_TABLE_COMMAND : 1 << (12*8+3),
|
||||||
HCI_READ_INQUIRY_SCAN_TYPE_COMMAND : 1 << (12*8+4),
|
HCI_READ_INQUIRY_SCAN_TYPE_COMMAND : 1 << (12*8+4),
|
||||||
HCI_WRITE_INQUIRY_SCAN_TYPE_COMMAND : 1 << (12*8+5),
|
HCI_WRITE_INQUIRY_SCAN_TYPE_COMMAND : 1 << (12*8+5),
|
||||||
HCI_READ_INQUIRY_MODE_COMMAND : 1 << (12*8+6),
|
HCI_READ_INQUIRY_MODE_COMMAND : 1 << (12*8+6),
|
||||||
@@ -940,6 +942,8 @@ HCI_SUPPORTED_COMMANDS_MASKS = {
|
|||||||
HCI_SETUP_SYNCHRONOUS_CONNECTION_COMMAND : 1 << (16*8+3),
|
HCI_SETUP_SYNCHRONOUS_CONNECTION_COMMAND : 1 << (16*8+3),
|
||||||
HCI_ACCEPT_SYNCHRONOUS_CONNECTION_REQUEST_COMMAND : 1 << (16*8+4),
|
HCI_ACCEPT_SYNCHRONOUS_CONNECTION_REQUEST_COMMAND : 1 << (16*8+4),
|
||||||
HCI_REJECT_SYNCHRONOUS_CONNECTION_REQUEST_COMMAND : 1 << (16*8+5),
|
HCI_REJECT_SYNCHRONOUS_CONNECTION_REQUEST_COMMAND : 1 << (16*8+5),
|
||||||
|
HCI_LE_CS_CREATE_CONFIG_COMMAND : 1 << (16*8+6),
|
||||||
|
HCI_LE_CS_REMOVE_CONFIG_COMMAND : 1 << (16*8+7),
|
||||||
HCI_READ_EXTENDED_INQUIRY_RESPONSE_COMMAND : 1 << (17*8+0),
|
HCI_READ_EXTENDED_INQUIRY_RESPONSE_COMMAND : 1 << (17*8+0),
|
||||||
HCI_WRITE_EXTENDED_INQUIRY_RESPONSE_COMMAND : 1 << (17*8+1),
|
HCI_WRITE_EXTENDED_INQUIRY_RESPONSE_COMMAND : 1 << (17*8+1),
|
||||||
HCI_REFRESH_ENCRYPTION_KEY_COMMAND : 1 << (17*8+2),
|
HCI_REFRESH_ENCRYPTION_KEY_COMMAND : 1 << (17*8+2),
|
||||||
@@ -963,13 +967,20 @@ HCI_SUPPORTED_COMMANDS_MASKS = {
|
|||||||
HCI_SEND_KEYPRESS_NOTIFICATION_COMMAND : 1 << (20*8+2),
|
HCI_SEND_KEYPRESS_NOTIFICATION_COMMAND : 1 << (20*8+2),
|
||||||
HCI_IO_CAPABILITY_REQUEST_NEGATIVE_REPLY_COMMAND : 1 << (20*8+3),
|
HCI_IO_CAPABILITY_REQUEST_NEGATIVE_REPLY_COMMAND : 1 << (20*8+3),
|
||||||
HCI_READ_ENCRYPTION_KEY_SIZE_COMMAND : 1 << (20*8+4),
|
HCI_READ_ENCRYPTION_KEY_SIZE_COMMAND : 1 << (20*8+4),
|
||||||
|
HCI_LE_CS_READ_LOCAL_SUPPORTED_CAPABILITIES_COMMAND : 1 << (20*8+5),
|
||||||
|
HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMMAND : 1 << (20*8+6),
|
||||||
|
HCI_LE_CS_WRITE_CACHED_REMOTE_SUPPORTED_CAPABILITIES : 1 << (20*8+7),
|
||||||
HCI_SET_EVENT_MASK_PAGE_2_COMMAND : 1 << (22*8+2),
|
HCI_SET_EVENT_MASK_PAGE_2_COMMAND : 1 << (22*8+2),
|
||||||
HCI_READ_FLOW_CONTROL_MODE_COMMAND : 1 << (23*8+0),
|
HCI_READ_FLOW_CONTROL_MODE_COMMAND : 1 << (23*8+0),
|
||||||
HCI_WRITE_FLOW_CONTROL_MODE_COMMAND : 1 << (23*8+1),
|
HCI_WRITE_FLOW_CONTROL_MODE_COMMAND : 1 << (23*8+1),
|
||||||
HCI_READ_DATA_BLOCK_SIZE_COMMAND : 1 << (23*8+2),
|
HCI_READ_DATA_BLOCK_SIZE_COMMAND : 1 << (23*8+2),
|
||||||
|
HCI_LE_CS_TEST_COMMAND : 1 << (23*8+3),
|
||||||
|
HCI_LE_CS_TEST_END_COMMAND : 1 << (23*8+4),
|
||||||
HCI_READ_ENHANCED_TRANSMIT_POWER_LEVEL_COMMAND : 1 << (24*8+0),
|
HCI_READ_ENHANCED_TRANSMIT_POWER_LEVEL_COMMAND : 1 << (24*8+0),
|
||||||
|
HCI_LE_CS_SECURITY_ENABLE_COMMAND : 1 << (24*8+1),
|
||||||
HCI_READ_LE_HOST_SUPPORT_COMMAND : 1 << (24*8+5),
|
HCI_READ_LE_HOST_SUPPORT_COMMAND : 1 << (24*8+5),
|
||||||
HCI_WRITE_LE_HOST_SUPPORT_COMMAND : 1 << (24*8+6),
|
HCI_WRITE_LE_HOST_SUPPORT_COMMAND : 1 << (24*8+6),
|
||||||
|
HCI_LE_CS_SET_DEFAULT_SETTINGS_COMMAND : 1 << (24*8+7),
|
||||||
HCI_LE_SET_EVENT_MASK_COMMAND : 1 << (25*8+0),
|
HCI_LE_SET_EVENT_MASK_COMMAND : 1 << (25*8+0),
|
||||||
HCI_LE_READ_BUFFER_SIZE_COMMAND : 1 << (25*8+1),
|
HCI_LE_READ_BUFFER_SIZE_COMMAND : 1 << (25*8+1),
|
||||||
HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND : 1 << (25*8+2),
|
HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND : 1 << (25*8+2),
|
||||||
@@ -1000,6 +1011,10 @@ HCI_SUPPORTED_COMMANDS_MASKS = {
|
|||||||
HCI_LE_RECEIVER_TEST_COMMAND : 1 << (28*8+4),
|
HCI_LE_RECEIVER_TEST_COMMAND : 1 << (28*8+4),
|
||||||
HCI_LE_TRANSMITTER_TEST_COMMAND : 1 << (28*8+5),
|
HCI_LE_TRANSMITTER_TEST_COMMAND : 1 << (28*8+5),
|
||||||
HCI_LE_TEST_END_COMMAND : 1 << (28*8+6),
|
HCI_LE_TEST_END_COMMAND : 1 << (28*8+6),
|
||||||
|
HCI_LE_ENABLE_MONITORING_ADVERTISERS_COMMAND : 1 << (28*8+7),
|
||||||
|
HCI_LE_CS_SET_CHANNEL_CLASSIFICATION_COMMAND : 1 << (29*8+0),
|
||||||
|
HCI_LE_CS_SET_PROCEDURE_PARAMETERS_COMMAND : 1 << (29*8+1),
|
||||||
|
HCI_LE_CS_PROCEDURE_ENABLE_COMMAND : 1 << (29*8+2),
|
||||||
HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND : 1 << (29*8+3),
|
HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND : 1 << (29*8+3),
|
||||||
HCI_ENHANCED_ACCEPT_SYNCHRONOUS_CONNECTION_REQUEST_COMMAND : 1 << (29*8+4),
|
HCI_ENHANCED_ACCEPT_SYNCHRONOUS_CONNECTION_REQUEST_COMMAND : 1 << (29*8+4),
|
||||||
HCI_READ_LOCAL_SUPPORTED_CODECS_COMMAND : 1 << (29*8+5),
|
HCI_READ_LOCAL_SUPPORTED_CODECS_COMMAND : 1 << (29*8+5),
|
||||||
@@ -1136,11 +1151,21 @@ HCI_SUPPORTED_COMMANDS_MASKS = {
|
|||||||
HCI_LE_SET_DEFAULT_SUBRATE_COMMAND : 1 << (46*8+0),
|
HCI_LE_SET_DEFAULT_SUBRATE_COMMAND : 1 << (46*8+0),
|
||||||
HCI_LE_SUBRATE_REQUEST_COMMAND : 1 << (46*8+1),
|
HCI_LE_SUBRATE_REQUEST_COMMAND : 1 << (46*8+1),
|
||||||
HCI_LE_SET_EXTENDED_ADVERTISING_PARAMETERS_V2_COMMAND : 1 << (46*8+2),
|
HCI_LE_SET_EXTENDED_ADVERTISING_PARAMETERS_V2_COMMAND : 1 << (46*8+2),
|
||||||
|
HCI_LE_SET_DECISION_DATA_COMMAND : 1 << (46*8+3),
|
||||||
|
HCI_LE_SET_DECISION_INSTRUCTIONS_COMMAND : 1 << (46*8+4),
|
||||||
HCI_LE_SET_PERIODIC_ADVERTISING_SUBEVENT_DATA_COMMAND : 1 << (46*8+5),
|
HCI_LE_SET_PERIODIC_ADVERTISING_SUBEVENT_DATA_COMMAND : 1 << (46*8+5),
|
||||||
HCI_LE_SET_PERIODIC_ADVERTISING_RESPONSE_DATA_COMMAND : 1 << (46*8+6),
|
HCI_LE_SET_PERIODIC_ADVERTISING_RESPONSE_DATA_COMMAND : 1 << (46*8+6),
|
||||||
HCI_LE_SET_PERIODIC_SYNC_SUBEVENT_COMMAND : 1 << (46*8+7),
|
HCI_LE_SET_PERIODIC_SYNC_SUBEVENT_COMMAND : 1 << (46*8+7),
|
||||||
HCI_LE_EXTENDED_CREATE_CONNECTION_V2_COMMAND : 1 << (47*8+0),
|
HCI_LE_EXTENDED_CREATE_CONNECTION_V2_COMMAND : 1 << (47*8+0),
|
||||||
HCI_LE_SET_PERIODIC_ADVERTISING_PARAMETERS_V2_COMMAND : 1 << (47*8+1),
|
HCI_LE_SET_PERIODIC_ADVERTISING_PARAMETERS_V2_COMMAND : 1 << (47*8+1),
|
||||||
|
HCI_LE_READ_ALL_LOCAL_SUPPORTED_FEATURES_COMMAND : 1 << (47*8+2),
|
||||||
|
HCI_LE_READ_ALL_REMOTE_FEATURES_COMMAND : 1 << (47*8+3),
|
||||||
|
HCI_LE_SET_HOST_FEATURE_V2_COMMAND : 1 << (47*8+4),
|
||||||
|
HCI_LE_ADD_DEVICE_TO_MONITORED_ADVERTISERS_LIST_COMMAND : 1 << (47*8+5),
|
||||||
|
HCI_LE_REMOVE_DEVICE_FROM_MONITORED_ADVERTISERS_LIST_COMMAND : 1 << (47*8+6),
|
||||||
|
HCI_LE_CLEAR_MONITORED_ADVERTISERS_LIST_COMMAND : 1 << (47*8+7),
|
||||||
|
HCI_LE_READ_MONITORED_ADVERTISERS_LIST_SIZE_COMMAND : 1 << (48*8+0),
|
||||||
|
HCI_LE_FRAME_SPACE_UPDATE_COMMAND : 1 << (48*8+1),
|
||||||
}
|
}
|
||||||
|
|
||||||
# LE Supported Features
|
# LE Supported Features
|
||||||
@@ -1457,7 +1482,7 @@ class CodingFormat:
|
|||||||
vendor_specific_codec_id: int = 0
|
vendor_specific_codec_id: int = 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_from_bytes(cls, data: bytes, offset: int):
|
def parse_from_bytes(cls, data: bytes, offset: int) -> tuple[int, CodingFormat]:
|
||||||
(codec_id, company_id, vendor_specific_codec_id) = struct.unpack_from(
|
(codec_id, company_id, vendor_specific_codec_id) = struct.unpack_from(
|
||||||
'<BHH', data, offset
|
'<BHH', data, offset
|
||||||
)
|
)
|
||||||
@@ -1467,6 +1492,10 @@ class CodingFormat:
|
|||||||
vendor_specific_codec_id=vendor_specific_codec_id,
|
vendor_specific_codec_id=vendor_specific_codec_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> CodingFormat:
|
||||||
|
return cls.parse_from_bytes(data, 0)[1]
|
||||||
|
|
||||||
def to_bytes(self) -> bytes:
|
def to_bytes(self) -> bytes:
|
||||||
return struct.pack(
|
return struct.pack(
|
||||||
'<BHH', self.codec_id, self.company_id, self.vendor_specific_codec_id
|
'<BHH', self.codec_id, self.company_id, self.vendor_specific_codec_id
|
||||||
@@ -4302,6 +4331,61 @@ class HCI_LE_Clear_Advertising_Sets_Command(HCI_Command):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command(
|
||||||
|
[
|
||||||
|
('advertising_handle', 1),
|
||||||
|
('periodic_advertising_interval_min', 2),
|
||||||
|
('periodic_advertising_interval_max', 2),
|
||||||
|
('periodic_advertising_properties', 2),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class HCI_LE_Set_Periodic_Advertising_Parameters_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.61 LE Set Periodic Advertising Parameters command
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Properties(enum.IntFlag):
|
||||||
|
INCLUDE_TX_POWER = 1 << 6
|
||||||
|
|
||||||
|
advertising_handle: int
|
||||||
|
periodic_advertising_interval_min: int
|
||||||
|
periodic_advertising_interval_max: int
|
||||||
|
periodic_advertising_properties: int
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command(
|
||||||
|
[
|
||||||
|
('advertising_handle', 1),
|
||||||
|
(
|
||||||
|
'operation',
|
||||||
|
{
|
||||||
|
'size': 1,
|
||||||
|
'mapper': lambda x: HCI_LE_Set_Extended_Advertising_Data_Command.Operation(
|
||||||
|
x
|
||||||
|
).name,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'advertising_data',
|
||||||
|
{
|
||||||
|
'parser': HCI_Object.parse_length_prefixed_bytes,
|
||||||
|
'serializer': HCI_Object.serialize_length_prefixed_bytes,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class HCI_LE_Set_Periodic_Advertising_Data_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.62 LE Set Periodic Advertising Data command
|
||||||
|
'''
|
||||||
|
|
||||||
|
advertising_handle: int
|
||||||
|
operation: int
|
||||||
|
advertising_data: bytes
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command([('enable', 1), ('advertising_handle', 1)])
|
@HCI_Command.command([('enable', 1), ('advertising_handle', 1)])
|
||||||
class HCI_LE_Set_Periodic_Advertising_Enable_Command(HCI_Command):
|
class HCI_LE_Set_Periodic_Advertising_Enable_Command(HCI_Command):
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ from bumble.device import (
|
|||||||
AdvertisingEventProperties,
|
AdvertisingEventProperties,
|
||||||
AdvertisingType,
|
AdvertisingType,
|
||||||
Device,
|
Device,
|
||||||
Phy,
|
|
||||||
)
|
)
|
||||||
from bumble.gatt import Service
|
from bumble.gatt import Service
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
@@ -47,6 +46,7 @@ from bumble.hci import (
|
|||||||
HCI_PAGE_TIMEOUT_ERROR,
|
HCI_PAGE_TIMEOUT_ERROR,
|
||||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||||
Address,
|
Address,
|
||||||
|
Phy,
|
||||||
)
|
)
|
||||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class AudioInputStatus(OpenIntEnum):
|
|||||||
Cf. 3.4 Audio Input Status
|
Cf. 3.4 Audio Input Status
|
||||||
'''
|
'''
|
||||||
|
|
||||||
INATIVE = 0x00
|
INACTIVE = 0x00
|
||||||
ACTIVE = 0x01
|
ACTIVE = 0x01
|
||||||
|
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ class AudioInputControlPointOpCode(OpenIntEnum):
|
|||||||
Cf. 3.5.1 Audio Input Control Point procedure requirements
|
Cf. 3.5.1 Audio Input Control Point procedure requirements
|
||||||
'''
|
'''
|
||||||
|
|
||||||
SET_GAIN_SETTING = 0x00
|
SET_GAIN_SETTING = 0x01
|
||||||
UNMUTE = 0x02
|
UNMUTE = 0x02
|
||||||
MUTE = 0x03
|
MUTE = 0x03
|
||||||
SET_MANUAL_GAIN_MODE = 0x04
|
SET_MANUAL_GAIN_MODE = 0x04
|
||||||
@@ -239,7 +239,7 @@ class AudioInputControlPoint:
|
|||||||
or gain_settings_operand
|
or gain_settings_operand
|
||||||
> self.gain_settings_properties.gain_settings_maximum
|
> self.gain_settings_properties.gain_settings_maximum
|
||||||
):
|
):
|
||||||
logger.error("gain_seetings value out of range")
|
logger.error("gain_settings value out of range")
|
||||||
raise ATT_Error(ErrorCode.VALUE_OUT_OF_RANGE)
|
raise ATT_Error(ErrorCode.VALUE_OUT_OF_RANGE)
|
||||||
|
|
||||||
if self.audio_input_state.gain_settings != gain_settings_operand:
|
if self.audio_input_state.gain_settings != gain_settings_operand:
|
||||||
|
|||||||
+121
-40
@@ -102,6 +102,7 @@ class ContextType(enum.IntFlag):
|
|||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
PROHIBITED = 0x0000
|
PROHIBITED = 0x0000
|
||||||
|
UNSPECIFIED = 0x0001
|
||||||
CONVERSATIONAL = 0x0002
|
CONVERSATIONAL = 0x0002
|
||||||
MEDIA = 0x0004
|
MEDIA = 0x0004
|
||||||
GAME = 0x0008
|
GAME = 0x0008
|
||||||
@@ -397,18 +398,21 @@ class CodecSpecificConfiguration:
|
|||||||
OCTETS_PER_FRAME = 0x04
|
OCTETS_PER_FRAME = 0x04
|
||||||
CODEC_FRAMES_PER_SDU = 0x05
|
CODEC_FRAMES_PER_SDU = 0x05
|
||||||
|
|
||||||
sampling_frequency: SamplingFrequency
|
sampling_frequency: SamplingFrequency | None = None
|
||||||
frame_duration: FrameDuration
|
frame_duration: FrameDuration | None = None
|
||||||
audio_channel_allocation: AudioLocation
|
audio_channel_allocation: AudioLocation | None = None
|
||||||
octets_per_codec_frame: int
|
octets_per_codec_frame: int | None = None
|
||||||
codec_frames_per_sdu: int
|
codec_frames_per_sdu: int | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, data: bytes) -> CodecSpecificConfiguration:
|
def from_bytes(cls, data: bytes) -> CodecSpecificConfiguration:
|
||||||
offset = 0
|
offset = 0
|
||||||
# Allowed default values.
|
sampling_frequency: SamplingFrequency | None = None
|
||||||
audio_channel_allocation = AudioLocation.NOT_ALLOWED
|
frame_duration: FrameDuration | None = None
|
||||||
codec_frames_per_sdu = 1
|
audio_channel_allocation: AudioLocation | None = None
|
||||||
|
octets_per_codec_frame: int | None = None
|
||||||
|
codec_frames_per_sdu: int | None = None
|
||||||
|
|
||||||
while offset < len(data):
|
while offset < len(data):
|
||||||
length, type = struct.unpack_from('BB', data, offset)
|
length, type = struct.unpack_from('BB', data, offset)
|
||||||
offset += 2
|
offset += 2
|
||||||
@@ -426,8 +430,6 @@ class CodecSpecificConfiguration:
|
|||||||
elif type == CodecSpecificConfiguration.Type.CODEC_FRAMES_PER_SDU:
|
elif type == CodecSpecificConfiguration.Type.CODEC_FRAMES_PER_SDU:
|
||||||
codec_frames_per_sdu = value
|
codec_frames_per_sdu = value
|
||||||
|
|
||||||
# It is expected here that if some fields are missing, an error should be raised.
|
|
||||||
# pylint: disable=possibly-used-before-assignment,used-before-assignment
|
|
||||||
return CodecSpecificConfiguration(
|
return CodecSpecificConfiguration(
|
||||||
sampling_frequency=sampling_frequency,
|
sampling_frequency=sampling_frequency,
|
||||||
frame_duration=frame_duration,
|
frame_duration=frame_duration,
|
||||||
@@ -437,23 +439,43 @@ class CodecSpecificConfiguration:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __bytes__(self) -> bytes:
|
def __bytes__(self) -> bytes:
|
||||||
return struct.pack(
|
return b''.join(
|
||||||
'<BBBBBBBBIBBHBBB',
|
[
|
||||||
2,
|
struct.pack(fmt, length, tag, value)
|
||||||
CodecSpecificConfiguration.Type.SAMPLING_FREQUENCY,
|
for fmt, length, tag, value in [
|
||||||
self.sampling_frequency,
|
(
|
||||||
2,
|
'<BBB',
|
||||||
CodecSpecificConfiguration.Type.FRAME_DURATION,
|
2,
|
||||||
self.frame_duration,
|
CodecSpecificConfiguration.Type.SAMPLING_FREQUENCY,
|
||||||
5,
|
self.sampling_frequency,
|
||||||
CodecSpecificConfiguration.Type.AUDIO_CHANNEL_ALLOCATION,
|
),
|
||||||
self.audio_channel_allocation,
|
(
|
||||||
3,
|
'<BBB',
|
||||||
CodecSpecificConfiguration.Type.OCTETS_PER_FRAME,
|
2,
|
||||||
self.octets_per_codec_frame,
|
CodecSpecificConfiguration.Type.FRAME_DURATION,
|
||||||
2,
|
self.frame_duration,
|
||||||
CodecSpecificConfiguration.Type.CODEC_FRAMES_PER_SDU,
|
),
|
||||||
self.codec_frames_per_sdu,
|
(
|
||||||
|
'<BBI',
|
||||||
|
5,
|
||||||
|
CodecSpecificConfiguration.Type.AUDIO_CHANNEL_ALLOCATION,
|
||||||
|
self.audio_channel_allocation,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'<BBH',
|
||||||
|
3,
|
||||||
|
CodecSpecificConfiguration.Type.OCTETS_PER_FRAME,
|
||||||
|
self.octets_per_codec_frame,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'<BBB',
|
||||||
|
2,
|
||||||
|
CodecSpecificConfiguration.Type.CODEC_FRAMES_PER_SDU,
|
||||||
|
self.codec_frames_per_sdu,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
if value is not None
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -465,6 +487,25 @@ class BroadcastAudioAnnouncement:
|
|||||||
def from_bytes(cls, data: bytes) -> Self:
|
def from_bytes(cls, data: bytes) -> Self:
|
||||||
return cls(int.from_bytes(data[:3], 'little'))
|
return cls(int.from_bytes(data[:3], 'little'))
|
||||||
|
|
||||||
|
def to_bytes(self) -> bytes:
|
||||||
|
return self.broadcast_id.to_bytes(3, 'little')
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return self.to_bytes()
|
||||||
|
|
||||||
|
def get_advertising_data(self) -> bytes:
|
||||||
|
return core.AdvertisingData(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||||
|
(
|
||||||
|
gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE.to_bytes()
|
||||||
|
+ self.to_bytes()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
).to_bytes()
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class BasicAudioAnnouncement:
|
class BasicAudioAnnouncement:
|
||||||
@@ -473,26 +514,43 @@ class BasicAudioAnnouncement:
|
|||||||
index: int
|
index: int
|
||||||
codec_specific_configuration: CodecSpecificConfiguration
|
codec_specific_configuration: CodecSpecificConfiguration
|
||||||
|
|
||||||
@dataclasses.dataclass
|
def to_bytes(self) -> bytes:
|
||||||
class CodecInfo:
|
codec_specific_configuration_bytes = bytes(
|
||||||
coding_format: hci.CodecID
|
self.codec_specific_configuration
|
||||||
company_id: int
|
)
|
||||||
vendor_specific_codec_id: int
|
return (
|
||||||
|
bytes([self.index, len(codec_specific_configuration_bytes)])
|
||||||
|
+ codec_specific_configuration_bytes
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
def __bytes__(self) -> bytes:
|
||||||
def from_bytes(cls, data: bytes) -> Self:
|
return self.to_bytes()
|
||||||
coding_format = hci.CodecID(data[0])
|
|
||||||
company_id = int.from_bytes(data[1:3], 'little')
|
|
||||||
vendor_specific_codec_id = int.from_bytes(data[3:5], 'little')
|
|
||||||
return cls(coding_format, company_id, vendor_specific_codec_id)
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class Subgroup:
|
class Subgroup:
|
||||||
codec_id: BasicAudioAnnouncement.CodecInfo
|
codec_id: hci.CodingFormat
|
||||||
codec_specific_configuration: CodecSpecificConfiguration
|
codec_specific_configuration: CodecSpecificConfiguration
|
||||||
metadata: le_audio.Metadata
|
metadata: le_audio.Metadata
|
||||||
bis: List[BasicAudioAnnouncement.BIS]
|
bis: List[BasicAudioAnnouncement.BIS]
|
||||||
|
|
||||||
|
def to_bytes(self) -> bytes:
|
||||||
|
metadata_bytes = bytes(self.metadata)
|
||||||
|
codec_specific_configuration_bytes = bytes(
|
||||||
|
self.codec_specific_configuration
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
bytes([len(self.bis)])
|
||||||
|
+ self.codec_id.to_bytes()
|
||||||
|
+ bytes([len(codec_specific_configuration_bytes)])
|
||||||
|
+ codec_specific_configuration_bytes
|
||||||
|
+ bytes([len(metadata_bytes)])
|
||||||
|
+ metadata_bytes
|
||||||
|
+ b''.join(map(bytes, self.bis))
|
||||||
|
)
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return self.to_bytes()
|
||||||
|
|
||||||
presentation_delay: int
|
presentation_delay: int
|
||||||
subgroups: List[BasicAudioAnnouncement.Subgroup]
|
subgroups: List[BasicAudioAnnouncement.Subgroup]
|
||||||
|
|
||||||
@@ -504,7 +562,7 @@ class BasicAudioAnnouncement:
|
|||||||
for _ in range(data[3]):
|
for _ in range(data[3]):
|
||||||
num_bis = data[offset]
|
num_bis = data[offset]
|
||||||
offset += 1
|
offset += 1
|
||||||
codec_id = cls.CodecInfo.from_bytes(data[offset : offset + 5])
|
codec_id = hci.CodingFormat.from_bytes(data[offset : offset + 5])
|
||||||
offset += 5
|
offset += 5
|
||||||
codec_specific_configuration_length = data[offset]
|
codec_specific_configuration_length = data[offset]
|
||||||
offset += 1
|
offset += 1
|
||||||
@@ -548,3 +606,26 @@ class BasicAudioAnnouncement:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return cls(presentation_delay, subgroups)
|
return cls(presentation_delay, subgroups)
|
||||||
|
|
||||||
|
def to_bytes(self) -> bytes:
|
||||||
|
return (
|
||||||
|
self.presentation_delay.to_bytes(3, 'little')
|
||||||
|
+ bytes([len(self.subgroups)])
|
||||||
|
+ b''.join(map(bytes, self.subgroups))
|
||||||
|
)
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return self.to_bytes()
|
||||||
|
|
||||||
|
def get_advertising_data(self) -> bytes:
|
||||||
|
return core.AdvertisingData(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||||
|
(
|
||||||
|
gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE.to_bytes()
|
||||||
|
+ self.to_bytes()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
).to_bytes()
|
||||||
|
|||||||
+1
-1
@@ -1839,7 +1839,7 @@ class Session:
|
|||||||
if self.is_initiator:
|
if self.is_initiator:
|
||||||
if self.pairing_method == PairingMethod.OOB:
|
if self.pairing_method == PairingMethod.OOB:
|
||||||
self.send_pairing_random_command()
|
self.send_pairing_random_command()
|
||||||
else:
|
elif self.pairing_method == PairingMethod.PASSKEY:
|
||||||
self.send_pairing_confirm_command()
|
self.send_pairing_confirm_command()
|
||||||
else:
|
else:
|
||||||
if self.pairing_method == PairingMethod.PASSKEY:
|
if self.pairing_method == PairingMethod.PASSKEY:
|
||||||
|
|||||||
@@ -370,11 +370,13 @@ class PumpedPacketSource(ParserSource):
|
|||||||
self.parser.feed_data(packet)
|
self.parser.feed_data(packet)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.debug('source pump task done')
|
logger.debug('source pump task done')
|
||||||
self.terminated.set_result(None)
|
if not self.terminated.done():
|
||||||
|
self.terminated.set_result(None)
|
||||||
break
|
break
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warning(f'exception while waiting for packet: {error}')
|
logger.warning(f'exception while waiting for packet: {error}')
|
||||||
self.terminated.set_exception(error)
|
if not self.terminated.done():
|
||||||
|
self.terminated.set_exception(error)
|
||||||
break
|
break
|
||||||
|
|
||||||
self.pump_task = asyncio.create_task(pump_packets())
|
self.pump_task = asyncio.create_task(pump_packets())
|
||||||
|
|||||||
@@ -161,7 +161,13 @@ async def main() -> None:
|
|||||||
else:
|
else:
|
||||||
file_output = open(f'{datetime.datetime.now().isoformat()}.lc3', 'wb')
|
file_output = open(f'{datetime.datetime.now().isoformat()}.lc3', 'wb')
|
||||||
codec_configuration = ase.codec_specific_configuration
|
codec_configuration = ase.codec_specific_configuration
|
||||||
assert isinstance(codec_configuration, CodecSpecificConfiguration)
|
if (
|
||||||
|
not isinstance(codec_configuration, CodecSpecificConfiguration)
|
||||||
|
or codec_configuration.sampling_frequency is None
|
||||||
|
or codec_configuration.audio_channel_allocation is None
|
||||||
|
or codec_configuration.frame_duration is None
|
||||||
|
):
|
||||||
|
return
|
||||||
# Write a LC3 header.
|
# Write a LC3 header.
|
||||||
file_output.write(
|
file_output.write(
|
||||||
bytes([0x1C, 0xCC]) # Header.
|
bytes([0x1C, 0xCC]) # Header.
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ impl Address {
|
|||||||
/// Creates a new [Address] object.
|
/// Creates a new [Address] object.
|
||||||
pub fn new(address: &str, address_type: AddressType) -> PyResult<Self> {
|
pub fn new(address: &str, address_type: AddressType) -> PyResult<Self> {
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
PyModule::import(py, intern!(py, "bumble.device"))?
|
PyModule::import(py, intern!(py, "bumble.hci"))?
|
||||||
.getattr(intern!(py, "Address"))?
|
.getattr(intern!(py, "Address"))?
|
||||||
.call1((address, address_type))
|
.call1((address, address_type))
|
||||||
.map(|any| Self(any.into()))
|
.map(|any| Self(any.into()))
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ install_requires =
|
|||||||
pyserial-asyncio >= 0.5; platform_system!='Emscripten'
|
pyserial-asyncio >= 0.5; platform_system!='Emscripten'
|
||||||
pyserial >= 3.5; platform_system!='Emscripten'
|
pyserial >= 3.5; platform_system!='Emscripten'
|
||||||
pyusb >= 1.2; platform_system!='Emscripten'
|
pyusb >= 1.2; platform_system!='Emscripten'
|
||||||
websockets >= 12.0; platform_system!='Emscripten'
|
websockets == 13.1; platform_system!='Emscripten'
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ from bumble.profiles.ascs import (
|
|||||||
)
|
)
|
||||||
from bumble.profiles.bap import (
|
from bumble.profiles.bap import (
|
||||||
AudioLocation,
|
AudioLocation,
|
||||||
|
BasicAudioAnnouncement,
|
||||||
|
BroadcastAudioAnnouncement,
|
||||||
SupportedFrameDuration,
|
SupportedFrameDuration,
|
||||||
SupportedSamplingFrequency,
|
SupportedSamplingFrequency,
|
||||||
SamplingFrequency,
|
SamplingFrequency,
|
||||||
@@ -200,6 +202,56 @@ def test_codec_specific_configuration() -> None:
|
|||||||
assert CodecSpecificConfiguration.from_bytes(bytes(config)) == config
|
assert CodecSpecificConfiguration.from_bytes(bytes(config)) == config
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_broadcast_audio_announcement() -> None:
|
||||||
|
broadcast_audio_announcement = BroadcastAudioAnnouncement(123456)
|
||||||
|
assert (
|
||||||
|
BroadcastAudioAnnouncement.from_bytes(bytes(broadcast_audio_announcement))
|
||||||
|
== broadcast_audio_announcement
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_basic_audio_announcement() -> None:
|
||||||
|
basic_audio_announcement = BasicAudioAnnouncement(
|
||||||
|
presentation_delay=40000,
|
||||||
|
subgroups=[
|
||||||
|
BasicAudioAnnouncement.Subgroup(
|
||||||
|
codec_id=CodingFormat(codec_id=CodecID.LC3),
|
||||||
|
codec_specific_configuration=CodecSpecificConfiguration(
|
||||||
|
sampling_frequency=SamplingFrequency.FREQ_48000,
|
||||||
|
frame_duration=FrameDuration.DURATION_10000_US,
|
||||||
|
octets_per_codec_frame=100,
|
||||||
|
),
|
||||||
|
metadata=Metadata(
|
||||||
|
[
|
||||||
|
Metadata.Entry(tag=Metadata.Tag.LANGUAGE, data=b'eng'),
|
||||||
|
Metadata.Entry(tag=Metadata.Tag.PROGRAM_INFO, data=b'Disco'),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
bis=[
|
||||||
|
BasicAudioAnnouncement.BIS(
|
||||||
|
index=0,
|
||||||
|
codec_specific_configuration=CodecSpecificConfiguration(
|
||||||
|
audio_channel_allocation=AudioLocation.FRONT_LEFT
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BasicAudioAnnouncement.BIS(
|
||||||
|
index=1,
|
||||||
|
codec_specific_configuration=CodecSpecificConfiguration(
|
||||||
|
audio_channel_allocation=AudioLocation.FRONT_RIGHT
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
BasicAudioAnnouncement.from_bytes(bytes(basic_audio_announcement))
|
||||||
|
== basic_audio_announcement
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pacs():
|
async def test_pacs():
|
||||||
|
|||||||
+55
-10
@@ -19,9 +19,7 @@ import asyncio
|
|||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from types import LambdaType
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
BT_BR_EDR_TRANSPORT,
|
||||||
@@ -29,7 +27,13 @@ from bumble.core import (
|
|||||||
BT_PERIPHERAL_ROLE,
|
BT_PERIPHERAL_ROLE,
|
||||||
ConnectionParameters,
|
ConnectionParameters,
|
||||||
)
|
)
|
||||||
from bumble.device import AdvertisingParameters, Connection, Device
|
from bumble.device import (
|
||||||
|
AdvertisingEventProperties,
|
||||||
|
AdvertisingParameters,
|
||||||
|
Connection,
|
||||||
|
Device,
|
||||||
|
PeriodicAdvertisingParameters,
|
||||||
|
)
|
||||||
from bumble.host import AclPacketQueue, Host
|
from bumble.host import AclPacketQueue, Host
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
HCI_ACCEPT_CONNECTION_REQUEST_COMMAND,
|
HCI_ACCEPT_CONNECTION_REQUEST_COMMAND,
|
||||||
@@ -265,7 +269,8 @@ async def test_flush():
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_legacy_advertising():
|
async def test_legacy_advertising():
|
||||||
device = Device(host=mock.AsyncMock(Host))
|
device = TwoDevices()[0]
|
||||||
|
await device.power_on()
|
||||||
|
|
||||||
# Start advertising
|
# Start advertising
|
||||||
await device.start_advertising()
|
await device.start_advertising()
|
||||||
@@ -283,7 +288,10 @@ async def test_legacy_advertising():
|
|||||||
)
|
)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_legacy_advertising_disconnection(auto_restart):
|
async def test_legacy_advertising_disconnection(auto_restart):
|
||||||
device = Device(host=mock.AsyncMock(spec=Host))
|
devices = TwoDevices()
|
||||||
|
device = devices[0]
|
||||||
|
devices.controllers[0].le_features = bytes.fromhex('ffffffffffffffff')
|
||||||
|
await device.power_on()
|
||||||
peer_address = Address('F0:F1:F2:F3:F4:F5')
|
peer_address = Address('F0:F1:F2:F3:F4:F5')
|
||||||
await device.start_advertising(auto_restart=auto_restart)
|
await device.start_advertising(auto_restart=auto_restart)
|
||||||
device.on_connection(
|
device.on_connection(
|
||||||
@@ -305,6 +313,11 @@ async def test_legacy_advertising_disconnection(auto_restart):
|
|||||||
await async_barrier()
|
await async_barrier()
|
||||||
|
|
||||||
if auto_restart:
|
if auto_restart:
|
||||||
|
assert device.legacy_advertising_set
|
||||||
|
started = asyncio.Event()
|
||||||
|
if not device.is_advertising:
|
||||||
|
device.legacy_advertising_set.once('start', started.set)
|
||||||
|
await asyncio.wait_for(started.wait(), _TIMEOUT)
|
||||||
assert device.is_advertising
|
assert device.is_advertising
|
||||||
else:
|
else:
|
||||||
assert not device.is_advertising
|
assert not device.is_advertising
|
||||||
@@ -313,7 +326,8 @@ async def test_legacy_advertising_disconnection(auto_restart):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_extended_advertising():
|
async def test_extended_advertising():
|
||||||
device = Device(host=mock.AsyncMock(Host))
|
device = TwoDevices()[0]
|
||||||
|
await device.power_on()
|
||||||
|
|
||||||
# Start advertising
|
# Start advertising
|
||||||
advertising_set = await device.create_advertising_set()
|
advertising_set = await device.create_advertising_set()
|
||||||
@@ -332,7 +346,8 @@ async def test_extended_advertising():
|
|||||||
)
|
)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_extended_advertising_connection(own_address_type):
|
async def test_extended_advertising_connection(own_address_type):
|
||||||
device = Device(host=mock.AsyncMock(spec=Host))
|
device = TwoDevices()[0]
|
||||||
|
await device.power_on()
|
||||||
peer_address = Address('F0:F1:F2:F3:F4:F5')
|
peer_address = Address('F0:F1:F2:F3:F4:F5')
|
||||||
advertising_set = await device.create_advertising_set(
|
advertising_set = await device.create_advertising_set(
|
||||||
advertising_parameters=AdvertisingParameters(own_address_type=own_address_type)
|
advertising_parameters=AdvertisingParameters(own_address_type=own_address_type)
|
||||||
@@ -368,8 +383,10 @@ async def test_extended_advertising_connection(own_address_type):
|
|||||||
)
|
)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_extended_advertising_connection_out_of_order(own_address_type):
|
async def test_extended_advertising_connection_out_of_order(own_address_type):
|
||||||
device = Device(host=mock.AsyncMock(spec=Host))
|
devices = TwoDevices()
|
||||||
peer_address = Address('F0:F1:F2:F3:F4:F5')
|
device = devices[0]
|
||||||
|
devices.controllers[0].le_features = bytes.fromhex('ffffffffffffffff')
|
||||||
|
await device.power_on()
|
||||||
advertising_set = await device.create_advertising_set(
|
advertising_set = await device.create_advertising_set(
|
||||||
advertising_parameters=AdvertisingParameters(own_address_type=own_address_type)
|
advertising_parameters=AdvertisingParameters(own_address_type=own_address_type)
|
||||||
)
|
)
|
||||||
@@ -382,7 +399,7 @@ async def test_extended_advertising_connection_out_of_order(own_address_type):
|
|||||||
device.on_connection(
|
device.on_connection(
|
||||||
0x0001,
|
0x0001,
|
||||||
BT_LE_TRANSPORT,
|
BT_LE_TRANSPORT,
|
||||||
peer_address,
|
Address('F0:F1:F2:F3:F4:F5'),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
BT_PERIPHERAL_ROLE,
|
BT_PERIPHERAL_ROLE,
|
||||||
@@ -397,6 +414,34 @@ async def test_extended_advertising_connection_out_of_order(own_address_type):
|
|||||||
await async_barrier()
|
await async_barrier()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_periodic_advertising():
|
||||||
|
device = TwoDevices()[0]
|
||||||
|
await device.power_on()
|
||||||
|
|
||||||
|
# Start advertising
|
||||||
|
advertising_set = await device.create_advertising_set(
|
||||||
|
advertising_parameters=AdvertisingParameters(
|
||||||
|
advertising_event_properties=AdvertisingEventProperties(
|
||||||
|
is_connectable=False
|
||||||
|
)
|
||||||
|
),
|
||||||
|
advertising_data=b'123',
|
||||||
|
periodic_advertising_parameters=PeriodicAdvertisingParameters(),
|
||||||
|
periodic_advertising_data=b'abc',
|
||||||
|
)
|
||||||
|
assert device.extended_advertising_sets
|
||||||
|
assert advertising_set.enabled
|
||||||
|
assert not advertising_set.periodic_enabled
|
||||||
|
|
||||||
|
await advertising_set.start_periodic()
|
||||||
|
assert advertising_set.periodic_enabled
|
||||||
|
|
||||||
|
await advertising_set.stop_periodic()
|
||||||
|
assert not advertising_set.periodic_enabled
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_remote_le_features():
|
async def test_get_remote_le_features():
|
||||||
|
|||||||
+13
-2
@@ -851,7 +851,12 @@ async def test_unsubscribe():
|
|||||||
await async_barrier()
|
await async_barrier()
|
||||||
mock1.assert_called_once_with(ANY, True, False)
|
mock1.assert_called_once_with(ANY, True, False)
|
||||||
|
|
||||||
await c2.subscribe()
|
assert len(server.gatt_server.subscribers) == 1
|
||||||
|
|
||||||
|
def callback(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
await c2.subscribe(callback)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
mock2.assert_called_once_with(ANY, True, False)
|
mock2.assert_called_once_with(ANY, True, False)
|
||||||
|
|
||||||
@@ -861,10 +866,16 @@ async def test_unsubscribe():
|
|||||||
mock1.assert_called_once_with(ANY, False, False)
|
mock1.assert_called_once_with(ANY, False, False)
|
||||||
|
|
||||||
mock2.reset_mock()
|
mock2.reset_mock()
|
||||||
await c2.unsubscribe()
|
await c2.unsubscribe(callback)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
mock2.assert_called_once_with(ANY, False, False)
|
mock2.assert_called_once_with(ANY, False, False)
|
||||||
|
|
||||||
|
# All CCCDs should be zeros now
|
||||||
|
assert list(server.gatt_server.subscribers.values())[0] == {
|
||||||
|
c1.handle: bytes([0, 0]),
|
||||||
|
c2.handle: bytes([0, 0]),
|
||||||
|
}
|
||||||
|
|
||||||
mock1.reset_mock()
|
mock1.reset_mock()
|
||||||
await c1.unsubscribe()
|
await c1.unsubscribe()
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
|
|||||||
Reference in New Issue
Block a user