Merge branch 'main' into update

This commit is contained in:
khsiao-google
2025-09-01 14:51:58 +08:00
29 changed files with 2427 additions and 1231 deletions

View File

@@ -104,5 +104,8 @@
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python-envs.defaultEnvManager": "ms-python.python:system", "python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": [] "python-envs.pythonProjects": [],
"nrf-connect.applications": [
"${workspaceFolder}/extras/zephyr/hci_usb"
]
} }

View File

@@ -39,7 +39,7 @@ import bumble.device
import bumble.logging import bumble.logging
import bumble.transport import bumble.transport
import bumble.utils import bumble.utils
from bumble import company_ids, core, gatt, hci from bumble import company_ids, core, data_types, gatt, hci
from bumble.audio import io as audio_io from bumble.audio import io as audio_io
from bumble.colors import color from bumble.colors import color
from bumble.profiles import bap, bass, le_audio, pbp from bumble.profiles import bap, bass, le_audio, pbp
@@ -859,21 +859,13 @@ async def run_transmit(
) )
broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id) broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id)
advertising_manufacturer_data = ( advertising_data_types: list[core.DataType] = [
b'' data_types.BroadcastName(broadcast_name)
if manufacturer_data is None ]
else bytes( if manufacturer_data is not None:
core.AdvertisingData( advertising_data_types.append(
[ data_types.ManufacturerSpecificData(*manufacturer_data)
(
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA,
struct.pack('<H', manufacturer_data[0])
+ manufacturer_data[1],
)
]
)
) )
)
advertising_set = await device.create_advertising_set( advertising_set = await device.create_advertising_set(
advertising_parameters=bumble.device.AdvertisingParameters( advertising_parameters=bumble.device.AdvertisingParameters(
@@ -885,12 +877,7 @@ async def run_transmit(
), ),
advertising_data=( advertising_data=(
broadcast_audio_announcement.get_advertising_data() broadcast_audio_announcement.get_advertising_data()
+ bytes( + bytes(core.AdvertisingData(advertising_data_types))
core.AdvertisingData(
[(core.AdvertisingData.BROADCAST_NAME, broadcast_name.encode())]
)
)
+ advertising_manufacturer_data
), ),
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters( periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
periodic_advertising_interval_min=80, periodic_advertising_interval_min=80,

View File

@@ -37,7 +37,7 @@ import click
import bumble import bumble
import bumble.logging import bumble.logging
from bumble import utils from bumble import data_types, utils
from bumble.colors import color from bumble.colors import color
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import AdvertisingParameters, CisLink, Device, DeviceConfiguration from bumble.device import AdvertisingParameters, CisLink, Device, DeviceConfiguration
@@ -330,22 +330,13 @@ class Speaker:
advertising_data = bytes( advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName(device_config.name),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Flags(
bytes(device_config.name, 'utf-8'), AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
), ),
( data_types.IncompleteListOf16BitServiceUUIDs(
AdvertisingData.FLAGS, [pacs.PublishedAudioCapabilitiesService.UUID]
bytes(
[
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(pacs.PublishedAudioCapabilitiesService.UUID),
), ),
] ]
) )

View File

@@ -23,6 +23,7 @@ import struct
import click import click
from prompt_toolkit.shortcuts import PromptSession from prompt_toolkit.shortcuts import PromptSession
from bumble import data_types
from bumble.a2dp import make_audio_sink_service_sdp_records from bumble.a2dp import make_audio_sink_service_sdp_records
from bumble.att import ( from bumble.att import (
ATT_INSUFFICIENT_AUTHENTICATION_ERROR, ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
@@ -34,6 +35,7 @@ from bumble.core import (
UUID, UUID,
AdvertisingData, AdvertisingData,
Appearance, Appearance,
DataType,
PhysicalTransport, PhysicalTransport,
ProtocolError, ProtocolError,
) )
@@ -506,33 +508,21 @@ async def pair(
if mode == 'dual': if mode == 'dual':
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
ad_structs = [ advertising_data_types: list[DataType] = [
( data_types.Flags(flags),
AdvertisingData.FLAGS, data_types.CompleteLocalName('Bumble'),
bytes([flags]),
),
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
] ]
if service_uuids_16: if service_uuids_16:
ad_structs.append( advertising_data_types.append(
( data_types.IncompleteListOf16BitServiceUUIDs(service_uuids_16)
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
b"".join(bytes(uuid) for uuid in service_uuids_16),
)
) )
if service_uuids_32: if service_uuids_32:
ad_structs.append( advertising_data_types.append(
( data_types.IncompleteListOf32BitServiceUUIDs(service_uuids_32)
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
b"".join(bytes(uuid) for uuid in service_uuids_32),
)
) )
if service_uuids_128: if service_uuids_128:
ad_structs.append( advertising_data_types.append(
( data_types.IncompleteListOf128BitServiceUUIDs(service_uuids_128)
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
b"".join(bytes(uuid) for uuid in service_uuids_128),
)
) )
if advertise_appearance: if advertise_appearance:
@@ -559,13 +549,10 @@ async def pair(
advertise_appearance_int = int( advertise_appearance_int = int(
Appearance(category_enum, subcategory_enum) Appearance(category_enum, subcategory_enum)
) )
ad_structs.append( advertising_data_types.append(
( data_types.Appearance(category_enum, subcategory_enum)
AdvertisingData.APPEARANCE,
struct.pack('<H', advertise_appearance_int),
)
) )
device.advertising_data = bytes(AdvertisingData(ad_structs)) device.advertising_data = bytes(AdvertisingData(advertising_data_types))
await device.start_advertising( await device.start_advertising(
auto_restart=True, auto_restart=True,
own_address_type=( own_address_type=(

View File

@@ -20,6 +20,7 @@ import asyncio
import click import click
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.colors import color from bumble.colors import color
from bumble.device import Advertisement, Device from bumble.device import Advertisement, Device
from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant
@@ -94,13 +95,22 @@ class AdvertisementPrinter:
else: else:
phy_info = '' phy_info = ''
details = separator.join(
[
data_type.to_string(use_label=True)
for data_type in data_types.data_types_from_advertising_data(
advertisement.data
)
]
)
print( print(
f'>>> {color(address, address_color)} ' f'>>> {color(address, address_color)} '
f'[{color(address_type_string, type_color)}]{address_qualifier}' f'[{color(address_type_string, type_color)}]{address_qualifier}'
f'{resolution_qualifier}:{separator}' f'{resolution_qualifier}:{separator}'
f'{phy_info}' f'{phy_info}'
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}' f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
f'{advertisement.data.to_string(separator)}\n' f'{details}\n'
) )
def on_advertisement(self, advertisement): def on_advertisement(self, advertisement):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1025
bumble/data_types.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -47,6 +47,7 @@ from typing_extensions import Self
from bumble import ( from bumble import (
core, core,
data_types,
gatt, gatt,
gatt_client, gatt_client,
gatt_server, gatt_server,
@@ -1881,16 +1882,6 @@ class Connection(utils.CompositeEventEmitter):
def send_l2cap_pdu(self, cid: int, pdu: bytes) -> None: def send_l2cap_pdu(self, cid: int, pdu: bytes) -> None:
self.device.send_l2cap_pdu(self.handle, cid, pdu) self.device.send_l2cap_pdu(self.handle, cid, pdu)
@utils.deprecated("Please use create_l2cap_channel()")
async def open_l2cap_channel(
self,
psm,
max_credits=DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS,
mtu=DEVICE_DEFAULT_L2CAP_COC_MTU,
mps=DEVICE_DEFAULT_L2CAP_COC_MPS,
):
return await self.device.open_l2cap_channel(self, psm, max_credits, mtu, mps)
@overload @overload
async def create_l2cap_channel( async def create_l2cap_channel(
self, spec: l2cap.ClassicChannelSpec self, spec: l2cap.ClassicChannelSpec
@@ -2076,9 +2067,7 @@ class DeviceConfiguration:
connectable: bool = True connectable: bool = True
discoverable: bool = True discoverable: bool = True
advertising_data: bytes = bytes( advertising_data: bytes = bytes(
AdvertisingData( AdvertisingData([data_types.CompleteLocalName(DEVICE_DEFAULT_NAME)])
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(DEVICE_DEFAULT_NAME, 'utf-8'))]
)
) )
irk: bytes = bytes(16) # This really must be changed for any level of security irk: bytes = bytes(16) # This really must be changed for any level of security
keystore: Optional[str] = None keystore: Optional[str] = None
@@ -2122,9 +2111,7 @@ class DeviceConfiguration:
self.advertising_data = bytes.fromhex(advertising_data) self.advertising_data = bytes.fromhex(advertising_data)
elif name is not None: elif name is not None:
self.advertising_data = bytes( self.advertising_data = bytes(
AdvertisingData( AdvertisingData([data_types.CompleteLocalName(self.name)])
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))]
)
) )
# Load scan response data # Load scan response data
@@ -2608,36 +2595,6 @@ class Device(utils.CompositeEventEmitter):
None, None,
) )
@utils.deprecated("Please use create_l2cap_server()")
def register_l2cap_server(self, psm, server) -> int:
return self.l2cap_channel_manager.register_server(psm, server)
@utils.deprecated("Please use create_l2cap_server()")
def register_l2cap_channel_server(
self,
psm,
server,
max_credits=DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS,
mtu=DEVICE_DEFAULT_L2CAP_COC_MTU,
mps=DEVICE_DEFAULT_L2CAP_COC_MPS,
):
return self.l2cap_channel_manager.register_le_coc_server(
psm, server, max_credits, mtu, mps
)
@utils.deprecated("Please use create_l2cap_channel()")
async def open_l2cap_channel(
self,
connection,
psm,
max_credits=DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS,
mtu=DEVICE_DEFAULT_L2CAP_COC_MTU,
mps=DEVICE_DEFAULT_L2CAP_COC_MPS,
):
return await self.l2cap_channel_manager.open_le_coc(
connection, psm, max_credits, mtu, mps
)
@overload @overload
async def create_l2cap_channel( async def create_l2cap_channel(
self, self,
@@ -3605,14 +3562,7 @@ class Device(utils.CompositeEventEmitter):
# Synthesize an inquiry response if none is set already # Synthesize an inquiry response if none is set already
if self.inquiry_response is None: if self.inquiry_response is None:
self.inquiry_response = bytes( self.inquiry_response = bytes(
AdvertisingData( AdvertisingData([data_types.CompleteLocalName(self.name)])
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes(self.name, 'utf-8'),
)
]
)
) )
# Update the controller # Update the controller

View File

@@ -112,7 +112,14 @@ class SpecableEnum(utils.OpenIntEnum):
@classmethod @classmethod
def type_spec(cls, size: int): def type_spec(cls, size: int):
return {'size': size, 'mapper': lambda x: cls(x).name} return {
'serializer': lambda x: x.to_bytes(size, 'little'),
'parser': lambda data, offset: (
offset + size,
cls(int.from_bytes(data[offset : offset + size], 'little')),
),
'mapper': lambda x: cls(x).name,
}
@classmethod @classmethod
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False): def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
@@ -123,7 +130,14 @@ class SpecableFlag(enum.IntFlag):
@classmethod @classmethod
def type_spec(cls, size: int): def type_spec(cls, size: int):
return {'size': size, 'mapper': lambda x: cls(x).name} return {
'serializer': lambda x: x.to_bytes(size, 'little'),
'parser': lambda data, offset: (
offset + size,
cls(int.from_bytes(data[offset : offset + size], 'little')),
),
'mapper': lambda x: cls(x).name,
}
@classmethod @classmethod
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False): def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
@@ -1322,7 +1336,7 @@ class LeFeature(SpecableEnum):
MONITORING_ADVERTISERS = 64 MONITORING_ADVERTISERS = 64
FRAME_SPACE_UPDATE = 65 FRAME_SPACE_UPDATE = 65
class LeFeatureMask(enum.IntFlag): class LeFeatureMask(utils.CompatibleIntFlag):
LE_ENCRYPTION = 1 << LeFeature.LE_ENCRYPTION LE_ENCRYPTION = 1 << LeFeature.LE_ENCRYPTION
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1 << LeFeature.CONNECTION_PARAMETERS_REQUEST_PROCEDURE CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1 << LeFeature.CONNECTION_PARAMETERS_REQUEST_PROCEDURE
EXTENDED_REJECT_INDICATION = 1 << LeFeature.EXTENDED_REJECT_INDICATION EXTENDED_REJECT_INDICATION = 1 << LeFeature.EXTENDED_REJECT_INDICATION
@@ -1463,7 +1477,7 @@ class LmpFeature(SpecableEnum):
SLOT_AVAILABILITY_MASK = 138 SLOT_AVAILABILITY_MASK = 138
TRAIN_NUDGING = 139 TRAIN_NUDGING = 139
class LmpFeatureMask(enum.IntFlag): class LmpFeatureMask(utils.CompatibleIntFlag):
# Page 0 (Legacy LMP features) # Page 0 (Legacy LMP features)
LMP_3_SLOT_PACKETS = (1 << LmpFeature.LMP_3_SLOT_PACKETS) LMP_3_SLOT_PACKETS = (1 << LmpFeature.LMP_3_SLOT_PACKETS)
LMP_5_SLOT_PACKETS = (1 << LmpFeature.LMP_5_SLOT_PACKETS) LMP_5_SLOT_PACKETS = (1 << LmpFeature.LMP_5_SLOT_PACKETS)
@@ -2135,6 +2149,7 @@ class Address:
if len(address) == 12 + 5: if len(address) == 12 + 5:
# Form with ':' separators # Form with ':' separators
address = address.replace(':', '') address = address.replace(':', '')
self.address_bytes = bytes(reversed(bytes.fromhex(address))) self.address_bytes = bytes(reversed(bytes.fromhex(address)))
if len(self.address_bytes) != 6: if len(self.address_bytes) != 6:

View File

@@ -217,33 +217,41 @@ class HID(ABC, utils.EventEmitter):
self.role = role self.role = role
# Register ourselves with the L2CAP channel manager # Register ourselves with the L2CAP channel manager
device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection) device.create_l2cap_server(
device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection) l2cap.ClassicChannelSpec(HID_CONTROL_PSM), self.on_l2cap_connection
)
device.create_l2cap_server(
l2cap.ClassicChannelSpec(HID_INTERRUPT_PSM), self.on_l2cap_connection
)
device.on(device.EVENT_CONNECTION, self.on_device_connection) device.on(device.EVENT_CONNECTION, self.on_device_connection)
async def connect_control_channel(self) -> None: async def connect_control_channel(self) -> None:
if not self.connection:
raise InvalidStateError("Connection is not established!")
# Create a new L2CAP connection - control channel # Create a new L2CAP connection - control channel
try: try:
channel = await self.device.l2cap_channel_manager.connect( channel = await self.connection.create_l2cap_channel(
self.connection, HID_CONTROL_PSM l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
) )
channel.sink = self.on_ctrl_pdu channel.sink = self.on_ctrl_pdu
self.l2cap_ctrl_channel = channel self.l2cap_ctrl_channel = channel
except ProtocolError: except ProtocolError:
logging.exception(f'L2CAP connection failed.') logging.exception('L2CAP connection failed.')
raise raise
async def connect_interrupt_channel(self) -> None: async def connect_interrupt_channel(self) -> None:
if not self.connection:
raise InvalidStateError("Connection is not established!")
# Create a new L2CAP connection - interrupt channel # Create a new L2CAP connection - interrupt channel
try: try:
channel = await self.device.l2cap_channel_manager.connect( channel = await self.connection.create_l2cap_channel(
self.connection, HID_INTERRUPT_PSM l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
) )
channel.sink = self.on_intr_pdu channel.sink = self.on_intr_pdu
self.l2cap_intr_channel = channel self.l2cap_intr_channel = channel
except ProtocolError: except ProtocolError:
logging.exception(f'L2CAP connection failed.') logging.exception('L2CAP connection failed.')
raise raise
async def disconnect_interrupt_channel(self) -> None: async def disconnect_interrupt_channel(self) -> None:

View File

@@ -1531,16 +1531,6 @@ class ChannelManager:
if cid in self.fixed_channels: if cid in self.fixed_channels:
del self.fixed_channels[cid] del self.fixed_channels[cid]
@utils.deprecated("Please use create_classic_server")
def register_server(
self,
psm: int,
server: Callable[[ClassicChannel], Any],
) -> int:
return self.create_classic_server(
handler=server, spec=ClassicChannelSpec(psm=psm)
).psm
def create_classic_server( def create_classic_server(
self, self,
spec: ClassicChannelSpec, spec: ClassicChannelSpec,
@@ -1577,22 +1567,6 @@ class ChannelManager:
return self.servers[spec.psm] return self.servers[spec.psm]
@utils.deprecated("Please use create_le_credit_based_server()")
def register_le_coc_server(
self,
psm: int,
server: Callable[[LeCreditBasedChannel], Any],
max_credits: int,
mtu: int,
mps: int,
) -> int:
return self.create_le_credit_based_server(
spec=LeCreditBasedChannelSpec(
psm=None if psm == 0 else psm, mtu=mtu, mps=mps, max_credits=max_credits
),
handler=server,
).psm
def create_le_credit_based_server( def create_le_credit_based_server(
self, self,
spec: LeCreditBasedChannelSpec, spec: LeCreditBasedChannelSpec,
@@ -2145,17 +2119,6 @@ class ChannelManager:
if channel.source_cid in connection_channels: if channel.source_cid in connection_channels:
del connection_channels[channel.source_cid] del connection_channels[channel.source_cid]
@utils.deprecated("Please use create_le_credit_based_channel()")
async def open_le_coc(
self, connection: Connection, psm: int, max_credits: int, mtu: int, mps: int
) -> LeCreditBasedChannel:
return await self.create_le_credit_based_channel(
connection=connection,
spec=LeCreditBasedChannelSpec(
psm=psm, max_credits=max_credits, mtu=mtu, mps=mps
),
)
async def create_le_credit_based_channel( async def create_le_credit_based_channel(
self, self,
connection: Connection, connection: Connection,
@@ -2202,12 +2165,6 @@ class ChannelManager:
return channel return channel
@utils.deprecated("Please use create_classic_channel()")
async def connect(self, connection: Connection, psm: int) -> ClassicChannel:
return await self.create_classic_channel(
connection=connection, spec=ClassicChannelSpec(psm=psm)
)
async def create_classic_channel( async def create_classic_channel(
self, connection: Connection, spec: ClassicChannelSpec self, connection: Connection, spec: ClassicChannelSpec
) -> ClassicChannel: ) -> ClassicChannel:
@@ -2244,20 +2201,3 @@ class ChannelManager:
raise e raise e
return channel return channel
# -----------------------------------------------------------------------------
# Deprecated Classes
# -----------------------------------------------------------------------------
class Channel(ClassicChannel):
@utils.deprecated("Please use ClassicChannel")
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
class LeConnectionOrientedChannel(LeCreditBasedChannel):
@utils.deprecated("Please use LeCreditBasedChannel")
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

View File

@@ -21,7 +21,7 @@ import logging
import struct import struct
from typing import Any, Callable, Optional, Union from typing import Any, Callable, Optional, Union
from bumble import gatt, gatt_client, l2cap, utils from bumble import data_types, gatt, gatt_client, l2cap, utils
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Connection, Device from bumble.device import Connection, Device
@@ -185,12 +185,11 @@ class AshaService(gatt.TemplateService):
return bytes( return bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.ServiceData16BitUUID(
AdvertisingData.SERVICE_DATA_16_BIT_UUID, gatt.GATT_ASHA_SERVICE,
bytes(gatt.GATT_ASHA_SERVICE) bytes([self.protocol_version, self.capability])
+ bytes([self.protocol_version, self.capability])
+ self.hisyncid[:4], + self.hisyncid[:4],
), )
] ]
) )
) )

View File

@@ -27,7 +27,7 @@ from collections.abc import Sequence
from typing_extensions import Self from typing_extensions import Self
from bumble import core, gatt, hci, utils from bumble import core, data_types, gatt, hci, utils
from bumble.profiles import le_audio from bumble.profiles import le_audio
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -257,11 +257,10 @@ class UnicastServerAdvertisingData:
return bytes( return bytes(
core.AdvertisingData( core.AdvertisingData(
[ [
( data_types.ServiceData16BitUUID(
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID, gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE,
struct.pack( struct.pack(
'<2sBIB', '<BIB',
bytes(gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE),
self.announcement_type, self.announcement_type,
self.available_audio_contexts, self.available_audio_contexts,
len(self.metadata), len(self.metadata),
@@ -490,12 +489,8 @@ class BroadcastAudioAnnouncement:
return bytes( return bytes(
core.AdvertisingData( core.AdvertisingData(
[ [
( data_types.ServiceData16BitUUID(
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID, gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
(
bytes(gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE)
+ bytes(self)
),
) )
] ]
) )
@@ -607,12 +602,8 @@ class BasicAudioAnnouncement:
return bytes( return bytes(
core.AdvertisingData( core.AdvertisingData(
[ [
( data_types.ServiceData16BitUUID(
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID, gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
(
bytes(gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE)
+ bytes(self)
),
) )
] ]
) )

View File

@@ -500,6 +500,22 @@ class OpenIntEnum(enum.IntEnum):
return obj return obj
# -----------------------------------------------------------------------------
class CompatibleIntFlag(enum.IntFlag):
"""
Subclass of `enum.IntFlag` with a `composite_name` property that behaves like the
`name` property of the `enum.IntFlag` implementation for python vesions >= 3.11
"""
@property
def composite_name(self) -> str:
return '|'.join(
name
for flag in self.__class__
if self.value & flag.value and (name := flag.name) is not None
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class ByteSerializable(Protocol): class ByteSerializable(Protocol):
""" """

View File

@@ -21,6 +21,7 @@ import struct
import sys import sys
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.profiles.battery_service import BatteryService from bumble.profiles.battery_service import BatteryService
@@ -47,15 +48,14 @@ async def main() -> None:
device.advertising_data = bytes( device.advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble Battery'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.IncompleteListOf16BitServiceUUIDs(
bytes('Bumble Battery', 'utf-8'), [battery_service.uuid]
), ),
( data_types.Appearance(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, data_types.Appearance.Category.WEARABLE_AUDIO_DEVICE,
bytes(battery_service.uuid), data_types.Appearance.WearableAudioDeviceSubcategory.EARBUD,
), ),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
] ]
) )
) )

View File

@@ -20,6 +20,7 @@ import struct
import sys import sys
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.profiles.device_information_service import DeviceInformationService from bumble.profiles.device_information_service import DeviceInformationService
@@ -53,11 +54,11 @@ async def main() -> None:
device.advertising_data = bytes( device.advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble Device'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Appearance(
bytes('Bumble Device', 'utf-8'), data_types.Appearance.Category.HEART_RATE_SENSOR,
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
), ),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
] ]
) )
) )

View File

@@ -24,6 +24,7 @@ import sys
import time import time
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.profiles.device_information_service import DeviceInformationService from bumble.profiles.device_information_service import DeviceInformationService
@@ -88,15 +89,14 @@ async def main() -> None:
device.advertising_data = bytes( device.advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble Heart'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.IncompleteListOf16BitServiceUUIDs(
bytes('Bumble Heart', 'utf-8'), [heart_rate_service.uuid]
), ),
( data_types.Appearance(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, data_types.Appearance.Category.HEART_RATE_SENSOR,
bytes(heart_rate_service.uuid), data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
), ),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
] ]
) )
) )

View File

@@ -23,6 +23,7 @@ import sys
import websockets import websockets
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.colors import color from bumble.colors import color
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Connection, Device, Peer from bumble.device import Connection, Device, Peer
@@ -341,16 +342,18 @@ async def keyboard_device(device, command):
device.advertising_data = bytes( device.advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble Keyboard'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.IncompleteListOf16BitServiceUUIDs(
bytes('Bumble Keyboard', 'utf-8'), [GATT_HUMAN_INTERFACE_DEVICE_SERVICE]
), ),
( data_types.Appearance(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, data_types.Appearance.Category.HUMAN_INTERFACE_DEVICE,
bytes(GATT_HUMAN_INTERFACE_DEVICE_SERVICE), data_types.Appearance.HumanInterfaceDeviceSubcategory.KEYBOARD,
),
data_types.Flags(
AdvertisingData.Flags.LE_LIMITED_DISCOVERABLE_MODE
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
), ),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x03C1)),
(AdvertisingData.FLAGS, bytes([0x05])),
] ]
) )
) )

View File

@@ -20,6 +20,7 @@ import struct
import sys import sys
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import AdvertisingType, Device from bumble.device import AdvertisingType, Device
from bumble.hci import Address from bumble.hci import Address
@@ -60,7 +61,10 @@ async def main() -> None:
device.scan_response_data = bytes( device.scan_response_data = bytes(
AdvertisingData( AdvertisingData(
[ [
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)), data_types.Appearance(
data_types.Appearance.Category.HEART_RATE_SENSOR,
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
)
] ]
) )
) )

View File

@@ -23,7 +23,7 @@ from typing import Optional
import websockets import websockets
import bumble.logging import bumble.logging
from bumble import decoder, gatt from bumble import data_types, decoder, gatt
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import AdvertisingParameters, Device from bumble.device import AdvertisingParameters, Device
from bumble.profiles import asha from bumble.profiles import asha
@@ -78,14 +78,10 @@ async def main() -> None:
bytes( bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName(device.name),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Flags(AdvertisingData.Flags(0x06)),
bytes(device.name, 'utf-8'), data_types.IncompleteListOf16BitServiceUUIDs(
), [gatt.GATT_ASHA_SERVICE]
(AdvertisingData.FLAGS, bytes([0x06])),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(gatt.GATT_ASHA_SERVICE),
), ),
] ]
) )

View File

@@ -20,6 +20,7 @@ import secrets
import sys import sys
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.hci import Address from bumble.hci import Address
@@ -66,23 +67,14 @@ async def main() -> None:
bytes( bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName(f'Bumble LE Audio-{i}'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Flags(
bytes(f'Bumble LE Audio-{i}', 'utf-8'), AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
), ),
( data_types.IncompleteListOf16BitServiceUUIDs(
AdvertisingData.FLAGS, [CoordinatedSetIdentificationService.UUID]
bytes(
[
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(CoordinatedSetIdentificationService.UUID),
), ),
] ]
) )

View File

@@ -19,6 +19,7 @@ import asyncio
import sys import sys
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.profiles.hap import ( from bumble.profiles.hap import (
@@ -71,23 +72,14 @@ async def main() -> None:
advertising_data = bytes( advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble HearingAccessService'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Flags(
bytes('Bumble HearingAccessService', 'utf-8'), AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
), ),
( data_types.IncompleteListOf16BitServiceUUIDs(
AdvertisingData.FLAGS, [HearingAccessService.UUID]
bytes(
[
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(HearingAccessService.UUID),
), ),
] ]
) )

View File

@@ -23,6 +23,7 @@ from typing import Optional
import websockets import websockets
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import ( from bumble.device import (
AdvertisingEventProperties, AdvertisingEventProperties,
@@ -106,17 +107,10 @@ async def main() -> None:
advertising_data = bytes( advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble LE Audio'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Flags(AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG),
bytes('Bumble LE Audio', 'utf-8'), data_types.IncompleteListOf16BitServiceUUIDs(
), [PublishedAudioCapabilitiesService.UUID]
(
AdvertisingData.FLAGS,
bytes([AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG]),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(PublishedAudioCapabilitiesService.UUID),
), ),
] ]
) )

View File

@@ -24,6 +24,7 @@ import struct
import sys import sys
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.hci import CodecID, CodingFormat, HCI_IsoDataPacket from bumble.hci import CodecID, CodingFormat, HCI_IsoDataPacket
@@ -111,23 +112,14 @@ async def main() -> None:
bytes( bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble LE Audio'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Flags(
bytes('Bumble LE Audio', 'utf-8'), AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
), ),
( data_types.IncompleteListOf16BitServiceUUIDs(
AdvertisingData.FLAGS, [PublishedAudioCapabilitiesService.UUID]
bytes(
[
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(PublishedAudioCapabilitiesService.UUID),
), ),
] ]
) )

View File

@@ -24,6 +24,7 @@ from typing import Optional
import websockets import websockets
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import AdvertisingEventProperties, AdvertisingParameters, Device from bumble.device import AdvertisingEventProperties, AdvertisingParameters, Device
from bumble.hci import CodecID, CodingFormat, OwnAddressType from bumble.hci import CodecID, CodingFormat, OwnAddressType
@@ -127,23 +128,14 @@ async def main() -> None:
bytes( bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble LE Audio'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Flags(
bytes('Bumble LE Audio', 'utf-8'), AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
), ),
( data_types.IncompleteListOf16BitServiceUUIDs(
AdvertisingData.FLAGS, [PublishedAudioCapabilitiesService.UUID]
bytes(
[
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(PublishedAudioCapabilitiesService.UUID),
), ),
] ]
) )

View File

@@ -15,67 +15,210 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio from __future__ import annotations
import struct import struct
from collections.abc import Sequence
import pytest import pytest
from bumble import avc, avctp, avrcp, controller, core, device, host, link from bumble import avc, avctp, avrcp
from bumble.transport import common
from . import test_utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class TwoDevices: class TwoDevices(test_utils.TwoDevices):
def __init__(self): protocols: Sequence[avrcp.Protocol] = ()
self.connections = [None, None]
addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0']
self.link = link.LocalLink()
self.controllers = [
controller.Controller('C1', link=self.link, public_address=addresses[0]),
controller.Controller('C2', link=self.link, public_address=addresses[1]),
]
self.devices = [
device.Device(
address=addresses[0],
host=host.Host(
self.controllers[0], common.AsyncPipeSink(self.controllers[0])
),
),
device.Device(
address=addresses[1],
host=host.Host(
self.controllers[1], common.AsyncPipeSink(self.controllers[1])
),
),
]
self.devices[0].classic_enabled = True
self.devices[1].classic_enabled = True
self.connections = [None, None]
self.protocols = [None, None]
def on_connection(self, which, connection):
self.connections[which] = connection
async def setup_connections(self):
await self.devices[0].power_on()
await self.devices[1].power_on()
self.connections = await asyncio.gather(
self.devices[0].connect(
self.devices[1].public_address, core.PhysicalTransport.BR_EDR
),
self.devices[1].accept(self.devices[0].public_address),
)
async def setup_avdtp_connections(self):
self.protocols = [avrcp.Protocol(), avrcp.Protocol()] self.protocols = [avrcp.Protocol(), avrcp.Protocol()]
self.protocols[0].listen(self.devices[1]) self.protocols[0].listen(self.devices[1])
await self.protocols[1].connect(self.connections[0]) await self.protocols[1].connect(self.connections[0])
@classmethod
async def create_with_avdtp(cls) -> TwoDevices:
devices = await cls.create_with_connection()
await devices.setup_avdtp_connections()
return devices
# -----------------------------------------------------------------------------
def test_GetPlayStatusCommand():
command = avrcp.GetPlayStatusCommand()
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_GetCapabilitiesCommand():
command = avrcp.GetCapabilitiesCommand(
capability_id=avrcp.GetCapabilitiesCommand.CapabilityId.COMPANY_ID
)
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_SetAbsoluteVolumeCommand():
command = avrcp.SetAbsoluteVolumeCommand(volume=5)
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_GetElementAttributesCommand():
command = avrcp.GetElementAttributesCommand(
identifier=999,
attribute_ids=[
avrcp.MediaAttributeId.ALBUM_NAME,
avrcp.MediaAttributeId.ARTIST_NAME,
],
)
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_RegisterNotificationCommand():
command = avrcp.RegisterNotificationCommand(
event_id=avrcp.EventId.ADDRESSED_PLAYER_CHANGED, playback_interval=123
)
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_UidsChangedEvent():
event = avrcp.UidsChangedEvent(uid_counter=7)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_TrackChangedEvent():
event = avrcp.TrackChangedEvent(identifier=b'12356')
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_VolumeChangedEvent():
event = avrcp.VolumeChangedEvent(volume=9)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_PlaybackStatusChangedEvent():
event = avrcp.PlaybackStatusChangedEvent(play_status=avrcp.PlayStatus.PLAYING)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_AddressedPlayerChangedEvent():
event = avrcp.AddressedPlayerChangedEvent(
player=avrcp.AddressedPlayerChangedEvent.Player(player_id=9, uid_counter=10)
)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_AvailablePlayersChangedEvent():
event = avrcp.AvailablePlayersChangedEvent()
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_PlaybackPositionChangedEvent():
event = avrcp.PlaybackPositionChangedEvent(playback_position=1314)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_NowPlayingContentChangedEvent():
event = avrcp.NowPlayingContentChangedEvent()
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_PlayerApplicationSettingChangedEvent():
event = avrcp.PlayerApplicationSettingChangedEvent(
player_application_settings=[
avrcp.PlayerApplicationSettingChangedEvent.Setting(
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
)
]
)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_RejectedResponse():
pdu_id = avrcp.PduId.GET_ELEMENT_ATTRIBUTES
response = avrcp.RejectedResponse(
pdu_id=pdu_id,
status_code=avrcp.StatusCode.DOES_NOT_EXIST,
)
assert (
avrcp.RejectedResponse.from_bytes(pdu=bytes(response), pdu_id=pdu_id)
== response
)
# -----------------------------------------------------------------------------
def test_GetPlayStatusResponse():
response = avrcp.GetPlayStatusResponse(
song_length=1010, song_position=13, play_status=avrcp.PlayStatus.PAUSED
)
assert avrcp.GetPlayStatusResponse.from_bytes(bytes(response)) == response
# -----------------------------------------------------------------------------
def test_NotImplementedResponse():
pdu_id = avrcp.PduId.GET_ELEMENT_ATTRIBUTES
response = avrcp.NotImplementedResponse(pdu_id=pdu_id, parameters=b'koasd')
assert (
avrcp.NotImplementedResponse.from_bytes(bytes(response), pdu_id=pdu_id)
== response
)
# -----------------------------------------------------------------------------
def test_GetCapabilitiesResponse():
response = avrcp.GetCapabilitiesResponse(
capability_id=avrcp.GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED,
capabilities=[
avrcp.EventId.ADDRESSED_PLAYER_CHANGED,
avrcp.EventId.BATT_STATUS_CHANGED,
],
)
assert avrcp.GetCapabilitiesResponse.from_bytes(bytes(response)) == response
# -----------------------------------------------------------------------------
def test_RegisterNotificationResponse():
response = avrcp.RegisterNotificationResponse(
event=avrcp.PlaybackPositionChangedEvent(playback_position=38)
)
assert avrcp.RegisterNotificationResponse.from_bytes(bytes(response)) == response
# -----------------------------------------------------------------------------
def test_SetAbsoluteVolumeResponse():
response = avrcp.SetAbsoluteVolumeResponse(volume=99)
assert avrcp.SetAbsoluteVolumeResponse.from_bytes(bytes(response)) == response
# -----------------------------------------------------------------------------
def test_GetElementAttributesResponse():
response = avrcp.GetElementAttributesResponse(
attributes=[
avrcp.MediaAttribute(
attribute_id=avrcp.MediaAttributeId.ALBUM_NAME,
attribute_value="White Album",
)
]
)
assert avrcp.GetElementAttributesResponse.from_bytes(bytes(response)) == response
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_frame_parser(): def test_frame_parser():
with pytest.raises(ValueError) as error: with pytest.raises(ValueError):
avc.Frame.from_bytes(bytes.fromhex("11480000")) avc.Frame.from_bytes(bytes.fromhex("11480000"))
x = bytes.fromhex("014D0208") x = bytes.fromhex("014D0208")
@@ -217,8 +360,7 @@ def test_passthrough_commands():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_supported_events(): async def test_get_supported_events():
two_devices = TwoDevices() two_devices = await TwoDevices.create_with_avdtp()
await two_devices.setup_connections()
supported_events = await two_devices.protocols[0].get_supported_events() supported_events = await two_devices.protocols[0].get_supported_events()
assert supported_events == [] assert supported_events == []

View File

@@ -16,7 +16,13 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from bumble.core import UUID, AdvertisingData, Appearance, get_dict_key_by_value from bumble.core import (
UUID,
AdvertisingData,
Appearance,
ClassOfDevice,
get_dict_key_by_value,
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -93,6 +99,24 @@ def test_appearance() -> None:
assert int(a) == 0x3333 assert int(a) == 0x3333
# -----------------------------------------------------------------------------
def test_class_of_device() -> None:
c1 = ClassOfDevice(
ClassOfDevice.MajorServiceClasses.AUDIO
| ClassOfDevice.MajorServiceClasses.RENDERING,
ClassOfDevice.MajorDeviceClass.AUDIO_VIDEO,
ClassOfDevice.AudioVideoMinorDeviceClass.CAMCORDER,
)
assert str(c1) == "ClassOfDevice(RENDERING|AUDIO,AUDIO_VIDEO/CAMCORDER)"
c2 = ClassOfDevice(
ClassOfDevice.MajorServiceClasses.AUDIO,
ClassOfDevice.MajorDeviceClass.AUDIO_VIDEO,
0x123,
)
assert str(c2) == "ClassOfDevice(AUDIO,AUDIO_VIDEO/0x123)"
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
if __name__ == '__main__': if __name__ == '__main__':
test_ad_data() test_ad_data()

View File

@@ -17,6 +17,7 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import struct import struct
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.hci import HCI_Reset_Command from bumble.hci import HCI_Reset_Command
@@ -65,24 +66,18 @@ class HeartRateMonitor:
self.device.advertising_data = bytes( self.device.advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.Flags(
AdvertisingData.FLAGS, AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
bytes( | AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
[
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
]
),
), ),
( data_types.CompleteLocalName('Bumble Heart'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.IncompleteListOf16BitServiceUUIDs(
bytes('Bumble Heart', 'utf-8'), [self.heart_rate_service.uuid]
), ),
( data_types.Appearance(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, data_types.Appearance.Category.HEART_RATE_SENSOR,
bytes(self.heart_rate_service.uuid), data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
), ),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
] ]
) )
) )