forked from auracaster/bumble_mirror
Merge branch 'main' into update
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
41
apps/pair.py
41
apps/pair.py
@@ -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=(
|
||||||
|
|||||||
12
apps/scan.py
12
apps/scan.py
@@ -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):
|
||||||
|
|||||||
1019
bumble/avrcp.py
1019
bumble/avrcp.py
File diff suppressed because it is too large
Load Diff
810
bumble/core.py
810
bumble/core.py
File diff suppressed because it is too large
Load Diff
1025
bumble/data_types.py
Normal file
1025
bumble/data_types.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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],
|
||||||
),
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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)),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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])),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 == []
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user