forked from auracaster/bumble_mirror
Merge pull request #679 from google/gbg/pairing-ios
This commit is contained in:
@@ -1256,6 +1256,7 @@ class Central(Connection.Listener):
|
|||||||
self.device.classic_enabled = self.classic
|
self.device.classic_enabled = self.classic
|
||||||
|
|
||||||
# Set up a pairing config factory with minimal requirements.
|
# Set up a pairing config factory with minimal requirements.
|
||||||
|
self.device.config.keystore = "JsonKeyStore"
|
||||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||||
sc=False, mitm=False, bonding=False
|
sc=False, mitm=False, bonding=False
|
||||||
)
|
)
|
||||||
@@ -1408,6 +1409,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|||||||
self.device.classic_enabled = self.classic
|
self.device.classic_enabled = self.classic
|
||||||
|
|
||||||
# Set up a pairing config factory with minimal requirements.
|
# Set up a pairing config factory with minimal requirements.
|
||||||
|
self.device.config.keystore = "JsonKeyStore"
|
||||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||||
sc=False, mitm=False, bonding=False
|
sc=False, mitm=False, bonding=False
|
||||||
)
|
)
|
||||||
|
|||||||
217
apps/pair.py
217
apps/pair.py
@@ -18,9 +18,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import struct
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from prompt_toolkit.shortcuts import PromptSession
|
from prompt_toolkit.shortcuts import PromptSession
|
||||||
|
|
||||||
|
from bumble.a2dp import make_audio_sink_service_sdp_records
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
@@ -30,16 +33,20 @@ from bumble.smp import error_name as smp_error_name
|
|||||||
from bumble.keys import JsonKeyStore
|
from bumble.keys import JsonKeyStore
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
AdvertisingData,
|
AdvertisingData,
|
||||||
|
Appearance,
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
PhysicalTransport,
|
PhysicalTransport,
|
||||||
|
UUID,
|
||||||
)
|
)
|
||||||
from bumble.gatt import (
|
from bumble.gatt import (
|
||||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||||
GATT_GENERIC_ACCESS_SERVICE,
|
GATT_GENERIC_ACCESS_SERVICE,
|
||||||
|
GATT_HEART_RATE_SERVICE,
|
||||||
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||||
Service,
|
Service,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
CharacteristicValue,
|
|
||||||
)
|
)
|
||||||
|
from bumble.hci import OwnAddressType
|
||||||
from bumble.att import (
|
from bumble.att import (
|
||||||
ATT_Error,
|
ATT_Error,
|
||||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||||
@@ -62,7 +69,7 @@ class Waiter:
|
|||||||
self.linger = linger
|
self.linger = linger
|
||||||
|
|
||||||
def terminate(self):
|
def terminate(self):
|
||||||
if not self.linger:
|
if not self.linger and not self.done.done:
|
||||||
self.done.set_result(None)
|
self.done.set_result(None)
|
||||||
|
|
||||||
async def wait_until_terminated(self):
|
async def wait_until_terminated(self):
|
||||||
@@ -193,7 +200,7 @@ class Delegate(PairingDelegate):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def get_peer_name(peer, mode):
|
async def get_peer_name(peer, mode):
|
||||||
if mode == 'classic':
|
if peer.connection.transport == PhysicalTransport.BR_EDR:
|
||||||
return await peer.request_name()
|
return await peer.request_name()
|
||||||
|
|
||||||
# Try to get the peer name from GATT
|
# Try to get the peer name from GATT
|
||||||
@@ -225,13 +232,14 @@ def read_with_error(connection):
|
|||||||
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
||||||
|
|
||||||
|
|
||||||
def write_with_error(connection, _value):
|
# -----------------------------------------------------------------------------
|
||||||
if not connection.is_encrypted:
|
def sdp_records():
|
||||||
raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR)
|
service_record_handle = 0x00010001
|
||||||
|
return {
|
||||||
if not AUTHENTICATION_ERROR_RETURNED[1]:
|
service_record_handle: make_audio_sink_service_sdp_records(
|
||||||
AUTHENTICATION_ERROR_RETURNED[1] = True
|
service_record_handle
|
||||||
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -239,15 +247,19 @@ def on_connection(connection, request):
|
|||||||
print(color(f'<<< Connection: {connection}', 'green'))
|
print(color(f'<<< Connection: {connection}', 'green'))
|
||||||
|
|
||||||
# Listen for pairing events
|
# Listen for pairing events
|
||||||
connection.on('pairing_start', on_pairing_start)
|
connection.on(connection.EVENT_PAIRING_START, on_pairing_start)
|
||||||
connection.on('pairing', lambda keys: on_pairing(connection, keys))
|
connection.on(connection.EVENT_PAIRING, lambda keys: on_pairing(connection, keys))
|
||||||
connection.on(
|
connection.on(
|
||||||
'pairing_failure', lambda reason: on_pairing_failure(connection, reason)
|
connection.EVENT_CLASSIC_PAIRING, lambda: on_classic_pairing(connection)
|
||||||
|
)
|
||||||
|
connection.on(
|
||||||
|
connection.EVENT_PAIRING_FAILURE,
|
||||||
|
lambda reason: on_pairing_failure(connection, reason),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Listen for encryption changes
|
# Listen for encryption changes
|
||||||
connection.on(
|
connection.on(
|
||||||
'connection_encryption_change',
|
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE,
|
||||||
lambda: on_connection_encryption_change(connection),
|
lambda: on_connection_encryption_change(connection),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -288,6 +300,20 @@ async def on_pairing(connection, keys):
|
|||||||
Waiter.instance.terminate()
|
Waiter.instance.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@AsyncRunner.run_in_task()
|
||||||
|
async def on_classic_pairing(connection):
|
||||||
|
print(color('***-----------------------------------', 'cyan'))
|
||||||
|
print(
|
||||||
|
color(
|
||||||
|
f'*** Paired [Classic]! (peer identity={connection.peer_address})', 'cyan'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(color('***-----------------------------------', 'cyan'))
|
||||||
|
await asyncio.sleep(POST_PAIRING_DELAY)
|
||||||
|
Waiter.instance.terminate()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@AsyncRunner.run_in_task()
|
@AsyncRunner.run_in_task()
|
||||||
async def on_pairing_failure(connection, reason):
|
async def on_pairing_failure(connection, reason):
|
||||||
@@ -305,6 +331,7 @@ async def pair(
|
|||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
ctkd,
|
ctkd,
|
||||||
|
advertising_address,
|
||||||
identity_address,
|
identity_address,
|
||||||
linger,
|
linger,
|
||||||
io,
|
io,
|
||||||
@@ -313,6 +340,8 @@ async def pair(
|
|||||||
request,
|
request,
|
||||||
print_keys,
|
print_keys,
|
||||||
keystore_file,
|
keystore_file,
|
||||||
|
advertise_service_uuids,
|
||||||
|
advertise_appearance,
|
||||||
device_config,
|
device_config,
|
||||||
hci_transport,
|
hci_transport,
|
||||||
address_or_name,
|
address_or_name,
|
||||||
@@ -328,29 +357,33 @@ async def pair(
|
|||||||
|
|
||||||
# Expose a GATT characteristic that can be used to trigger pairing by
|
# Expose a GATT characteristic that can be used to trigger pairing by
|
||||||
# responding with an authentication error when read
|
# responding with an authentication error when read
|
||||||
if mode == 'le':
|
if mode in ('le', 'dual'):
|
||||||
device.le_enabled = True
|
|
||||||
device.add_service(
|
device.add_service(
|
||||||
Service(
|
Service(
|
||||||
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
|
GATT_HEART_RATE_SERVICE,
|
||||||
[
|
[
|
||||||
Characteristic(
|
Characteristic(
|
||||||
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||||
Characteristic.Properties.READ
|
Characteristic.Properties.READ,
|
||||||
| Characteristic.Properties.WRITE,
|
Characteristic.READ_REQUIRES_AUTHENTICATION,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
bytes(1),
|
||||||
CharacteristicValue(
|
|
||||||
read=read_with_error, write=write_with_error
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Select LE or Classic
|
# LE and Classic support
|
||||||
if mode == 'classic':
|
if mode in ('classic', 'dual'):
|
||||||
device.classic_enabled = True
|
device.classic_enabled = True
|
||||||
device.classic_smp_enabled = ctkd
|
device.classic_smp_enabled = ctkd
|
||||||
|
if mode in ('le', 'dual'):
|
||||||
|
device.le_enabled = True
|
||||||
|
if mode == 'dual':
|
||||||
|
device.le_simultaneous_enabled = True
|
||||||
|
|
||||||
|
# Setup SDP
|
||||||
|
if mode in ('classic', 'dual'):
|
||||||
|
device.sdp_service_records = sdp_records()
|
||||||
|
|
||||||
# Get things going
|
# Get things going
|
||||||
await device.power_on()
|
await device.power_on()
|
||||||
@@ -436,13 +469,109 @@ async def pair(
|
|||||||
print(color(f'Pairing failed: {error}', 'red'))
|
print(color(f'Pairing failed: {error}', 'red'))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if mode == 'le':
|
if mode in ('le', 'dual'):
|
||||||
# Advertise so that peers can find us and connect
|
# Advertise so that peers can find us and connect.
|
||||||
await device.start_advertising(auto_restart=True)
|
# Include the heart rate service UUID in the advertisement data
|
||||||
else:
|
# so that devices like iPhones can show this device in their
|
||||||
|
# Bluetooth selector.
|
||||||
|
service_uuids_16 = []
|
||||||
|
service_uuids_32 = []
|
||||||
|
service_uuids_128 = []
|
||||||
|
if advertise_service_uuids:
|
||||||
|
for uuid in advertise_service_uuids:
|
||||||
|
uuid = uuid.replace("-", "")
|
||||||
|
if len(uuid) == 4:
|
||||||
|
service_uuids_16.append(UUID(uuid))
|
||||||
|
elif len(uuid) == 8:
|
||||||
|
service_uuids_32.append(UUID(uuid))
|
||||||
|
elif len(uuid) == 32:
|
||||||
|
service_uuids_128.append(UUID(uuid))
|
||||||
|
else:
|
||||||
|
print(color('Invalid UUID format', 'red'))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
service_uuids_16.append(GATT_HEART_RATE_SERVICE)
|
||||||
|
|
||||||
|
flags = AdvertisingData.Flags.LE_LIMITED_DISCOVERABLE_MODE
|
||||||
|
if mode == 'le':
|
||||||
|
flags |= AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
||||||
|
if mode == 'dual':
|
||||||
|
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||||
|
|
||||||
|
ad_structs = [
|
||||||
|
(
|
||||||
|
AdvertisingData.FLAGS,
|
||||||
|
bytes([flags]),
|
||||||
|
),
|
||||||
|
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
|
||||||
|
]
|
||||||
|
if service_uuids_16:
|
||||||
|
ad_structs.append(
|
||||||
|
(
|
||||||
|
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||||
|
b"".join(bytes(uuid) for uuid in service_uuids_16),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if service_uuids_32:
|
||||||
|
ad_structs.append(
|
||||||
|
(
|
||||||
|
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||||
|
b"".join(bytes(uuid) for uuid in service_uuids_32),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if service_uuids_128:
|
||||||
|
ad_structs.append(
|
||||||
|
(
|
||||||
|
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||||
|
b"".join(bytes(uuid) for uuid in service_uuids_128),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if advertise_appearance:
|
||||||
|
advertise_appearance = advertise_appearance.upper()
|
||||||
|
try:
|
||||||
|
advertise_appearance_int = int(advertise_appearance)
|
||||||
|
except ValueError:
|
||||||
|
category, subcategory = advertise_appearance.split('/')
|
||||||
|
try:
|
||||||
|
category_enum = Appearance.Category[category]
|
||||||
|
except ValueError:
|
||||||
|
print(
|
||||||
|
color(f'Invalid appearance category {category}', 'red')
|
||||||
|
)
|
||||||
|
return
|
||||||
|
subcategory_class = Appearance.SUBCATEGORY_CLASSES[
|
||||||
|
category_enum
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
subcategory_enum = subcategory_class[subcategory]
|
||||||
|
except ValueError:
|
||||||
|
print(color(f'Invalid subcategory {subcategory}', 'red'))
|
||||||
|
return
|
||||||
|
advertise_appearance_int = int(
|
||||||
|
Appearance(category_enum, subcategory_enum)
|
||||||
|
)
|
||||||
|
ad_structs.append(
|
||||||
|
(
|
||||||
|
AdvertisingData.APPEARANCE,
|
||||||
|
struct.pack('<H', advertise_appearance_int),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.advertising_data = bytes(AdvertisingData(ad_structs))
|
||||||
|
await device.start_advertising(
|
||||||
|
auto_restart=True,
|
||||||
|
own_address_type=(
|
||||||
|
OwnAddressType.PUBLIC
|
||||||
|
if advertising_address == 'public'
|
||||||
|
else OwnAddressType.RANDOM
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if mode in ('classic', 'dual'):
|
||||||
# Become discoverable and connectable
|
# Become discoverable and connectable
|
||||||
await device.set_discoverable(True)
|
await device.set_discoverable(True)
|
||||||
await device.set_connectable(True)
|
await device.set_connectable(True)
|
||||||
|
print(color('Ready for connections on', 'blue'), device.public_address)
|
||||||
|
|
||||||
# Run until the user asks to exit
|
# Run until the user asks to exit
|
||||||
await Waiter.instance.wait_until_terminated()
|
await Waiter.instance.wait_until_terminated()
|
||||||
@@ -462,7 +591,10 @@ class LogHandler(logging.Handler):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
'--mode', type=click.Choice(['le', 'classic']), default='le', show_default=True
|
'--mode',
|
||||||
|
type=click.Choice(['le', 'classic', 'dual']),
|
||||||
|
default='le',
|
||||||
|
show_default=True,
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--sc',
|
'--sc',
|
||||||
@@ -484,6 +616,10 @@ class LogHandler(logging.Handler):
|
|||||||
help='Enable CTKD',
|
help='Enable CTKD',
|
||||||
show_default=True,
|
show_default=True,
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--advertising-address',
|
||||||
|
type=click.Choice(['random', 'public']),
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--identity-address',
|
'--identity-address',
|
||||||
type=click.Choice(['random', 'public']),
|
type=click.Choice(['random', 'public']),
|
||||||
@@ -512,9 +648,20 @@ class LogHandler(logging.Handler):
|
|||||||
@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing')
|
@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing')
|
||||||
@click.option(
|
@click.option(
|
||||||
'--keystore-file',
|
'--keystore-file',
|
||||||
metavar='<filename>',
|
metavar='FILENAME',
|
||||||
help='File in which to store the pairing keys',
|
help='File in which to store the pairing keys',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--advertise-service-uuid',
|
||||||
|
metavar="UUID",
|
||||||
|
multiple=True,
|
||||||
|
help="Advertise a GATT service UUID (may be specified more than once)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--advertise-appearance',
|
||||||
|
metavar='APPEARANCE',
|
||||||
|
help='Advertise an Appearance ID (int value or string)',
|
||||||
|
)
|
||||||
@click.argument('device-config')
|
@click.argument('device-config')
|
||||||
@click.argument('hci_transport')
|
@click.argument('hci_transport')
|
||||||
@click.argument('address-or-name', required=False)
|
@click.argument('address-or-name', required=False)
|
||||||
@@ -524,6 +671,7 @@ def main(
|
|||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
ctkd,
|
ctkd,
|
||||||
|
advertising_address,
|
||||||
identity_address,
|
identity_address,
|
||||||
linger,
|
linger,
|
||||||
io,
|
io,
|
||||||
@@ -532,6 +680,8 @@ def main(
|
|||||||
request,
|
request,
|
||||||
print_keys,
|
print_keys,
|
||||||
keystore_file,
|
keystore_file,
|
||||||
|
advertise_service_uuid,
|
||||||
|
advertise_appearance,
|
||||||
device_config,
|
device_config,
|
||||||
hci_transport,
|
hci_transport,
|
||||||
address_or_name,
|
address_or_name,
|
||||||
@@ -550,6 +700,7 @@ def main(
|
|||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
ctkd,
|
ctkd,
|
||||||
|
advertising_address,
|
||||||
identity_address,
|
identity_address,
|
||||||
linger,
|
linger,
|
||||||
io,
|
io,
|
||||||
@@ -558,6 +709,8 @@ def main(
|
|||||||
request,
|
request,
|
||||||
print_keys,
|
print_keys,
|
||||||
keystore_file,
|
keystore_file,
|
||||||
|
advertise_service_uuid,
|
||||||
|
advertise_appearance,
|
||||||
device_config,
|
device_config,
|
||||||
hci_transport,
|
hci_transport,
|
||||||
address_or_name,
|
address_or_name,
|
||||||
|
|||||||
@@ -809,7 +809,7 @@ class Appearance:
|
|||||||
STICK_PC = 0x0F
|
STICK_PC = 0x0F
|
||||||
|
|
||||||
class WatchSubcategory(utils.OpenIntEnum):
|
class WatchSubcategory(utils.OpenIntEnum):
|
||||||
GENENERIC_WATCH = 0x00
|
GENERIC_WATCH = 0x00
|
||||||
SPORTS_WATCH = 0x01
|
SPORTS_WATCH = 0x01
|
||||||
SMARTWATCH = 0x02
|
SMARTWATCH = 0x02
|
||||||
|
|
||||||
@@ -1127,7 +1127,7 @@ class Appearance:
|
|||||||
TURNTABLE = 0x05
|
TURNTABLE = 0x05
|
||||||
CD_PLAYER = 0x06
|
CD_PLAYER = 0x06
|
||||||
DVD_PLAYER = 0x07
|
DVD_PLAYER = 0x07
|
||||||
BLUERAY_PLAYER = 0x08
|
BLURAY_PLAYER = 0x08
|
||||||
OPTICAL_DISC_PLAYER = 0x09
|
OPTICAL_DISC_PLAYER = 0x09
|
||||||
SET_TOP_BOX = 0x0A
|
SET_TOP_BOX = 0x0A
|
||||||
|
|
||||||
@@ -1351,6 +1351,12 @@ class AdvertisingData:
|
|||||||
THREE_D_INFORMATION_DATA = 0x3D
|
THREE_D_INFORMATION_DATA = 0x3D
|
||||||
MANUFACTURER_SPECIFIC_DATA = 0xFF
|
MANUFACTURER_SPECIFIC_DATA = 0xFF
|
||||||
|
|
||||||
|
class Flags(enum.IntFlag):
|
||||||
|
LE_LIMITED_DISCOVERABLE_MODE = 1 << 0
|
||||||
|
LE_GENERAL_DISCOVERABLE_MODE = 1 << 1
|
||||||
|
BR_EDR_NOT_SUPPORTED = 1 << 2
|
||||||
|
SIMULTANEOUS_LE_BR_EDR_CAPABLE = 1 << 3
|
||||||
|
|
||||||
# For backward-compatibility
|
# For backward-compatibility
|
||||||
FLAGS = Type.FLAGS
|
FLAGS = Type.FLAGS
|
||||||
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = Type.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS
|
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = Type.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS
|
||||||
@@ -1407,11 +1413,11 @@ class AdvertisingData:
|
|||||||
THREE_D_INFORMATION_DATA = Type.THREE_D_INFORMATION_DATA
|
THREE_D_INFORMATION_DATA = Type.THREE_D_INFORMATION_DATA
|
||||||
MANUFACTURER_SPECIFIC_DATA = Type.MANUFACTURER_SPECIFIC_DATA
|
MANUFACTURER_SPECIFIC_DATA = Type.MANUFACTURER_SPECIFIC_DATA
|
||||||
|
|
||||||
LE_LIMITED_DISCOVERABLE_MODE_FLAG = 0x01
|
LE_LIMITED_DISCOVERABLE_MODE_FLAG = Flags.LE_LIMITED_DISCOVERABLE_MODE
|
||||||
LE_GENERAL_DISCOVERABLE_MODE_FLAG = 0x02
|
LE_GENERAL_DISCOVERABLE_MODE_FLAG = Flags.LE_GENERAL_DISCOVERABLE_MODE
|
||||||
BR_EDR_NOT_SUPPORTED_FLAG = 0x04
|
BR_EDR_NOT_SUPPORTED_FLAG = Flags.BR_EDR_NOT_SUPPORTED
|
||||||
BR_EDR_CONTROLLER_FLAG = 0x08
|
BR_EDR_CONTROLLER_FLAG = Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||||
BR_EDR_HOST_FLAG = 0x10
|
BR_EDR_HOST_FLAG = 0x10 # Deprecated
|
||||||
|
|
||||||
ad_structures: list[tuple[int, bytes]]
|
ad_structures: list[tuple[int, bytes]]
|
||||||
|
|
||||||
|
|||||||
@@ -1586,9 +1586,10 @@ class Connection(utils.CompositeEventEmitter):
|
|||||||
peer_le_features: Optional[hci.LeFeatureMask]
|
peer_le_features: Optional[hci.LeFeatureMask]
|
||||||
role: hci.Role
|
role: hci.Role
|
||||||
encryption: int
|
encryption: int
|
||||||
|
encryption_key_size: int
|
||||||
authenticated: bool
|
authenticated: bool
|
||||||
sc: bool
|
sc: bool
|
||||||
link_key_type: Optional[int]
|
link_key_type: Optional[int] # [Classic only]
|
||||||
gatt_client: gatt_client.Client
|
gatt_client: gatt_client.Client
|
||||||
pairing_peer_io_capability: Optional[int]
|
pairing_peer_io_capability: Optional[int]
|
||||||
pairing_peer_authentication_requirements: Optional[int]
|
pairing_peer_authentication_requirements: Optional[int]
|
||||||
@@ -1688,6 +1689,7 @@ class Connection(utils.CompositeEventEmitter):
|
|||||||
self.role = role
|
self.role = role
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
self.encryption = 0
|
self.encryption = 0
|
||||||
|
self.encryption_key_size = 0
|
||||||
self.authenticated = False
|
self.authenticated = False
|
||||||
self.sc = False
|
self.sc = False
|
||||||
self.link_key_type = None
|
self.link_key_type = None
|
||||||
@@ -1809,7 +1811,7 @@ class Connection(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(
|
await asyncio.wait_for(
|
||||||
utils.cancel_on_event(self.device, 'flush', abort), timeout
|
utils.cancel_on_event(self.device, Device.EVENT_FLUSH, abort), timeout
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self.remove_listener(self.EVENT_DISCONNECTION, abort.set_result)
|
self.remove_listener(self.EVENT_DISCONNECTION, abort.set_result)
|
||||||
@@ -3756,7 +3758,9 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
self.le_connecting = True
|
self.le_connecting = True
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
return await utils.cancel_on_event(self, 'flush', pending_connection)
|
return await utils.cancel_on_event(
|
||||||
|
self, Device.EVENT_FLUSH, pending_connection
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await asyncio.wait_for(
|
return await asyncio.wait_for(
|
||||||
@@ -3774,7 +3778,7 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return await utils.cancel_on_event(
|
return await utils.cancel_on_event(
|
||||||
self, 'flush', pending_connection
|
self, Device.EVENT_FLUSH, pending_connection
|
||||||
)
|
)
|
||||||
except core.ConnectionError as error:
|
except core.ConnectionError as error:
|
||||||
raise core.TimeoutError() from error
|
raise core.TimeoutError() from error
|
||||||
@@ -3831,7 +3835,9 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Wait for a request or a completed connection
|
# Wait for a request or a completed connection
|
||||||
pending_request = utils.cancel_on_event(self, 'flush', pending_request_fut)
|
pending_request = utils.cancel_on_event(
|
||||||
|
self, Device.EVENT_FLUSH, pending_request_fut
|
||||||
|
)
|
||||||
result = await (
|
result = await (
|
||||||
asyncio.wait_for(pending_request, timeout)
|
asyncio.wait_for(pending_request, timeout)
|
||||||
if timeout
|
if timeout
|
||||||
@@ -3893,7 +3899,9 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Wait for connection complete
|
# Wait for connection complete
|
||||||
return await utils.cancel_on_event(self, 'flush', pending_connection)
|
return await utils.cancel_on_event(
|
||||||
|
self, Device.EVENT_FLUSH, pending_connection
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.remove_listener(self.EVENT_CONNECTION, on_connection)
|
self.remove_listener(self.EVENT_CONNECTION, on_connection)
|
||||||
@@ -3969,7 +3977,9 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
# Wait for the disconnection process to complete
|
# Wait for the disconnection process to complete
|
||||||
self.disconnecting = True
|
self.disconnecting = True
|
||||||
return await utils.cancel_on_event(self, 'flush', pending_disconnection)
|
return await utils.cancel_on_event(
|
||||||
|
self, Device.EVENT_FLUSH, pending_disconnection
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
connection.remove_listener(
|
connection.remove_listener(
|
||||||
connection.EVENT_DISCONNECTION, pending_disconnection.set_result
|
connection.EVENT_DISCONNECTION, pending_disconnection.set_result
|
||||||
@@ -4193,7 +4203,7 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await utils.cancel_on_event(self, 'flush', peer_address)
|
return await utils.cancel_on_event(self, Device.EVENT_FLUSH, peer_address)
|
||||||
finally:
|
finally:
|
||||||
if listener is not None:
|
if listener is not None:
|
||||||
self.remove_listener(event_name, listener)
|
self.remove_listener(event_name, listener)
|
||||||
@@ -4243,7 +4253,7 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
if not self.scanning:
|
if not self.scanning:
|
||||||
await self.start_scanning(filter_duplicates=True)
|
await self.start_scanning(filter_duplicates=True)
|
||||||
|
|
||||||
return await utils.cancel_on_event(self, 'flush', peer_address)
|
return await utils.cancel_on_event(self, Device.EVENT_FLUSH, peer_address)
|
||||||
finally:
|
finally:
|
||||||
if listener is not None:
|
if listener is not None:
|
||||||
self.remove_listener(event_name, listener)
|
self.remove_listener(event_name, listener)
|
||||||
@@ -4351,7 +4361,7 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
# Wait for the authentication to complete
|
# Wait for the authentication to complete
|
||||||
await utils.cancel_on_event(
|
await utils.cancel_on_event(
|
||||||
connection, 'disconnection', pending_authentication
|
connection, Connection.EVENT_DISCONNECTION, pending_authentication
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
connection.remove_listener(
|
connection.remove_listener(
|
||||||
@@ -4439,7 +4449,9 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
raise hci.HCI_StatusError(result)
|
raise hci.HCI_StatusError(result)
|
||||||
|
|
||||||
# Wait for the result
|
# Wait for the result
|
||||||
await utils.cancel_on_event(connection, 'disconnection', pending_encryption)
|
await utils.cancel_on_event(
|
||||||
|
connection, Connection.EVENT_DISCONNECTION, pending_encryption
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
connection.remove_listener(
|
connection.remove_listener(
|
||||||
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
|
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
|
||||||
@@ -4484,7 +4496,7 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
raise hci.HCI_StatusError(result)
|
raise hci.HCI_StatusError(result)
|
||||||
await utils.cancel_on_event(
|
await utils.cancel_on_event(
|
||||||
connection, 'disconnection', pending_role_change
|
connection, Connection.EVENT_DISCONNECTION, pending_role_change
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
|
connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
|
||||||
@@ -4536,7 +4548,7 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
raise hci.HCI_StatusError(result)
|
raise hci.HCI_StatusError(result)
|
||||||
|
|
||||||
# Wait for the result
|
# Wait for the result
|
||||||
return await utils.cancel_on_event(self, 'flush', pending_name)
|
return await utils.cancel_on_event(self, Device.EVENT_FLUSH, pending_name)
|
||||||
finally:
|
finally:
|
||||||
self.remove_listener(self.EVENT_REMOTE_NAME, handler)
|
self.remove_listener(self.EVENT_REMOTE_NAME, handler)
|
||||||
self.remove_listener(self.EVENT_REMOTE_NAME_FAILURE, failure_handler)
|
self.remove_listener(self.EVENT_REMOTE_NAME_FAILURE, failure_handler)
|
||||||
@@ -5069,13 +5081,14 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
utils.cancel_on_event(
|
utils.cancel_on_event(
|
||||||
self, 'flush', self.update_keys(str(bd_addr), pairing_keys)
|
self, Device.EVENT_FLUSH, self.update_keys(str(bd_addr), pairing_keys)
|
||||||
)
|
)
|
||||||
|
|
||||||
if connection := self.find_connection_by_bd_addr(
|
if connection := self.find_connection_by_bd_addr(
|
||||||
bd_addr, transport=PhysicalTransport.BR_EDR
|
bd_addr, transport=PhysicalTransport.BR_EDR
|
||||||
):
|
):
|
||||||
connection.link_key_type = key_type
|
connection.link_key_type = key_type
|
||||||
|
connection.emit(connection.EVENT_LINK_KEY)
|
||||||
|
|
||||||
def add_service(self, service):
|
def add_service(self, service):
|
||||||
self.gatt_server.add_service(service)
|
self.gatt_server.add_service(service)
|
||||||
@@ -5338,8 +5351,10 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
# Setup auto-restart of the advertising set if needed.
|
# Setup auto-restart of the advertising set if needed.
|
||||||
if advertising_set.auto_restart:
|
if advertising_set.auto_restart:
|
||||||
connection.once(
|
connection.once(
|
||||||
'disconnection',
|
Connection.EVENT_DISCONNECTION,
|
||||||
lambda _: utils.cancel_on_event(self, 'flush', advertising_set.start()),
|
lambda _: utils.cancel_on_event(
|
||||||
|
self, Device.EVENT_FLUSH, advertising_set.start()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.emit(self.EVENT_CONNECTION, connection)
|
self.emit(self.EVENT_CONNECTION, connection)
|
||||||
@@ -5453,8 +5468,10 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
if self.legacy_advertiser.auto_restart:
|
if self.legacy_advertiser.auto_restart:
|
||||||
advertiser = self.legacy_advertiser
|
advertiser = self.legacy_advertiser
|
||||||
connection.once(
|
connection.once(
|
||||||
'disconnection',
|
Connection.EVENT_DISCONNECTION,
|
||||||
lambda _: utils.cancel_on_event(self, 'flush', advertiser.start()),
|
lambda _: utils.cancel_on_event(
|
||||||
|
self, Device.EVENT_FLUSH, advertiser.start()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.legacy_advertiser = None
|
self.legacy_advertiser = None
|
||||||
@@ -5713,7 +5730,9 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
async def reply() -> None:
|
async def reply() -> None:
|
||||||
try:
|
try:
|
||||||
if await utils.cancel_on_event(connection, 'disconnection', method()):
|
if await utils.cancel_on_event(
|
||||||
|
connection, Connection.EVENT_DISCONNECTION, method()
|
||||||
|
):
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
hci.HCI_User_Confirmation_Request_Reply_Command(
|
hci.HCI_User_Confirmation_Request_Reply_Command(
|
||||||
bd_addr=connection.peer_address
|
bd_addr=connection.peer_address
|
||||||
@@ -5741,7 +5760,9 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
async def reply() -> None:
|
async def reply() -> None:
|
||||||
try:
|
try:
|
||||||
number = await utils.cancel_on_event(
|
number = await utils.cancel_on_event(
|
||||||
connection, 'disconnection', pairing_config.delegate.get_number()
|
connection,
|
||||||
|
Connection.EVENT_DISCONNECTION,
|
||||||
|
pairing_config.delegate.get_number(),
|
||||||
)
|
)
|
||||||
if number is not None:
|
if number is not None:
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
@@ -5775,7 +5796,9 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
# Ask the user to enter a string
|
# Ask the user to enter a string
|
||||||
async def get_pin_code():
|
async def get_pin_code():
|
||||||
pin_code = await utils.cancel_on_event(
|
pin_code = await utils.cancel_on_event(
|
||||||
connection, 'disconnection', pairing_config.delegate.get_string(16)
|
connection,
|
||||||
|
Connection.EVENT_DISCONNECTION,
|
||||||
|
pairing_config.delegate.get_string(16),
|
||||||
)
|
)
|
||||||
|
|
||||||
if pin_code is not None:
|
if pin_code is not None:
|
||||||
@@ -5814,7 +5837,9 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
# Show the passkey to the user
|
# Show the passkey to the user
|
||||||
utils.cancel_on_event(
|
utils.cancel_on_event(
|
||||||
connection, 'disconnection', pairing_config.delegate.display_number(passkey)
|
connection,
|
||||||
|
Connection.EVENT_DISCONNECTION,
|
||||||
|
pairing_config.delegate.display_number(passkey, digits=6),
|
||||||
)
|
)
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@@ -5950,13 +5975,17 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_encryption_change(self, connection, encryption):
|
def on_connection_encryption_change(
|
||||||
|
self, connection, encryption, encryption_key_size
|
||||||
|
):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
|
f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
|
||||||
f'{connection.peer_address} as {connection.role_name}, '
|
f'{connection.peer_address} as {connection.role_name}, '
|
||||||
f'encryption={encryption}'
|
f'encryption={encryption}, '
|
||||||
|
f'key_size={encryption_key_size}'
|
||||||
)
|
)
|
||||||
connection.encryption = encryption
|
connection.encryption = encryption
|
||||||
|
connection.encryption_key_size = encryption_key_size
|
||||||
if (
|
if (
|
||||||
not connection.authenticated
|
not connection.authenticated
|
||||||
and connection.transport == PhysicalTransport.BR_EDR
|
and connection.transport == PhysicalTransport.BR_EDR
|
||||||
|
|||||||
@@ -29,13 +29,12 @@ from typing_extensions import Self
|
|||||||
from bumble import crypto
|
from bumble import crypto
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
PhysicalTransport,
|
|
||||||
AdvertisingData,
|
AdvertisingData,
|
||||||
DeviceClass,
|
DeviceClass,
|
||||||
InvalidArgumentError,
|
InvalidArgumentError,
|
||||||
InvalidPacketError,
|
InvalidPacketError,
|
||||||
ProtocolError,
|
|
||||||
PhysicalTransport,
|
PhysicalTransport,
|
||||||
|
ProtocolError,
|
||||||
bit_flags_to_strings,
|
bit_flags_to_strings,
|
||||||
name_or_number,
|
name_or_number,
|
||||||
padded_bytes,
|
padded_bytes,
|
||||||
@@ -225,6 +224,7 @@ HCI_CONNECTIONLESS_PERIPHERAL_BROADCAST_CHANNEL_MAP_CHANGE_EVENT = 0X55
|
|||||||
HCI_INQUIRY_RESPONSE_NOTIFICATION_EVENT = 0X56
|
HCI_INQUIRY_RESPONSE_NOTIFICATION_EVENT = 0X56
|
||||||
HCI_AUTHENTICATED_PAYLOAD_TIMEOUT_EXPIRED_EVENT = 0X57
|
HCI_AUTHENTICATED_PAYLOAD_TIMEOUT_EXPIRED_EVENT = 0X57
|
||||||
HCI_SAM_STATUS_CHANGE_EVENT = 0X58
|
HCI_SAM_STATUS_CHANGE_EVENT = 0X58
|
||||||
|
HCI_ENCRYPTION_CHANGE_V2_EVENT = 0x59
|
||||||
|
|
||||||
HCI_VENDOR_EVENT = 0xFF
|
HCI_VENDOR_EVENT = 0xFF
|
||||||
|
|
||||||
@@ -3364,6 +3364,20 @@ class HCI_Set_Event_Mask_Page_2_Command(HCI_Command):
|
|||||||
See Bluetooth spec @ 7.3.69 Set Event Mask Page 2 Command
|
See Bluetooth spec @ 7.3.69 Set Event Mask Page 2 Command
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mask(event_codes: Iterable[int]) -> bytes:
|
||||||
|
'''
|
||||||
|
Compute the event mask value for a list of events.
|
||||||
|
'''
|
||||||
|
# NOTE: this implementation takes advantage of the fact that as of version 6.0
|
||||||
|
# of the core specification, the bit number for each event code is equal to 64
|
||||||
|
# less than the event code.
|
||||||
|
# If future versions of the specification deviate from that, a different
|
||||||
|
# implementation would be needed.
|
||||||
|
return sum((1 << event_code - 64) for event_code in event_codes).to_bytes(
|
||||||
|
8, 'little'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(
|
@HCI_Command.command(
|
||||||
@@ -6977,6 +6991,30 @@ class HCI_Encryption_Change_Event(HCI_Event):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Event.event(
|
||||||
|
[
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('connection_handle', 2),
|
||||||
|
(
|
||||||
|
'encryption_enabled',
|
||||||
|
{
|
||||||
|
'size': 1,
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
|
'mapper': lambda x: HCI_Encryption_Change_Event.encryption_enabled_name(
|
||||||
|
x
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
('encryption_key_size', 1),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class HCI_Encryption_Change_V2_Event(HCI_Event):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.7.8 Encryption Change Event
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Event.event(
|
@HCI_Event.event(
|
||||||
[('status', STATUS_SPEC), ('connection_handle', 2), ('lmp_features', 8)]
|
[('status', STATUS_SPEC), ('connection_handle', 2), ('lmp_features', 8)]
|
||||||
|
|||||||
@@ -435,6 +435,14 @@ class Host(utils.EventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if self.supports_command(hci.HCI_SET_EVENT_MASK_PAGE_2_COMMAND):
|
||||||
|
await self.send_command(
|
||||||
|
hci.HCI_Set_Event_Mask_Page_2_Command(
|
||||||
|
event_mask_page_2=hci.HCI_Set_Event_Mask_Page_2_Command.mask(
|
||||||
|
[hci.HCI_ENCRYPTION_CHANGE_V2_EVENT]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.local_version is not None
|
self.local_version is not None
|
||||||
@@ -1384,6 +1392,21 @@ class Host(utils.EventEmitter):
|
|||||||
'connection_encryption_change',
|
'connection_encryption_change',
|
||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
event.encryption_enabled,
|
event.encryption_enabled,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.emit(
|
||||||
|
'connection_encryption_failure', event.connection_handle, event.status
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_hci_encryption_change_v2_event(self, event):
|
||||||
|
# Notify the client
|
||||||
|
if event.status == hci.HCI_SUCCESS:
|
||||||
|
self.emit(
|
||||||
|
'connection_encryption_change',
|
||||||
|
event.connection_handle,
|
||||||
|
event.encryption_enabled,
|
||||||
|
event.encryption_key_size,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.emit(
|
self.emit(
|
||||||
|
|||||||
@@ -33,12 +33,6 @@ from bumble.avdtp import (
|
|||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
make_audio_sink_service_sdp_records,
|
make_audio_sink_service_sdp_records,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
SBC_MONO_CHANNEL_MODE,
|
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user