Merge pull request #383 from zxzxwu/controller

Controller: SCO implementation
This commit is contained in:
zxzxwu
2024-01-09 09:39:13 +08:00
committed by GitHub
8 changed files with 274 additions and 34 deletions

View File

@@ -33,6 +33,7 @@
"dhkey", "dhkey",
"diversifier", "diversifier",
"endianness", "endianness",
"ESCO",
"Fitbit", "Fitbit",
"GATTLINK", "GATTLINK",
"HANDSFREE", "HANDSFREE",

View File

@@ -19,6 +19,7 @@ from __future__ import annotations
import logging import logging
import asyncio import asyncio
import dataclasses
import itertools import itertools
import random import random
import struct import struct
@@ -42,6 +43,7 @@ from bumble.hci import (
HCI_LE_1M_PHY, HCI_LE_1M_PHY,
HCI_SUCCESS, HCI_SUCCESS,
HCI_UNKNOWN_HCI_COMMAND_ERROR, HCI_UNKNOWN_HCI_COMMAND_ERROR,
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR, HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
HCI_VERSION_BLUETOOTH_CORE_5_0, HCI_VERSION_BLUETOOTH_CORE_5_0,
Address, Address,
@@ -53,6 +55,7 @@ from bumble.hci import (
HCI_Connection_Request_Event, HCI_Connection_Request_Event,
HCI_Disconnection_Complete_Event, HCI_Disconnection_Complete_Event,
HCI_Encryption_Change_Event, HCI_Encryption_Change_Event,
HCI_Synchronous_Connection_Complete_Event,
HCI_LE_Advertising_Report_Event, HCI_LE_Advertising_Report_Event,
HCI_LE_Connection_Complete_Event, HCI_LE_Connection_Complete_Event,
HCI_LE_Read_Remote_Features_Complete_Event, HCI_LE_Read_Remote_Features_Complete_Event,
@@ -60,10 +63,11 @@ from bumble.hci import (
HCI_Packet, HCI_Packet,
HCI_Role_Change_Event, HCI_Role_Change_Event,
) )
from typing import Optional, Union, Dict, TYPE_CHECKING from typing import Optional, Union, Dict, Any, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.transport.common import TransportSink, TransportSource from bumble.link import LocalLink
from bumble.transport.common import TransportSink
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -79,15 +83,18 @@ class DataObject:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@dataclasses.dataclass
class Connection: class Connection:
def __init__(self, controller, handle, role, peer_address, link, transport): controller: Controller
self.controller = controller handle: int
self.handle = handle role: int
self.role = role peer_address: Address
self.peer_address = peer_address link: Any
self.link = link transport: int
link_type: int
def __post_init__(self):
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu) self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
self.transport = transport
def on_hci_acl_data_packet(self, packet): def on_hci_acl_data_packet(self, packet):
self.assembler.feed_packet(packet) self.assembler.feed_packet(packet)
@@ -106,10 +113,10 @@ class Connection:
class Controller: class Controller:
def __init__( def __init__(
self, self,
name, name: str,
host_source=None, host_source=None,
host_sink: Optional[TransportSink] = None, host_sink: Optional[TransportSink] = None,
link=None, link: Optional[LocalLink] = None,
public_address: Optional[Union[bytes, str, Address]] = None, public_address: Optional[Union[bytes, str, Address]] = None,
): ):
self.name = name self.name = name
@@ -359,12 +366,13 @@ class Controller:
if connection is None: if connection is None:
connection_handle = self.allocate_connection_handle() connection_handle = self.allocate_connection_handle()
connection = Connection( connection = Connection(
self, controller=self,
connection_handle, handle=connection_handle,
BT_PERIPHERAL_ROLE, role=BT_PERIPHERAL_ROLE,
peer_address, peer_address=peer_address,
self.link, link=self.link,
BT_LE_TRANSPORT, transport=BT_LE_TRANSPORT,
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
) )
self.peripheral_connections[peer_address] = connection self.peripheral_connections[peer_address] = connection
logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}') logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}')
@@ -418,12 +426,13 @@ class Controller:
if connection is None: if connection is None:
connection_handle = self.allocate_connection_handle() connection_handle = self.allocate_connection_handle()
connection = Connection( connection = Connection(
self, controller=self,
connection_handle, handle=connection_handle,
BT_CENTRAL_ROLE, role=BT_CENTRAL_ROLE,
peer_address, peer_address=peer_address,
self.link, link=self.link,
BT_LE_TRANSPORT, transport=BT_LE_TRANSPORT,
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
) )
self.central_connections[peer_address] = connection self.central_connections[peer_address] = connection
logger.debug( logger.debug(
@@ -568,6 +577,7 @@ class Controller:
peer_address=peer_address, peer_address=peer_address,
link=self.link, link=self.link,
transport=BT_BR_EDR_TRANSPORT, transport=BT_BR_EDR_TRANSPORT,
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
) )
self.classic_connections[peer_address] = connection self.classic_connections[peer_address] = connection
logger.debug( logger.debug(
@@ -621,6 +631,42 @@ class Controller:
) )
) )
def on_classic_sco_connection_complete(
self, peer_address: Address, status: int, link_type: int
):
if status == HCI_SUCCESS:
# Allocate (or reuse) a connection handle
connection_handle = self.allocate_connection_handle()
connection = Connection(
controller=self,
handle=connection_handle,
# Role doesn't matter in SCO.
role=BT_CENTRAL_ROLE,
peer_address=peer_address,
link=self.link,
transport=BT_BR_EDR_TRANSPORT,
link_type=link_type,
)
self.classic_connections[peer_address] = connection
logger.debug(f'New SCO connection handle: 0x{connection_handle:04X}')
else:
connection_handle = 0
self.send_hci_packet(
HCI_Synchronous_Connection_Complete_Event(
status=status,
connection_handle=connection_handle,
bd_addr=peer_address,
link_type=link_type,
# TODO: Provide SCO connection parameters.
transmission_interval=0,
retransmission_window=0,
rx_packet_length=0,
tx_packet_length=0,
air_mode=0,
)
)
############################################################ ############################################################
# Advertising support # Advertising support
############################################################ ############################################################
@@ -740,6 +786,68 @@ class Controller:
) )
self.link.classic_accept_connection(self, command.bd_addr, command.role) self.link.classic_accept_connection(self, command.bd_addr, command.role)
def on_hci_enhanced_setup_synchronous_connection_command(self, command):
'''
See Bluetooth spec Vol 4, Part E - 7.1.45 Enhanced Setup Synchronous Connection command
'''
if self.link is None:
return
if not (
connection := self.find_classic_connection_by_handle(
command.connection_handle
)
):
self.send_hci_packet(
HCI_Command_Status_Event(
status=HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
return
self.send_hci_packet(
HCI_Command_Status_Event(
status=HCI_SUCCESS,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
self.link.classic_sco_connect(
self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
)
def on_hci_enhanced_accept_synchronous_connection_request_command(self, command):
'''
See Bluetooth spec Vol 4, Part E - 7.1.46 Enhanced Accept Synchronous Connection Request command
'''
if self.link is None:
return
if not (connection := self.find_classic_connection_by_address(command.bd_addr)):
self.send_hci_packet(
HCI_Command_Status_Event(
status=HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
return
self.send_hci_packet(
HCI_Command_Status_Event(
status=HCI_SUCCESS,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
self.link.classic_accept_sco_connection(
self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
)
def on_hci_switch_role_command(self, command): def on_hci_switch_role_command(self, command):
''' '''
See Bluetooth spec Vol 4, Part E - 7.2.8 Switch Role command See Bluetooth spec Vol 4, Part E - 7.2.8 Switch Role command

View File

@@ -61,7 +61,6 @@ from .hci import (
HCI_LE_1M_PHY_BIT, HCI_LE_1M_PHY_BIT,
HCI_LE_2M_PHY, HCI_LE_2M_PHY,
HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE, HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE,
HCI_LE_CLEAR_RESOLVING_LIST_COMMAND,
HCI_LE_CODED_PHY, HCI_LE_CODED_PHY,
HCI_LE_CODED_PHY_BIT, HCI_LE_CODED_PHY_BIT,
HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE, HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE,
@@ -86,7 +85,7 @@ from .hci import (
HCI_Constant, HCI_Constant,
HCI_Create_Connection_Cancel_Command, HCI_Create_Connection_Cancel_Command,
HCI_Create_Connection_Command, HCI_Create_Connection_Command,
HCI_Create_Connection_Command, HCI_Connection_Complete_Event,
HCI_Disconnect_Command, HCI_Disconnect_Command,
HCI_Encryption_Change_Event, HCI_Encryption_Change_Event,
HCI_Error, HCI_Error,
@@ -3319,8 +3318,21 @@ class Device(CompositeEventEmitter):
def on_connection_request(self, bd_addr, class_of_device, link_type): def on_connection_request(self, bd_addr, class_of_device, link_type):
logger.debug(f'*** Connection request: {bd_addr}') logger.debug(f'*** Connection request: {bd_addr}')
# Handle SCO request.
if link_type in (
HCI_Connection_Complete_Event.SCO_LINK_TYPE,
HCI_Connection_Complete_Event.ESCO_LINK_TYPE,
):
if connection := self.find_connection_by_bd_addr(
bd_addr, transport=BT_BR_EDR_TRANSPORT
):
self.emit('sco_request', connection, link_type)
else:
logger.error(f'SCO request from a non-connected device {bd_addr}')
return
# match a pending future using `bd_addr` # match a pending future using `bd_addr`
if bd_addr in self.classic_pending_accepts: elif bd_addr in self.classic_pending_accepts:
future, *_ = self.classic_pending_accepts.pop(bd_addr) future, *_ = self.classic_pending_accepts.pop(bd_addr)
future.set_result((bd_addr, class_of_device, link_type)) future.set_result((bd_addr, class_of_device, link_type))

View File

@@ -4765,7 +4765,11 @@ class HCI_Event(HCI_Packet):
HCI_Object.init_from_bytes(self, parameters, 0, fields) HCI_Object.init_from_bytes(self, parameters, 0, fields)
return self return self
def __init__(self, event_code, parameters=None, **kwargs): def __init__(self, event_code=-1, parameters=None, **kwargs):
# Since the legacy implementation relies on an __init__ injector, typing always
# complains that positional argument event_code is not passed, so here sets a
# default value to allow building derived HCI_Event without event_code.
assert event_code != -1
super().__init__(HCI_Event.event_name(event_code)) super().__init__(HCI_Event.event_name(event_code))
if (fields := getattr(self, 'fields', None)) and kwargs: if (fields := getattr(self, 'fields', None)) and kwargs:
HCI_Object.init_from_fields(self, fields, kwargs) HCI_Object.init_from_fields(self, fields, kwargs)

View File

@@ -26,9 +26,13 @@ from bumble.hci import (
HCI_SUCCESS, HCI_SUCCESS,
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR, HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
HCI_CONNECTION_TIMEOUT_ERROR, HCI_CONNECTION_TIMEOUT_ERROR,
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
HCI_PAGE_TIMEOUT_ERROR, HCI_PAGE_TIMEOUT_ERROR,
HCI_Connection_Complete_Event, HCI_Connection_Complete_Event,
) )
from bumble import controller
from typing import Optional, Set
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -57,6 +61,8 @@ class LocalLink:
Link bus for controllers to communicate with each other Link bus for controllers to communicate with each other
''' '''
controllers: Set[controller.Controller]
def __init__(self): def __init__(self):
self.controllers = set() self.controllers = set()
self.pending_connection = None self.pending_connection = None
@@ -79,7 +85,9 @@ class LocalLink:
return controller return controller
return None return None
def find_classic_controller(self, address): def find_classic_controller(
self, address: Address
) -> Optional[controller.Controller]:
for controller in self.controllers: for controller in self.controllers:
if controller.public_address == address: if controller.public_address == address:
return controller return controller
@@ -271,6 +279,52 @@ class LocalLink:
initiator_controller.public_address, int(not (initiator_new_role)) initiator_controller.public_address, int(not (initiator_new_role))
) )
def classic_sco_connect(
self,
initiator_controller: controller.Controller,
responder_address: Address,
link_type: int,
):
logger.debug(
f'[Classic] {initiator_controller.public_address} connects SCO to {responder_address}'
)
responder_controller = self.find_classic_controller(responder_address)
# Initiator controller should handle it.
assert responder_controller
responder_controller.on_classic_connection_request(
initiator_controller.public_address,
link_type,
)
def classic_accept_sco_connection(
self,
responder_controller: controller.Controller,
initiator_address: Address,
link_type: int,
):
logger.debug(
f'[Classic] {responder_controller.public_address} accepts to connect SCO {initiator_address}'
)
initiator_controller = self.find_classic_controller(initiator_address)
if initiator_controller is None:
responder_controller.on_classic_sco_connection_complete(
responder_controller.public_address,
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
link_type,
)
return
async def task():
initiator_controller.on_classic_sco_connection_complete(
responder_controller.public_address, HCI_SUCCESS, link_type
)
asyncio.create_task(task())
responder_controller.on_classic_sco_connection_complete(
initiator_controller.public_address, HCI_SUCCESS, link_type
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class RemoteLink: class RemoteLink:

View File

@@ -198,12 +198,13 @@ async def open_transport_or_link(name: str) -> Transport:
""" """
if name.startswith('link-relay:'): if name.startswith('link-relay:'):
logger.warning('Link Relay has been deprecated.')
from ..controller import Controller from ..controller import Controller
from ..link import RemoteLink # lazy import from ..link import RemoteLink # lazy import
link = RemoteLink(name[11:]) link = RemoteLink(name[11:])
await link.wait_until_connected() await link.wait_until_connected()
controller = Controller('remote', link=link) controller = Controller('remote', link=link) # type:ignore[arg-type]
class LinkTransport(Transport): class LinkTransport(Transport):
async def close(self): async def close(self):

View File

@@ -23,9 +23,11 @@ import pytest
from typing import Tuple from typing import Tuple
from .test_utils import TwoDevices from .test_utils import TwoDevices
from bumble import core
from bumble import device
from bumble import hfp from bumble import hfp
from bumble import rfcomm from bumble import rfcomm
from bumble import hci
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -87,6 +89,63 @@ async def test_slc():
ag_task.cancel() ag_task.cancel()
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_sco_setup():
devices = TwoDevices()
# Enable Classic connections
devices[0].classic_enabled = True
devices[1].classic_enabled = True
# Start
await devices[0].power_on()
await devices[1].power_on()
connections = await asyncio.gather(
devices[0].connect(
devices[1].public_address, transport=core.BT_BR_EDR_TRANSPORT
),
devices[1].accept(devices[0].public_address),
)
def on_sco_request(_connection: device.Connection, _link_type: int):
connections[1].abort_on(
'disconnection',
devices[1].send_command(
hci.HCI_Enhanced_Accept_Synchronous_Connection_Request_Command(
bd_addr=connections[1].peer_address,
**hfp.ESCO_PARAMETERS[
hfp.DefaultCodecParameters.ESCO_CVSD_S1
].asdict(),
)
),
)
devices[1].on('sco_request', on_sco_request)
sco_connections = [
asyncio.get_running_loop().create_future(),
asyncio.get_running_loop().create_future(),
]
devices[0].on(
'sco_connection', lambda sco_link: sco_connections[0].set_result(sco_link)
)
devices[1].on(
'sco_connection', lambda sco_link: sco_connections[1].set_result(sco_link)
)
await devices[0].send_command(
hci.HCI_Enhanced_Setup_Synchronous_Connection_Command(
connection_handle=connections[0].handle,
**hfp.ESCO_PARAMETERS[hfp.DefaultCodecParameters.ESCO_CVSD_S1].asdict(),
)
)
await asyncio.gather(*sco_connections)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def run(): async def run():
await test_slc() await test_slc()

View File

@@ -29,17 +29,18 @@ class TwoDevices:
self.connections = [None, None] self.connections = [None, None]
self.link = LocalLink() self.link = LocalLink()
addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0']
self.controllers = [ self.controllers = [
Controller('C1', link=self.link), Controller('C1', link=self.link, public_address=addresses[0]),
Controller('C2', link=self.link), Controller('C2', link=self.link, public_address=addresses[1]),
] ]
self.devices = [ self.devices = [
Device( Device(
address=Address('F0:F1:F2:F3:F4:F5'), address=Address(addresses[0]),
host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])), host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
), ),
Device( Device(
address=Address('F5:F4:F3:F2:F1:F0'), address=Address(addresses[1]),
host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])), host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
), ),
] ]