Basic LMP implementation

This commit is contained in:
Josh Wu
2025-11-08 23:23:27 +08:00
parent bb9aa12a74
commit e0dee2135f
4 changed files with 633 additions and 197 deletions

View File

@@ -23,9 +23,9 @@ import itertools
import logging import logging
import random import random
import struct import struct
from typing import TYPE_CHECKING, Any, Optional, Union from typing import TYPE_CHECKING, Any, Optional, Union, cast
from bumble import hci from bumble import hci, lmp
from bumble.colors import color from bumble.colors import color
from bumble.core import PhysicalTransport from bumble.core import PhysicalTransport
@@ -56,6 +56,14 @@ class CisLink:
data_paths: set[int] = dataclasses.field(default_factory=set) data_paths: set[int] = dataclasses.field(default_factory=set)
# -----------------------------------------------------------------------------
@dataclasses.dataclass
class ScoLink:
handle: int
link_type: int
peer_address: hci.Address
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@dataclasses.dataclass @dataclasses.dataclass
class Connection: class Connection:
@@ -66,6 +74,7 @@ class Connection:
link: Any link: Any
transport: int transport: int
link_type: int link_type: int
classic_allow_role_switch: bool = False
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.assembler = hci.HCI_AclDataPacketAssembler(self.on_acl_pdu) self.assembler = hci.HCI_AclDataPacketAssembler(self.on_acl_pdu)
@@ -96,6 +105,8 @@ class Controller:
hci.Address, Connection hci.Address, Connection
] # Connections where this controller is the peripheral ] # Connections where this controller is the peripheral
classic_connections: dict[hci.Address, Connection] # Connections in BR/EDR classic_connections: dict[hci.Address, Connection] # Connections in BR/EDR
classic_pending_commands: dict[hci.Address, dict[lmp.Opcode, asyncio.Future[int]]]
sco_links: dict[hci.Address, ScoLink] # SCO links by address
central_cis_links: dict[int, CisLink] # CIS links by handle central_cis_links: dict[int, CisLink] # CIS links by handle
peripheral_cis_links: dict[int, CisLink] # CIS links by handle peripheral_cis_links: dict[int, CisLink] # CIS links by handle
@@ -151,6 +162,7 @@ class Controller:
advertising_data: Optional[bytes] = None advertising_data: Optional[bytes] = None
advertising_timer_handle: Optional[asyncio.Handle] = None advertising_timer_handle: Optional[asyncio.Handle] = None
classic_scan_enable: int = 0 classic_scan_enable: int = 0
classic_allow_role_switch: bool = True
_random_address: hci.Address = hci.Address('00:00:00:00:00:00') _random_address: hci.Address = hci.Address('00:00:00:00:00:00')
@@ -167,6 +179,8 @@ class Controller:
self.central_connections = {} self.central_connections = {}
self.peripheral_connections = {} self.peripheral_connections = {}
self.classic_connections = {} self.classic_connections = {}
self.sco_links = {}
self.classic_pending_commands = {}
self.central_cis_links = {} self.central_cis_links = {}
self.peripheral_cis_links = {} self.peripheral_cis_links = {}
self.default_phy = { self.default_phy = {
@@ -293,7 +307,7 @@ class Controller:
f'{color("CONTROLLER -> HOST", "green")}: {packet}' f'{color("CONTROLLER -> HOST", "green")}: {packet}'
) )
if self.host: if self.host:
self.host.on_packet(bytes(packet)) asyncio.get_running_loop().call_soon(self.host.on_packet, bytes(packet))
# This method allows the controller to emulate the same API as a transport source # This method allows the controller to emulate the same API as a transport source
async def wait_for_termination(self) -> None: async def wait_for_termination(self) -> None:
@@ -303,25 +317,20 @@ class Controller:
# Link connections # Link connections
############################################################ ############################################################
def allocate_connection_handle(self) -> int: def allocate_connection_handle(self) -> int:
handle = 0 current_handles = set(
max_handle = 0 cast(Connection | CisLink | ScoLink, link).handle
for connection in itertools.chain( for link in itertools.chain(
self.central_connections.values(), self.central_connections.values(),
self.peripheral_connections.values(), self.peripheral_connections.values(),
self.classic_connections.values(), self.classic_connections.values(),
): self.sco_links.values(),
max_handle = max(max_handle, connection.handle) self.central_cis_links.values(),
if connection.handle == handle: self.peripheral_cis_links.values(),
# Already used, continue searching after the current max )
handle = max_handle + 1 )
for cis_handle in itertools.chain( return next(
self.central_cis_links.keys(), self.peripheral_cis_links.keys() handle for handle in range(0xEFF + 1) if handle not in current_handles
): )
max_handle = max(max_handle, cis_handle)
if cis_handle == handle:
# Already used, continue searching after the current max
handle = max_handle + 1
return handle
def find_le_connection_by_address( def find_le_connection_by_address(
self, address: hci.Address self, address: hci.Address
@@ -363,6 +372,12 @@ class Controller:
return connection return connection
return None return None
def find_classic_sco_link_by_handle(self, handle: int) -> Optional[ScoLink]:
for connection in self.sco_links.values():
if connection.handle == handle:
return connection
return None
def find_iso_link_by_handle(self, handle: int) -> Optional[CisLink]: def find_iso_link_by_handle(self, handle: int) -> Optional[CisLink]:
return self.central_cis_links.get(handle) or self.peripheral_cis_links.get( return self.central_cis_links.get(handle) or self.peripheral_cis_links.get(
handle handle
@@ -669,9 +684,75 @@ class Controller:
# Classic link connections # Classic link connections
############################################################ ############################################################
def send_lmp_packet(
self, receiver_address: hci.Address, packet: lmp.Packet
) -> asyncio.Future[int]:
loop = asyncio.get_running_loop()
assert self.link
self.link.send_lmp_packet(self, receiver_address, packet)
future = self.classic_pending_commands.setdefault(receiver_address, {})[
packet.opcode
] = loop.create_future()
return future
def on_lmp_packet(self, sender_address: hci.Address, packet: lmp.Packet):
if isinstance(packet, (lmp.LmpAccepted, lmp.LmpAcceptedExt)):
if future := self.classic_pending_commands.setdefault(
sender_address, {}
).get(packet.response_opcode):
future.set_result(hci.HCI_SUCCESS)
else:
logger.error("!!! Unhandled packet: %s", packet)
elif isinstance(packet, (lmp.LmpNotAccepted, lmp.LmpNotAcceptedExt)):
if future := self.classic_pending_commands.setdefault(
sender_address, {}
).get(packet.response_opcode):
future.set_result(packet.error_code)
else:
logger.error("!!! Unhandled packet: %s", packet)
elif isinstance(packet, (lmp.LmpHostConnectionReq)):
self.on_classic_connection_request(
sender_address, hci.HCI_Connection_Complete_Event.LinkType.ACL
)
elif isinstance(packet, (lmp.LmpScoLinkReq)):
self.on_classic_connection_request(
sender_address, hci.HCI_Connection_Complete_Event.LinkType.SCO
)
elif isinstance(packet, (lmp.LmpEscoLinkReq)):
self.on_classic_connection_request(
sender_address, hci.HCI_Connection_Complete_Event.LinkType.ESCO
)
elif isinstance(packet, (lmp.LmpDetach)):
self.on_classic_disconnected(
sender_address, hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR
)
elif isinstance(packet, (lmp.LmpSwitchReq)):
self.on_classic_role_change_request(sender_address)
elif isinstance(packet, (lmp.LmpRemoveScoLinkReq, lmp.LmpRemoveEscoLinkReq)):
self.on_classic_sco_disconnected(sender_address, packet.error_code)
else:
logger.error("!!! Unhandled packet: %s", packet)
def on_classic_connection_request( def on_classic_connection_request(
self, peer_address: hci.Address, link_type: int self, peer_address: hci.Address, link_type: int
) -> None: ) -> None:
if link_type == hci.HCI_Connection_Complete_Event.LinkType.ACL:
self.classic_connections[peer_address] = Connection(
controller=self,
handle=0,
role=hci.Role.PERIPHERAL,
peer_address=peer_address,
link=self.link,
transport=PhysicalTransport.BR_EDR,
link_type=link_type,
classic_allow_role_switch=self.classic_allow_role_switch,
)
else:
self.sco_links[peer_address] = ScoLink(
handle=0,
link_type=link_type,
peer_address=peer_address,
)
self.send_hci_packet( self.send_hci_packet(
hci.HCI_Connection_Request_Event( hci.HCI_Connection_Request_Event(
bd_addr=peer_address, bd_addr=peer_address,
@@ -686,13 +767,13 @@ class Controller:
if status == hci.HCI_SUCCESS: if status == hci.HCI_SUCCESS:
# Allocate (or reuse) a connection handle # Allocate (or reuse) a connection handle
peer_address = peer_address peer_address = peer_address
connection = self.classic_connections.get(peer_address)
if connection is None:
connection_handle = self.allocate_connection_handle() connection_handle = self.allocate_connection_handle()
if connection := self.classic_connections.get(peer_address):
connection.handle = connection_handle
else:
connection = Connection( connection = Connection(
controller=self, controller=self,
handle=connection_handle, handle=connection_handle,
# hci.Role doesn't matter in Classic because they are managed by hci.HCI_Role_Change and hci.HCI_Role_Discovery
role=hci.Role.CENTRAL, role=hci.Role.CENTRAL,
peer_address=peer_address, peer_address=peer_address,
link=self.link, link=self.link,
@@ -703,8 +784,6 @@ class Controller:
logger.debug( logger.debug(
f'New CLASSIC connection handle: 0x{connection_handle:04X}' f'New CLASSIC connection handle: 0x{connection_handle:04X}'
) )
else:
connection_handle = connection.handle
self.send_hci_packet( self.send_hci_packet(
hci.HCI_Connection_Complete_Event( hci.HCI_Connection_Complete_Event(
status=status, status=status,
@@ -728,7 +807,7 @@ class Controller:
def on_classic_disconnected(self, peer_address: hci.Address, reason: int) -> None: def on_classic_disconnected(self, peer_address: hci.Address, reason: int) -> None:
# Send a disconnection complete event # Send a disconnection complete event
if connection := self.classic_connections.get(peer_address): if connection := self.classic_connections.pop(peer_address, None):
self.send_hci_packet( self.send_hci_packet(
hci.HCI_Disconnection_Complete_Event( hci.HCI_Disconnection_Complete_Event(
status=hci.HCI_SUCCESS, status=hci.HCI_SUCCESS,
@@ -736,17 +815,51 @@ class Controller:
reason=reason, reason=reason,
) )
) )
# Remove the connection
del self.classic_connections[peer_address]
else: else:
logger.warning(f'!!! No classic connection found for {peer_address}') logger.warning(f'!!! No classic connection found for {peer_address}')
def on_classic_role_change(self, peer_address: hci.Address, new_role: int) -> None: def on_classic_sco_disconnected(
self, peer_address: hci.Address, reason: int
) -> None:
# Send a disconnection complete event
if sco_link := self.sco_links.pop(peer_address, None):
self.send_hci_packet(
hci.HCI_Disconnection_Complete_Event(
status=hci.HCI_SUCCESS,
connection_handle=sco_link.handle,
reason=reason,
)
)
else:
logger.warning(f'!!! No classic connection found for {peer_address}')
def on_classic_role_change_request(self, peer_address: hci.Address) -> None:
assert (connection := self.classic_connections.get(peer_address))
if not connection.classic_allow_role_switch:
self.send_lmp_packet(
peer_address,
lmp.LmpNotAccepted(
lmp.Opcode.LMP_SWITCH_REQ, hci.HCI_ROLE_CHANGE_NOT_ALLOWED_ERROR
),
)
else:
self.send_lmp_packet(
peer_address,
lmp.LmpAccepted(lmp.Opcode.LMP_SWITCH_REQ),
)
self.classic_role_change(connection)
def classic_role_change(self, connection: Connection) -> None:
new_role = (
hci.Role.CENTRAL
if connection.role == hci.Role.PERIPHERAL
else hci.Role.PERIPHERAL
)
connection.role = new_role
self.send_hci_packet( self.send_hci_packet(
hci.HCI_Role_Change_Event( hci.HCI_Role_Change_Event(
status=hci.HCI_SUCCESS, status=hci.HCI_SUCCESS,
bd_addr=peer_address, bd_addr=connection.peer_address,
new_role=new_role, new_role=new_role,
) )
) )
@@ -757,17 +870,12 @@ class Controller:
if status == hci.HCI_SUCCESS: if status == hci.HCI_SUCCESS:
# Allocate (or reuse) a connection handle # Allocate (or reuse) a connection handle
connection_handle = self.allocate_connection_handle() connection_handle = self.allocate_connection_handle()
connection = Connection( sco_link = ScoLink(
controller=self,
handle=connection_handle, handle=connection_handle,
# hci.Role doesn't matter in SCO.
role=hci.Role.CENTRAL,
peer_address=peer_address,
link=self.link,
transport=PhysicalTransport.BR_EDR,
link_type=link_type, link_type=link_type,
peer_address=peer_address,
) )
self.classic_connections[peer_address] = connection self.sco_links[peer_address] = sco_link
logger.debug(f'New SCO connection handle: 0x{connection_handle:04X}') logger.debug(f'New SCO connection handle: 0x{connection_handle:04X}')
else: else:
connection_handle = 0 connection_handle = 0
@@ -847,7 +955,16 @@ class Controller:
) )
return None return None
self.link.classic_connect(self, command.bd_addr) self.classic_connections[command.bd_addr] = Connection(
controller=self,
handle=0,
role=hci.Role.CENTRAL,
peer_address=command.bd_addr,
link=self.link,
transport=PhysicalTransport.BR_EDR,
link_type=hci.HCI_Connection_Complete_Event.LinkType.ACL,
classic_allow_role_switch=bool(command.allow_role_switch),
)
# Say that the connection is pending # Say that the connection is pending
self.send_hci_packet( self.send_hci_packet(
@@ -857,6 +974,12 @@ class Controller:
command_opcode=command.op_code, command_opcode=command.op_code,
) )
) )
future = self.send_lmp_packet(command.bd_addr, lmp.LmpHostConnectionReq())
def on_response(future: asyncio.Future[int]):
self.on_classic_connection_complete(command.bd_addr, future.result())
future.add_done_callback(on_response)
return None return None
def on_hci_disconnect_command( def on_hci_disconnect_command(
@@ -894,14 +1017,37 @@ class Controller:
del self.peripheral_connections[connection.peer_address] del self.peripheral_connections[connection.peer_address]
elif connection := self.find_classic_connection_by_handle(handle): elif connection := self.find_classic_connection_by_handle(handle):
if self.link: if self.link:
self.link.classic_disconnect( self.send_lmp_packet(
self,
connection.peer_address, connection.peer_address,
hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR, lmp.LmpDetach(command.reason),
) )
self.on_classic_disconnected(connection.peer_address, command.reason)
else: else:
# Remove the connection # Remove the connection
del self.classic_connections[connection.peer_address] del self.classic_connections[connection.peer_address]
elif sco_link := self.find_classic_sco_link_by_handle(handle):
if self.link:
if (
sco_link.link_type
== hci.HCI_Connection_Complete_Event.LinkType.ESCO
):
self.send_lmp_packet(
sco_link.peer_address,
lmp.LmpRemoveScoLinkReq(
sco_handle=0, error_code=command.reason
),
)
else:
self.send_lmp_packet(
sco_link.peer_address,
lmp.LmpRemoveEscoLinkReq(
esco_handle=0, error_code=command.reason
),
)
self.on_classic_sco_disconnected(sco_link.peer_address, command.reason)
else:
# Remove the connection
del self.sco_links[sco_link.peer_address]
elif cis_link := ( elif cis_link := (
self.central_cis_links.get(handle) or self.peripheral_cis_links.get(handle) self.central_cis_links.get(handle) or self.peripheral_cis_links.get(handle)
): ):
@@ -925,6 +1071,16 @@ class Controller:
if self.link is None: if self.link is None:
return None return None
if not (connection := self.classic_connections.get(command.bd_addr)):
self.send_hci_packet(
hci.HCI_Command_Status_Event(
status=hci.HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
return None
self.send_hci_packet( self.send_hci_packet(
hci.HCI_Command_Status_Event( hci.HCI_Command_Status_Event(
status=hci.HCI_SUCCESS, status=hci.HCI_SUCCESS,
@@ -932,7 +1088,36 @@ class Controller:
command_opcode=command.op_code, command_opcode=command.op_code,
) )
) )
self.link.classic_accept_connection(self, command.bd_addr, command.role)
if command.role == hci.Role.CENTRAL:
# Perform role switching before accept.
future = self.send_lmp_packet(command.bd_addr, lmp.LmpSwitchReq())
def on_response(future: asyncio.Future[int]):
if (status := future.result()) == hci.HCI_SUCCESS:
self.classic_role_change(connection)
# Continue connection setup.
self.send_lmp_packet(
command.bd_addr,
lmp.LmpAccepted(lmp.Opcode.LMP_HOST_CONNECTION_REQ),
)
else:
# Abort connection setup.
self.send_lmp_packet(
command.bd_addr,
lmp.LmpNotAccepted(lmp.Opcode.LMP_HOST_CONNECTION_REQ, status),
)
self.on_classic_connection_complete(command.bd_addr, status)
future.add_done_callback(on_response)
else:
# Simply accept connection.
self.send_lmp_packet(
command.bd_addr,
lmp.LmpAccepted(lmp.Opcode.LMP_HOST_CONNECTION_REQ),
)
self.on_classic_connection_complete(command.bd_addr, hci.HCI_SUCCESS)
return None return None
def on_hci_enhanced_setup_synchronous_connection_command( def on_hci_enhanced_setup_synchronous_connection_command(
@@ -966,11 +1151,32 @@ class Controller:
command_opcode=command.op_code, command_opcode=command.op_code,
) )
) )
self.link.classic_sco_connect( future = self.send_lmp_packet(
self,
connection.peer_address, connection.peer_address,
lmp.LmpEscoLinkReq(
esco_handle=0,
esco_lt_addr=0,
timing_control_flags=0,
d_esco=0,
t_esco=0,
w_esco=0,
esco_packet_type_c_to_p=0,
esco_packet_type_p_to_c=0,
packet_length_c_to_p=0,
packet_length_p_to_c=0,
air_mode=0,
negotiation_state=0,
),
)
def on_response(future: asyncio.Future[int]):
self.on_classic_sco_connection_complete(
connection.peer_address,
future.result(),
hci.HCI_Connection_Complete_Event.LinkType.ESCO, hci.HCI_Connection_Complete_Event.LinkType.ESCO,
) )
future.add_done_callback(on_response)
return None return None
def on_hci_enhanced_accept_synchronous_connection_request_command( def on_hci_enhanced_accept_synchronous_connection_request_command(
@@ -1000,9 +1206,13 @@ class Controller:
command_opcode=command.op_code, command_opcode=command.op_code,
) )
) )
self.link.classic_accept_sco_connection( self.send_lmp_packet(
self,
connection.peer_address, connection.peer_address,
lmp.LmpAcceptedExt(lmp.Opcode.LMP_ESCO_LINK_REQ),
)
self.on_classic_sco_connection_complete(
connection.peer_address,
hci.HCI_SUCCESS,
hci.HCI_Connection_Complete_Event.LinkType.ESCO, hci.HCI_Connection_Complete_Event.LinkType.ESCO,
) )
return None return None
@@ -1083,6 +1293,9 @@ class Controller:
if self.link is None: if self.link is None:
return None return None
if connection := self.classic_connections.get(command.bd_addr):
current_role = connection.role
self.send_hci_packet( self.send_hci_packet(
hci.HCI_Command_Status_Event( hci.HCI_Command_Status_Event(
status=hci.HCI_SUCCESS, status=hci.HCI_SUCCESS,
@@ -1090,7 +1303,42 @@ class Controller:
command_opcode=command.op_code, command_opcode=command.op_code,
) )
) )
self.link.classic_switch_role(self, command.bd_addr, command.role) else:
# Connection doesn't exist, reject.
self.send_hci_packet(
hci.HCI_Command_Status_Event(
status=hci.HCI_COMMAND_DISALLOWED_ERROR,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
return None
# If role doesn't change, only send event to local host.
if current_role == command.role:
self.send_hci_packet(
hci.HCI_Role_Change_Event(
status=hci.HCI_SUCCESS,
bd_addr=command.bd_addr,
new_role=current_role,
)
)
else:
future = self.send_lmp_packet(command.bd_addr, lmp.LmpSwitchReq())
def on_response(future: asyncio.Future[int]):
if (status := future.result()) == hci.HCI_SUCCESS:
connection.role = hci.Role(command.role)
self.send_hci_packet(
hci.HCI_Role_Change_Event(
status=status,
bd_addr=command.bd_addr,
new_role=connection.role,
)
)
future.add_done_callback(on_response)
return None return None
def on_hci_set_event_mask_command( def on_hci_set_event_mask_command(

View File

@@ -21,7 +21,7 @@ import asyncio
import logging import logging
from typing import Optional from typing import Optional
from bumble import controller, core, hci from bumble import controller, core, hci, lmp
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -109,7 +109,11 @@ class LocalLink:
raise ValueError("unsupported transport type") raise ValueError("unsupported transport type")
if destination_controller is not None: if destination_controller is not None:
destination_controller.on_link_acl_data(source_address, transport, data) asyncio.get_running_loop().call_soon(
lambda: destination_controller.on_link_acl_data(
source_address, transport, data
)
)
def on_connection_complete(self) -> None: def on_connection_complete(self) -> None:
# Check that we expect this call # Check that we expect this call
@@ -261,142 +265,18 @@ class LocalLink:
# Classic handlers # Classic handlers
############################################################ ############################################################
def classic_connect( def send_lmp_packet(
self, self,
initiator_controller: controller.Controller, sender_controller: controller.Controller,
responder_address: hci.Address, receiver_address: hci.Address,
packet: lmp.Packet,
): ):
logger.debug( if not (receiver_controller := self.find_classic_controller(receiver_address)):
f'[Classic] {initiator_controller.public_address} connects to {responder_address}' raise core.InvalidArgumentError(
f"Unable to find controller for address {receiver_address}"
) )
responder_controller = self.find_classic_controller(responder_address)
if responder_controller is None:
initiator_controller.on_classic_connection_complete(
responder_address, hci.HCI_PAGE_TIMEOUT_ERROR
)
return
self.pending_classic_connection = (initiator_controller, responder_controller)
responder_controller.on_classic_connection_request(
initiator_controller.public_address,
hci.HCI_Connection_Complete_Event.LinkType.ACL,
)
def classic_accept_connection(
self,
responder_controller: controller.Controller,
initiator_address: hci.Address,
responder_role: int,
):
logger.debug(
f'[Classic] {responder_controller.public_address} accepts to connect {initiator_address}'
)
initiator_controller = self.find_classic_controller(initiator_address)
if initiator_controller is None:
responder_controller.on_classic_connection_complete(
responder_controller.public_address, hci.HCI_PAGE_TIMEOUT_ERROR
)
return
def connection_complete() -> None:
if responder_role != hci.Role.PERIPHERAL:
initiator_controller.on_classic_role_change(
responder_controller.public_address, int(not (responder_role))
)
initiator_controller.on_classic_connection_complete(
responder_controller.public_address, hci.HCI_SUCCESS
)
responder_controller.on_classic_role_change(
initiator_controller.public_address, responder_role
)
responder_controller.on_classic_connection_complete(
initiator_controller.public_address, hci.HCI_SUCCESS
)
self.pending_classic_connection = None
asyncio.get_running_loop().call_soon(connection_complete)
def classic_disconnect(
self,
initiator_controller: controller.Controller,
responder_address: hci.Address,
reason: int,
):
logger.debug(
f'[Classic] {initiator_controller.public_address} disconnects {responder_address}'
)
responder_controller = self.find_classic_controller(responder_address)
assert responder_controller
asyncio.get_running_loop().call_soon( asyncio.get_running_loop().call_soon(
lambda: initiator_controller.on_classic_disconnected( lambda: receiver_controller.on_lmp_packet(
responder_address, reason sender_controller.public_address, packet
) )
) )
responder_controller.on_classic_disconnected(
initiator_controller.public_address, reason
)
def classic_switch_role(
self,
initiator_controller: controller.Controller,
responder_address: hci.Address,
initiator_new_role: int,
):
responder_controller = self.find_classic_controller(responder_address)
if responder_controller is None:
return
asyncio.get_running_loop().call_soon(
lambda: initiator_controller.on_classic_role_change(
responder_address, initiator_new_role
)
)
responder_controller.on_classic_role_change(
initiator_controller.public_address, int(not (initiator_new_role))
)
def classic_sco_connect(
self,
initiator_controller: controller.Controller,
responder_address: hci.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: hci.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.HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
link_type,
)
return
asyncio.get_running_loop().call_soon(
lambda: initiator_controller.on_classic_sco_connection_complete(
responder_controller.public_address, hci.HCI_SUCCESS, link_type
)
)
responder_controller.on_classic_sco_connection_complete(
initiator_controller.public_address, hci.HCI_SUCCESS, link_type
)

306
bumble/lmp.py Normal file
View File

@@ -0,0 +1,306 @@
# Copyright 2021-2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
from __future__ import annotations
import struct
from dataclasses import dataclass, field
from typing import TypeVar
from bumble import hci, utils
class Opcode(utils.OpenIntEnum):
'''
See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary.
Follow the alphabetical order defined there.
'''
# fmt: off
LMP_ACCEPTED = 3
LMP_ACCEPTED_EXT = 127 << 8 + 1
LMP_AU_RAND = 11
LMP_AUTO_RATE = 35
LMP_CHANNEL_CLASSIFICATION = 127 << 8 + 17
LMP_CHANNEL_CLASSIFICATION_REQ = 127 << 8 + 16
LMP_CLK_ADJ = 127 << 8 + 5
LMP_CLK_ADJ_ACK = 127 << 8 + 6
LMP_CLK_ADJ_REQ = 127 << 8 + 7
LMP_CLKOFFSET_REQ = 5
LMP_CLKOFFSET_RES = 6
LMP_COMB_KEY = 9
LMP_DECR_POWER_REQ = 32
LMP_DETACH = 7
LMP_DHKEY_CHECK = 65
LMP_ENCAPSULATED_HEADER = 61
LMP_ENCAPSULATED_PAYLOAD = 62
LMP_ENCRYPTION_KEY_SIZE_MASK_REQ= 58
LMP_ENCRYPTION_KEY_SIZE_MASK_RES= 59
LMP_ENCRYPTION_KEY_SIZE_REQ = 16
LMP_ENCRYPTION_MODE_REQ = 15
LMP_ESCO_LINK_REQ = 127 << 8 + 12
LMP_FEATURES_REQ = 39
LMP_FEATURES_REQ_EXT = 127 << 8 + 3
LMP_FEATURES_RES = 40
LMP_FEATURES_RES_EXT = 127 << 8 + 4
LMP_HOLD = 20
LMP_HOLD_REQ = 21
LMP_HOST_CONNECTION_REQ = 51
LMP_IN_RAND = 8
LMP_INCR_POWER_REQ = 31
LMP_IO_CAPABILITY_REQ = 127 << 8 + 25
LMP_IO_CAPABILITY_RES = 127 << 8 + 26
LMP_KEYPRESS_NOTIFICATION = 127 << 8 + 30
LMP_MAX_POWER = 33
LMP_MAX_SLOT = 45
LMP_MAX_SLOT_REQ = 46
LMP_MIN_POWER = 34
LMP_NAME_REQ = 1
LMP_NAME_RES = 2
LMP_NOT_ACCEPTED = 4
LMP_NOT_ACCEPTED_EXT = 127 << 8 + 2
LMP_NUMERIC_COMPARISON_FAILED = 127 << 8 + 27
LMP_OOB_FAILED = 127 << 8 + 29
LMP_PACKET_TYPE_TABLE_REQ = 127 << 8 + 11
LMP_PAGE_MODE_REQ = 53
LMP_PAGE_SCAN_MODE_REQ = 54
LMP_PASSKEY_FAILED = 127 << 8 + 28
LMP_PAUSE_ENCRYPTION_AES_REQ = 66
LMP_PAUSE_ENCRYPTION_REQ = 127 << 8 + 23
LMP_PING_REQ = 127 << 8 + 33
LMP_PING_RES = 127 << 8 + 34
LMP_POWER_CONTROL_REQ = 127 << 8 + 31
LMP_POWER_CONTROL_RES = 127 << 8 + 32
LMP_PREFERRED_RATE = 36
LMP_QUALITY_OF_SERVICE = 41
LMP_QUALITY_OF_SERVICE_REQ = 42
LMP_REMOVE_ESCO_LINK_REQ = 127 << 8 + 13
LMP_REMOVE_SCO_LINK_REQ = 44
LMP_RESUME_ENCRYPTION_REQ = 127 << 8 + 24
LMP_SAM_DEFINE_MAP = 127 << 8 + 36
LMP_SAM_SET_TYPE0 = 127 << 8 + 35
LMP_SAM_SWITCH = 127 << 8 + 37
LMP_SCO_LINK_REQ = 43
LMP_SET_AFH = 60
LMP_SETUP_COMPLETE = 49
LMP_SIMPLE_PAIRING_CONFIRM = 63
LMP_SIMPLE_PAIRING_NUMBER = 64
LMP_SLOT_OFFSET = 52
LMP_SNIFF_REQ = 23
LMP_SNIFF_SUBRATING_REQ = 127 << 8 + 21
LMP_SNIFF_SUBRATING_RES = 127 << 8 + 22
LMP_SRES = 12
LMP_START_ENCRYPTION_REQ = 17
LMP_STOP_ENCRYPTION_REQ = 18
LMP_SUPERVISION_TIMEOUT = 55
LMP_SWITCH_REQ = 19
LMP_TEMP_KEY = 14
LMP_TEMP_RAND = 13
LMP_TEST_ACTIVATE = 56
LMP_TEST_CONTROL = 57
LMP_TIMING_ACCURACY_REQ = 47
LMP_TIMING_ACCURACY_RES = 48
LMP_UNIT_KEY = 10
LMP_UNSNIFF_REQ = 24
LMP_USE_SEMI_PERMANENT_KEY = 50
LMP_VERSION_REQ = 37
LMP_VERSION_RES = 38
# fmt: on
@classmethod
def parse_from(cls, data: bytes, offset: int = 0) -> tuple[int, Opcode]:
opcode = data[offset]
if opcode in (124, 127):
opcode = struct.unpack('>H', data)[0]
return offset + 2, Opcode(opcode)
return offset + 1, Opcode(opcode)
def __bytes__(self) -> bytes:
if self.value >> 8:
return struct.pack('>H', self.value)
return bytes([self.value])
@classmethod
def type_metadata(cls):
return hci.metadata(
{
'serializer': bytes,
'parser': lambda data, offset: (Opcode.parse_from(data, offset)),
}
)
class Packet:
'''
See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary
'''
subclasses: dict[int, type[Packet]] = {}
opcode: Opcode
fields: hci.Fields = ()
_payload: bytes = b''
_Packet = TypeVar("_Packet", bound="Packet")
@classmethod
def subclass(cls, subclass: type[_Packet]) -> type[_Packet]:
# Register a factory for this class
cls.subclasses[subclass.opcode] = subclass
subclass.fields = hci.HCI_Object.fields_from_dataclass(subclass)
return subclass
@classmethod
def from_bytes(cls, data: bytes) -> Packet:
offset, opcode = Opcode.parse_from(data)
if not (subclass := cls.subclasses.get(opcode)):
instance = Packet()
instance.opcode = opcode
else:
instance = subclass(
**hci.HCI_Object.dict_from_bytes(data, offset, subclass.fields)
)
instance.payload = data[offset:]
return instance
@property
def payload(self) -> bytes:
if self._payload is None:
self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
return self._payload
@payload.setter
def payload(self, value: bytes) -> None:
self._payload = value
def __bytes__(self) -> bytes:
return bytes(self.opcode) + self.payload
@Packet.subclass
@dataclass
class LmpAccepted(Packet):
opcode = Opcode.LMP_ACCEPTED
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
@Packet.subclass
@dataclass
class LmpNotAccepted(Packet):
opcode = Opcode.LMP_NOT_ACCEPTED
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
error_code: int = field(metadata=hci.metadata(1))
@Packet.subclass
@dataclass
class LmpAcceptedExt(Packet):
opcode = Opcode.LMP_ACCEPTED_EXT
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
@Packet.subclass
@dataclass
class LmpNotAcceptedExt(Packet):
opcode = Opcode.LMP_NOT_ACCEPTED_EXT
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
error_code: int = field(metadata=hci.metadata(1))
@Packet.subclass
@dataclass
class LmpAuRand(Packet):
opcode = Opcode.LMP_AU_RAND
random_number: bytes = field(metadata=hci.metadata(16))
@Packet.subclass
@dataclass
class LmpDetach(Packet):
opcode = Opcode.LMP_DETACH
error_code: int = field(metadata=hci.metadata(1))
@Packet.subclass
@dataclass
class LmpEscoLinkReq(Packet):
opcode = Opcode.LMP_ESCO_LINK_REQ
esco_handle: int = field(metadata=hci.metadata(1))
esco_lt_addr: int = field(metadata=hci.metadata(1))
timing_control_flags: int = field(metadata=hci.metadata(1))
d_esco: int = field(metadata=hci.metadata(1))
t_esco: int = field(metadata=hci.metadata(1))
w_esco: int = field(metadata=hci.metadata(1))
esco_packet_type_c_to_p: int = field(metadata=hci.metadata(1))
esco_packet_type_p_to_c: int = field(metadata=hci.metadata(1))
packet_length_c_to_p: int = field(metadata=hci.metadata(2))
packet_length_p_to_c: int = field(metadata=hci.metadata(2))
air_mode: int = field(metadata=hci.metadata(1))
negotiation_state: int = field(metadata=hci.metadata(1))
@Packet.subclass
@dataclass
class LmpHostConnectionReq(Packet):
opcode = Opcode.LMP_HOST_CONNECTION_REQ
@Packet.subclass
@dataclass
class LmpRemoveEscoLinkReq(Packet):
opcode = Opcode.LMP_REMOVE_ESCO_LINK_REQ
esco_handle: int = field(metadata=hci.metadata(1))
error_code: int = field(metadata=hci.metadata(1))
@Packet.subclass
@dataclass
class LmpRemoveScoLinkReq(Packet):
opcode = Opcode.LMP_REMOVE_SCO_LINK_REQ
sco_handle: int = field(metadata=hci.metadata(1))
error_code: int = field(metadata=hci.metadata(1))
@Packet.subclass
@dataclass
class LmpScoLinkReq(Packet):
opcode = Opcode.LMP_SCO_LINK_REQ
sco_handle: int = field(metadata=hci.metadata(1))
timing_control_flags: int = field(metadata=hci.metadata(1))
d_sco: int = field(metadata=hci.metadata(1))
t_sco: int = field(metadata=hci.metadata(1))
sco_packet: int = field(metadata=hci.metadata(1))
air_mode: int = field(metadata=hci.metadata(1))
@Packet.subclass
@dataclass
class LmpSwitchReq(Packet):
opcode = Opcode.LMP_SWITCH_REQ
switch_instant: int = field(metadata=hci.metadata(4), default=0)

View File

@@ -99,6 +99,8 @@ class TwoDevices:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def async_barrier(): async def async_barrier():
# TODO: Remove async barrier - this doesn't always mean what we want.
for _ in range(3):
ready = asyncio.get_running_loop().create_future() ready = asyncio.get_running_loop().create_future()
asyncio.get_running_loop().call_soon(ready.set_result, None) asyncio.get_running_loop().call_soon(ready.set_result, None)
await ready await ready