# 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 asyncio import collections import dataclasses import logging import struct from typing import ( Any, Awaitable, Callable, Optional, cast, TYPE_CHECKING, ) from bumble.colors import color from bumble.l2cap import L2CAP_PDU from bumble.snoop import Snooper from bumble import drivers from bumble import hci from bumble.core import ( PhysicalTransport, ConnectionPHY, ConnectionParameters, ) from bumble import utils from bumble.transport.common import TransportLostError if TYPE_CHECKING: from bumble.transport.common import TransportSink, TransportSource # ----------------------------------------------------------------------------- # Logging # ----------------------------------------------------------------------------- logger = logging.getLogger(__name__) # ----------------------------------------------------------------------------- class DataPacketQueue(utils.EventEmitter): """ Flow-control queue for host->controller data packets (ACL, ISO). The queue holds packets associated with a connection handle. The packets are sent to the controller, up to a maximum total number of packets in flight. A packet is considered to be "in flight" when it has been sent to the controller but not completed yet. Packets are no longer "in flight" when the controller declares them as completed. The queue emits a 'flow' event whenever one or more packets are completed. """ max_packet_size: int class PerConnectionState: def __init__(self) -> None: self.in_flight = 0 self.drained = asyncio.Event() def __init__( self, max_packet_size: int, max_in_flight: int, send: Callable[[hci.HCI_Packet], None], ) -> None: super().__init__() self.max_packet_size = max_packet_size self.max_in_flight = max_in_flight self._in_flight = 0 # Total number of packets in flight across all connections self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = ( collections.defaultdict(DataPacketQueue.PerConnectionState) ) self._drained_per_connection: dict[int, asyncio.Event] = ( collections.defaultdict(asyncio.Event) ) self._send = send self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = ( collections.deque() ) self._queued = 0 self._completed = 0 @property def queued(self) -> int: """Total number of packets queued since creation.""" return self._queued @property def completed(self) -> int: """Total number of packets completed since creation.""" return self._completed @property def pending(self) -> int: """Number of packets that have been queued but not completed.""" return self._queued - self._completed def enqueue(self, packet: hci.HCI_Packet, connection_handle: int) -> None: """Enqueue a packet associated with a connection""" self._packets.appendleft((packet, connection_handle)) self._queued += 1 self._check_queue() if self._packets: logger.debug( f'{self._in_flight} packets in flight, ' f'{len(self._packets)} in queue' ) def flush(self, connection_handle: int) -> None: """ Remove all packets associated with a connection. All packets associated with the connection that are in flight are implicitly marked as completed, but no 'flow' event is emitted. """ packets_to_keep = [ (packet, handle) for (packet, handle) in self._packets if handle != connection_handle ] if flushed_count := len(self._packets) - len(packets_to_keep): self._completed += flushed_count self._packets = collections.deque(packets_to_keep) if connection_state := self._connection_state.pop(connection_handle, None): in_flight = connection_state.in_flight self._completed += in_flight self._in_flight -= in_flight connection_state.drained.set() def _check_queue(self) -> None: while self._packets and self._in_flight < self.max_in_flight: packet, connection_handle = self._packets.pop() self._send(packet) self._in_flight += 1 connection_state = self._connection_state[connection_handle] connection_state.in_flight += 1 connection_state.drained.clear() def on_packets_completed(self, packet_count: int, connection_handle: int) -> None: """Mark one or more packets associated with a connection as completed.""" if connection_handle not in self._connection_state: logger.warning( f'received completion for unknown connection {connection_handle}' ) return connection_state = self._connection_state[connection_handle] if packet_count <= connection_state.in_flight: connection_state.in_flight -= packet_count else: logger.warning( f'{packet_count} completed for {connection_handle} ' f'but only {connection_state.in_flight} in flight' ) connection_state.in_flight = 0 if connection_state.in_flight == 0: connection_state.drained.set() if packet_count <= self._in_flight: self._in_flight -= packet_count self._completed += packet_count else: logger.warning( f'{packet_count} completed but only {self._in_flight} in flight' ) self._in_flight = 0 self._completed = self._queued self._check_queue() self.emit('flow') async def drain(self, connection_handle: int) -> None: """Wait until there are no pending packets for a connection.""" if not (connection_state := self._connection_state.get(connection_handle)): raise ValueError('no such connection') await connection_state.drained.wait() # ----------------------------------------------------------------------------- class Connection: def __init__( self, host: Host, handle: int, peer_address: hci.Address, transport: PhysicalTransport, ): self.host = host self.handle = handle self.peer_address = peer_address self.assembler = hci.HCI_AclDataPacketAssembler(self.on_acl_pdu) self.transport = transport acl_packet_queue: Optional[DataPacketQueue] = ( host.le_acl_packet_queue if transport == PhysicalTransport.LE else host.acl_packet_queue ) assert acl_packet_queue self.acl_packet_queue = acl_packet_queue def on_hci_acl_data_packet(self, packet: hci.HCI_AclDataPacket) -> None: self.assembler.feed_packet(packet) def on_acl_pdu(self, pdu: bytes) -> None: l2cap_pdu = L2CAP_PDU.from_bytes(pdu) self.host.on_l2cap_pdu(self, l2cap_pdu.cid, l2cap_pdu.payload) def __str__(self) -> str: return ( f'Connection(transport={self.transport}, peer_address={self.peer_address})' ) # ----------------------------------------------------------------------------- @dataclasses.dataclass class ScoLink: peer_address: hci.Address connection_handle: int # ----------------------------------------------------------------------------- @dataclasses.dataclass class IsoLink: handle: int packet_queue: DataPacketQueue = dataclasses.field(repr=False) packet_sequence_number: int = 0 # ----------------------------------------------------------------------------- class Host(utils.EventEmitter): connections: dict[int, Connection] cis_links: dict[int, IsoLink] bis_links: dict[int, IsoLink] sco_links: dict[int, ScoLink] bigs: dict[int, set[int]] acl_packet_queue: Optional[DataPacketQueue] = None le_acl_packet_queue: Optional[DataPacketQueue] = None iso_packet_queue: Optional[DataPacketQueue] = None hci_sink: Optional[TransportSink] = None hci_metadata: dict[str, Any] long_term_key_provider: Optional[ Callable[[int, bytes, int], Awaitable[Optional[bytes]]] ] link_key_provider: Optional[Callable[[hci.Address], Awaitable[Optional[bytes]]]] def __init__( self, controller_source: Optional[TransportSource] = None, controller_sink: Optional[TransportSink] = None, ) -> None: super().__init__() self.hci_metadata = {} self.ready = False # True when we can accept incoming packets self.connections = {} # Connections, by connection handle self.cis_links = {} # CIS links, by connection handle self.bis_links = {} # BIS links, by connection handle self.sco_links = {} # SCO links, by connection handle self.bigs = {} # BIG Handle to BIS Handles self.pending_command = None self.pending_response: Optional[asyncio.Future[Any]] = None self.number_of_supported_advertising_sets = 0 self.maximum_advertising_data_length = 31 self.local_version = None self.local_supported_commands = 0 self.local_le_features = 0 self.local_lmp_features = hci.LmpFeatureMask(0) # Classic LMP features self.suggested_max_tx_octets = 251 # Max allowed self.suggested_max_tx_time = 2120 # Max allowed self.command_semaphore = asyncio.Semaphore(1) self.long_term_key_provider = None self.link_key_provider = None self.pairing_io_capability_provider = None # Classic only self.snooper: Optional[Snooper] = None # Connect to the source and sink if specified if controller_source: self.set_packet_source(controller_source) if controller_sink: self.set_packet_sink(controller_sink) def find_connection_by_bd_addr( self, bd_addr: hci.Address, transport: Optional[int] = None, check_address_type: bool = False, ) -> Optional[Connection]: for connection in self.connections.values(): if bytes(connection.peer_address) == bytes(bd_addr): if ( check_address_type and connection.peer_address.address_type != bd_addr.address_type ): continue if transport is None or connection.transport == transport: return connection return None async def flush(self) -> None: # Make sure no command is pending await self.command_semaphore.acquire() # Flush current host state, then release command semaphore self.emit('flush') self.command_semaphore.release() async def reset(self, driver_factory=drivers.get_driver_for_host): if self.ready: self.ready = False await self.flush() # Instantiate and init a driver for the host if needed. # NOTE: we don't keep a reference to the driver here, because we don't # currently have a need for the driver later on. But if the driver interface # evolves, it may be required, then, to store a reference to the driver in # an object property. reset_needed = True if driver_factory is not None: if driver := await driver_factory(self): await driver.init_controller() reset_needed = False # Send a reset command unless a driver has already done so. if reset_needed: await self.send_command(hci.HCI_Reset_Command(), check_result=True) self.ready = True response = await self.send_command( hci.HCI_Read_Local_Supported_Commands_Command(), check_result=True ) self.local_supported_commands = int.from_bytes( response.return_parameters.supported_commands, 'little' ) if self.supports_command(hci.HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND): response = await self.send_command( hci.HCI_LE_Read_Local_Supported_Features_Command(), check_result=True ) self.local_le_features = struct.unpack( ' Optional[TransportSink]: return self.hci_sink @controller.setter def controller(self, controller) -> None: self.set_packet_sink(controller) if controller: self.set_packet_source(controller) def set_packet_sink(self, sink: Optional[TransportSink]) -> None: self.hci_sink = sink def set_packet_source(self, source: TransportSource) -> None: source.set_packet_sink(self) self.hci_metadata = getattr(source, 'metadata', self.hci_metadata) def send_hci_packet(self, packet: hci.HCI_Packet) -> None: logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {packet}') if self.snooper: self.snooper.snoop(bytes(packet), Snooper.Direction.HOST_TO_CONTROLLER) if self.hci_sink: self.hci_sink.on_packet(bytes(packet)) async def send_command( self, command, check_result=False, response_timeout: Optional[int] = None ): # Wait until we can send (only one pending command at a time) async with self.command_semaphore: assert self.pending_command is None assert self.pending_response is None # Create a future value to hold the eventual response self.pending_response = asyncio.get_running_loop().create_future() self.pending_command = command try: self.send_hci_packet(command) await asyncio.wait_for(self.pending_response, timeout=response_timeout) response = self.pending_response.result() # Check the return parameters if required if check_result: if isinstance(response, hci.HCI_Command_Status_Event): status = response.status # type: ignore[attr-defined] elif isinstance(response.return_parameters, int): status = response.return_parameters elif isinstance(response.return_parameters, bytes): # return parameters first field is a one byte status code status = response.return_parameters[0] else: status = response.return_parameters.status if status != hci.HCI_SUCCESS: logger.warning( f'{command.name} failed ' f'({hci.HCI_Constant.error_name(status)})' ) raise hci.HCI_Error(status) return response except Exception as error: logger.exception( f'{color("!!! Exception while sending command:", "red")} {error}' ) raise error finally: self.pending_command = None self.pending_response = None # Use this method to send a command from a task def send_command_sync(self, command: hci.HCI_Command) -> None: async def send_command(command: hci.HCI_Command) -> None: await self.send_command(command) asyncio.create_task(send_command(command)) def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None: if not (connection := self.connections.get(connection_handle)): logger.warning(f'connection 0x{connection_handle:04X} not found') return packet_queue = connection.acl_packet_queue if packet_queue is None: logger.warning( f'no ACL packet queue for connection 0x{connection_handle:04X}' ) return # Create a PDU l2cap_pdu = bytes(L2CAP_PDU(cid, pdu)) # Send the data to the controller via ACL packets bytes_remaining = len(l2cap_pdu) offset = 0 pb_flag = 0 while bytes_remaining: data_total_length = min(bytes_remaining, packet_queue.max_packet_size) acl_packet = hci.HCI_AclDataPacket( connection_handle=connection_handle, pb_flag=pb_flag, bc_flag=0, data_total_length=data_total_length, data=l2cap_pdu[offset : offset + data_total_length], ) logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}') packet_queue.enqueue(acl_packet, connection_handle) pb_flag = 1 offset += data_total_length bytes_remaining -= data_total_length def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None: if connection := self.connections.get(connection_handle): return connection.acl_packet_queue if iso_link := self.cis_links.get(connection_handle) or self.bis_links.get( connection_handle ): return iso_link.packet_queue return None def send_iso_sdu(self, connection_handle: int, sdu: bytes) -> None: if not ( iso_link := self.cis_links.get(connection_handle) or self.bis_links.get(connection_handle) ): logger.warning(f"no ISO link for connection handle {connection_handle}") return if iso_link.packet_queue is None: logger.warning("ISO link has no data packet queue") return bytes_remaining = len(sdu) offset = 0 while bytes_remaining: is_first_fragment = offset == 0 header_length = 4 if is_first_fragment else 0 assert iso_link.packet_queue.max_packet_size > header_length fragment_length = min( bytes_remaining, iso_link.packet_queue.max_packet_size - header_length ) is_last_fragment = bytes_remaining == fragment_length iso_sdu_fragment = sdu[offset : offset + fragment_length] iso_link.packet_queue.enqueue( ( hci.HCI_IsoDataPacket( connection_handle=connection_handle, data_total_length=header_length + fragment_length, packet_sequence_number=iso_link.packet_sequence_number, pb_flag=0b10 if is_last_fragment else 0b00, packet_status_flag=0, iso_sdu_length=len(sdu), iso_sdu_fragment=iso_sdu_fragment, ) if is_first_fragment else hci.HCI_IsoDataPacket( connection_handle=connection_handle, data_total_length=fragment_length, pb_flag=0b11 if is_last_fragment else 0b01, iso_sdu_fragment=iso_sdu_fragment, ) ), connection_handle, ) offset += fragment_length bytes_remaining -= fragment_length iso_link.packet_sequence_number = (iso_link.packet_sequence_number + 1) & 0xFFFF def remove_big(self, big_handle: int) -> None: if big := self.bigs.pop(big_handle, None): for connection_handle in big: if bis_link := self.bis_links.pop(connection_handle, None): bis_link.packet_queue.flush(bis_link.handle) def supports_command(self, op_code: int) -> bool: return ( self.local_supported_commands & hci.HCI_SUPPORTED_COMMANDS_MASKS.get(op_code, 0) ) != 0 @property def supported_commands(self) -> set[int]: return set( op_code for op_code, mask in hci.HCI_SUPPORTED_COMMANDS_MASKS.items() if self.local_supported_commands & mask ) def supports_le_features(self, feature: hci.LeFeatureMask) -> bool: return (self.local_le_features & feature) == feature def supports_lmp_features(self, feature: hci.LmpFeatureMask) -> bool: return self.local_lmp_features & (feature) == feature @property def supported_le_features(self): return [ feature for feature in range(64) if self.local_le_features & (1 << feature) ] # Packet Sink protocol (packets coming from the controller via HCI) def on_packet(self, packet: bytes) -> None: try: hci_packet = hci.HCI_Packet.from_bytes(packet) except Exception: logger.exception('!!! error parsing packet from bytes') return if self.ready or ( isinstance(hci_packet, hci.HCI_Command_Complete_Event) and hci_packet.command_opcode == hci.HCI_RESET_COMMAND ): self.on_hci_packet(hci_packet) else: logger.debug( f'reset not done, ignoring packet from controller: {hci_packet}' ) def on_transport_lost(self): # Called by the source when the transport has been lost. if self.pending_response: self.pending_response.set_exception(TransportLostError('transport lost')) self.emit('flush') def on_hci_packet(self, packet: hci.HCI_Packet) -> None: logger.debug(f'{color("### CONTROLLER -> HOST", "green")}: {packet}') if self.snooper: self.snooper.snoop(bytes(packet), Snooper.Direction.CONTROLLER_TO_HOST) # If the packet is a command, invoke the handler for this packet if packet.hci_packet_type == hci.HCI_COMMAND_PACKET: self.on_hci_command_packet(cast(hci.HCI_Command, packet)) elif packet.hci_packet_type == hci.HCI_EVENT_PACKET: self.on_hci_event_packet(cast(hci.HCI_Event, packet)) elif packet.hci_packet_type == hci.HCI_ACL_DATA_PACKET: self.on_hci_acl_data_packet(cast(hci.HCI_AclDataPacket, packet)) elif packet.hci_packet_type == hci.HCI_SYNCHRONOUS_DATA_PACKET: self.on_hci_sco_data_packet(cast(hci.HCI_SynchronousDataPacket, packet)) elif packet.hci_packet_type == hci.HCI_ISO_DATA_PACKET: self.on_hci_iso_data_packet(cast(hci.HCI_IsoDataPacket, packet)) else: logger.warning(f'!!! unknown packet type {packet.hci_packet_type}') def on_hci_command_packet(self, command: hci.HCI_Command) -> None: logger.warning(f'!!! unexpected command packet: {command}') def on_hci_event_packet(self, event: hci.HCI_Event) -> None: handler_name = f'on_{event.name.lower()}' handler = getattr(self, handler_name, self.on_hci_event) handler(event) def on_hci_acl_data_packet(self, packet: hci.HCI_AclDataPacket) -> None: # Look for the connection to which this data belongs if connection := self.connections.get(packet.connection_handle): connection.on_hci_acl_data_packet(packet) def on_hci_sco_data_packet(self, packet: hci.HCI_SynchronousDataPacket) -> None: # Experimental self.emit('sco_packet', packet.connection_handle, packet) def on_hci_iso_data_packet(self, packet: hci.HCI_IsoDataPacket) -> None: # Experimental self.emit('iso_packet', packet.connection_handle, packet) def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None: self.emit('l2cap_pdu', connection.handle, cid, pdu) def on_command_processed(self, event): if self.pending_response: # Check that it is what we were expecting if self.pending_command.op_code != event.command_opcode: logger.warning( '!!! command result mismatch, expected ' f'0x{self.pending_command.op_code:X} but got ' f'0x{event.command_opcode:X}' ) self.pending_response.set_result(event) else: logger.warning('!!! no pending response future to set') ############################################################ # HCI handlers ############################################################ def on_hci_event(self, event): logger.warning(f'{color(f"--- Ignoring event {event}", "red")}') def on_hci_command_complete_event(self, event): if event.command_opcode == 0: # This is used just for the Num_HCI_Command_Packets field, not related to # an actual command logger.debug('no-command event') return return self.on_command_processed(event) def on_hci_command_status_event(self, event): return self.on_command_processed(event) def on_hci_number_of_completed_packets_event( self, event: hci.HCI_Number_Of_Completed_Packets_Event ) -> None: for connection_handle, num_completed_packets in zip( event.connection_handles, event.num_completed_packets ): if queue := self.get_data_packet_queue(connection_handle): queue.on_packets_completed(num_completed_packets, connection_handle) continue if connection_handle not in self.sco_links: logger.warning( 'received packet completion event for unknown handle ' f'0x{connection_handle:04X}' ) # Classic only def on_hci_connection_request_event(self, event): # Notify the listeners self.emit( 'connection_request', event.bd_addr, event.class_of_device, event.link_type, ) def on_hci_le_connection_complete_event(self, event): # Check if this is a cancellation if event.status == hci.HCI_SUCCESS: # Create/update the connection logger.debug( f'### LE CONNECTION: [0x{event.connection_handle:04X}] ' f'{event.peer_address} as {hci.HCI_Constant.role_name(event.role)}' ) connection = self.connections.get(event.connection_handle) if connection is None: connection = Connection( self, event.connection_handle, event.peer_address, PhysicalTransport.LE, ) self.connections[event.connection_handle] = connection # Notify the client connection_parameters = ConnectionParameters( event.connection_interval, event.peripheral_latency, event.supervision_timeout, ) self.emit( 'connection', event.connection_handle, PhysicalTransport.LE, event.peer_address, getattr(event, 'local_resolvable_private_address', None), getattr(event, 'peer_resolvable_private_address', None), hci.Role(event.role), connection_parameters, ) else: logger.debug(f'### CONNECTION FAILED: {event.status}') # Notify the listeners self.emit( 'connection_failure', PhysicalTransport.LE, event.peer_address, event.status, ) def on_hci_le_enhanced_connection_complete_event(self, event): # Just use the same implementation as for the non-enhanced event for now self.on_hci_le_connection_complete_event(event) def on_hci_le_enhanced_connection_complete_v2_event(self, event): # Just use the same implementation as for the v1 event for now self.on_hci_le_enhanced_connection_complete_event(event) def on_hci_connection_complete_event(self, event): if event.status == hci.HCI_SUCCESS: # Create/update the connection logger.debug( f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] ' f'{event.bd_addr}' ) connection = self.connections.get(event.connection_handle) if connection is None: connection = Connection( self, event.connection_handle, event.bd_addr, PhysicalTransport.BR_EDR, ) self.connections[event.connection_handle] = connection # Notify the client self.emit( 'connection', event.connection_handle, PhysicalTransport.BR_EDR, event.bd_addr, None, None, None, None, ) else: logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}') # Notify the client self.emit( 'connection_failure', PhysicalTransport.BR_EDR, event.bd_addr, event.status, ) def on_hci_disconnection_complete_event(self, event): # Find the connection handle = event.connection_handle if ( connection := ( self.connections.get(handle) or self.cis_links.get(handle) or self.sco_links.get(handle) ) ) is None: logger.warning('!!! DISCONNECTION COMPLETE: unknown handle') return if event.status == hci.HCI_SUCCESS: logger.debug(f'### DISCONNECTION: {connection}, reason={event.reason}') # Notify the listeners self.emit('disconnection', handle, event.reason) # Remove the handle reference _ = ( self.connections.pop(handle, 0) or self.cis_links.pop(handle, 0) or self.sco_links.pop(handle, 0) ) # Flush the data queues if self.acl_packet_queue: self.acl_packet_queue.flush(handle) if self.le_acl_packet_queue: self.le_acl_packet_queue.flush(handle) if self.iso_packet_queue: self.iso_packet_queue.flush(handle) else: logger.debug(f'### DISCONNECTION FAILED: {event.status}') # Notify the listeners self.emit('disconnection_failure', handle, event.status) def on_hci_le_connection_update_complete_event(self, event): if (connection := self.connections.get(event.connection_handle)) is None: logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle') return # Notify the client if event.status == hci.HCI_SUCCESS: connection_parameters = ConnectionParameters( event.connection_interval, event.peripheral_latency, event.supervision_timeout, ) self.emit( 'connection_parameters_update', connection.handle, connection_parameters ) else: self.emit( 'connection_parameters_update_failure', connection.handle, event.status ) def on_hci_le_phy_update_complete_event(self, event): if (connection := self.connections.get(event.connection_handle)) is None: logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle') return # Notify the client if event.status == hci.HCI_SUCCESS: self.emit( 'connection_phy_update', connection.handle, ConnectionPHY(event.tx_phy, event.rx_phy), ) else: self.emit('connection_phy_update_failure', connection.handle, event.status) def on_hci_le_advertising_report_event( self, event: ( hci.HCI_LE_Advertising_Report_Event | hci.HCI_LE_Extended_Advertising_Report_Event ), ): for report in event.reports: self.emit('advertising_report', report) def on_hci_le_extended_advertising_report_event( self, event: hci.HCI_LE_Extended_Advertising_Report_Event ): self.on_hci_le_advertising_report_event(event) def on_hci_le_advertising_set_terminated_event(self, event): self.emit( 'advertising_set_termination', event.status, event.advertising_handle, event.connection_handle, event.num_completed_extended_advertising_events, ) def on_hci_le_periodic_advertising_sync_established_event(self, event): self.emit( 'periodic_advertising_sync_establishment', event.status, event.sync_handle, event.advertising_sid, event.advertiser_address, event.advertiser_phy, event.periodic_advertising_interval, event.advertiser_clock_accuracy, ) def on_hci_le_periodic_advertising_sync_lost_event(self, event): self.emit('periodic_advertising_sync_loss', event.sync_handle) def on_hci_le_periodic_advertising_report_event(self, event): self.emit('periodic_advertising_report', event.sync_handle, event) def on_hci_le_biginfo_advertising_report_event(self, event): self.emit('biginfo_advertising_report', event.sync_handle, event) def on_hci_le_cis_request_event(self, event): self.emit( 'cis_request', event.acl_connection_handle, event.cis_connection_handle, event.cig_id, event.cis_id, ) def on_hci_le_create_big_complete_event(self, event): self.bigs[event.big_handle] = set(event.connection_handle) if self.iso_packet_queue is None: logger.warning("BIS established but ISO packets not supported") for connection_handle in event.connection_handle: self.bis_links[connection_handle] = IsoLink( connection_handle, self.iso_packet_queue ) self.emit( 'big_establishment', event.status, event.big_handle, event.connection_handle, event.big_sync_delay, event.transport_latency_big, event.phy, event.nse, event.bn, event.pto, event.irc, event.max_pdu, event.iso_interval, ) def on_hci_le_big_sync_established_event(self, event): self.bigs[event.big_handle] = set(event.connection_handle) for connection_handle in event.connection_handle: self.bis_links[connection_handle] = IsoLink( connection_handle, self.iso_packet_queue ) self.emit( 'big_sync_establishment', event.status, event.big_handle, event.transport_latency_big, event.nse, event.bn, event.pto, event.irc, event.max_pdu, event.iso_interval, event.connection_handle, ) def on_hci_le_big_sync_lost_event(self, event): self.remove_big(event.big_handle) self.emit('big_sync_lost', event.big_handle, event.reason) def on_hci_le_terminate_big_complete_event(self, event): self.remove_big(event.big_handle) self.emit('big_termination', event.reason, event.big_handle) def on_hci_le_periodic_advertising_sync_transfer_received_event(self, event): self.emit( 'periodic_advertising_sync_transfer', event.status, event.connection_handle, event.sync_handle, event.advertising_sid, event.advertiser_address, event.advertiser_phy, event.periodic_advertising_interval, event.advertiser_clock_accuracy, ) def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(self, event): self.emit( 'periodic_advertising_sync_transfer', event.status, event.connection_handle, event.sync_handle, event.advertising_sid, event.advertiser_address, event.advertiser_phy, event.periodic_advertising_interval, event.advertiser_clock_accuracy, ) def on_hci_le_cis_established_event(self, event): # The remaining parameters are unused for now. if event.status == hci.HCI_SUCCESS: if self.iso_packet_queue is None: logger.warning("CIS established but ISO packets not supported") self.cis_links[event.connection_handle] = IsoLink( handle=event.connection_handle, packet_queue=self.iso_packet_queue ) self.emit( 'cis_establishment', event.connection_handle, event.cig_sync_delay, event.cis_sync_delay, event.transport_latency_c_to_p, event.transport_latency_p_to_c, event.phy_c_to_p, event.phy_p_to_c, event.nse, event.bn_c_to_p, event.bn_p_to_c, event.ft_c_to_p, event.ft_p_to_c, event.max_pdu_c_to_p, event.max_pdu_p_to_c, event.iso_interval, ) else: self.emit( 'cis_establishment_failure', event.connection_handle, event.status ) def on_hci_le_remote_connection_parameter_request_event(self, event): if event.connection_handle not in self.connections: logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle') return # For now, just accept everything # TODO: delegate the decision self.send_command_sync( hci.HCI_LE_Remote_Connection_Parameter_Request_Reply_Command( connection_handle=event.connection_handle, interval_min=event.interval_min, interval_max=event.interval_max, max_latency=event.max_latency, timeout=event.timeout, min_ce_length=0, max_ce_length=0, ) ) def on_hci_le_long_term_key_request_event(self, event): if (connection := self.connections.get(event.connection_handle)) is None: logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle') return async def send_long_term_key(): if self.long_term_key_provider is None: logger.debug('no long term key provider') long_term_key = None else: long_term_key = await utils.cancel_on_event( self, 'flush', # pylint: disable-next=not-callable self.long_term_key_provider( connection.handle, event.random_number, event.encryption_diversifier, ), ) if long_term_key: response = hci.HCI_LE_Long_Term_Key_Request_Reply_Command( connection_handle=event.connection_handle, long_term_key=long_term_key, ) else: response = hci.HCI_LE_Long_Term_Key_Request_Negative_Reply_Command( connection_handle=event.connection_handle ) await self.send_command(response) asyncio.create_task(send_long_term_key()) def on_hci_synchronous_connection_complete_event(self, event): if event.status == hci.HCI_SUCCESS: # Create/update the connection logger.debug( f'### SCO CONNECTION: [0x{event.connection_handle:04X}] ' f'{event.bd_addr}' ) self.sco_links[event.connection_handle] = ScoLink( peer_address=event.bd_addr, connection_handle=event.connection_handle, ) # Notify the client self.emit( 'sco_connection', event.bd_addr, event.connection_handle, event.link_type, ) else: logger.debug(f'### SCO CONNECTION FAILED: {event.status}') # Notify the client self.emit('sco_connection_failure', event.bd_addr, event.status) def on_hci_synchronous_connection_changed_event(self, event): pass def on_hci_role_change_event(self, event): if event.status == hci.HCI_SUCCESS: logger.debug( f'role change for {event.bd_addr}: ' f'{hci.HCI_Constant.role_name(event.new_role)}' ) self.emit('role_change', event.bd_addr, hci.Role(event.new_role)) else: logger.debug( f'role change for {event.bd_addr} failed: ' f'{hci.HCI_Constant.error_name(event.status)}' ) self.emit('role_change_failure', event.bd_addr, event.status) def on_hci_le_data_length_change_event(self, event): if (connection := self.connections.get(event.connection_handle)) is None: logger.warning('!!! DATA LENGTH CHANGE: unknown handle') return self.emit( 'connection_data_length_change', event.connection_handle, event.max_tx_octets, event.max_tx_time, event.max_rx_octets, event.max_rx_time, ) def on_hci_authentication_complete_event(self, event): # Notify the client if event.status == hci.HCI_SUCCESS: self.emit('connection_authentication', event.connection_handle) else: self.emit( 'connection_authentication_failure', event.connection_handle, event.status, ) def on_hci_encryption_change_event(self, event: hci.HCI_Encryption_Change_Event): # Notify the client if event.status == hci.HCI_SUCCESS: self.emit( 'connection_encryption_change', event.connection_handle, event.encryption_enabled, 0, ) else: self.emit( 'connection_encryption_failure', event.connection_handle, event.status ) def on_hci_encryption_change_v2_event( self, event: hci.HCI_Encryption_Change_V2_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: self.emit( 'connection_encryption_failure', event.connection_handle, event.status ) def on_hci_encryption_key_refresh_complete_event(self, event): # Notify the client if event.status == hci.HCI_SUCCESS: self.emit('connection_encryption_key_refresh', event.connection_handle) else: self.emit( 'connection_encryption_key_refresh_failure', event.connection_handle, event.status, ) def on_hci_qos_setup_complete_event(self, event): if event.status == hci.HCI_SUCCESS: self.emit( 'connection_qos_setup', event.connection_handle, event.service_type ) else: self.emit( 'connection_qos_setup_failure', event.connection_handle, event.status, ) def on_hci_link_supervision_timeout_changed_event(self, event): pass def on_hci_max_slots_change_event(self, event): pass def on_hci_page_scan_repetition_mode_change_event(self, event): pass def on_hci_link_key_notification_event(self, event): logger.debug( f'link key for {event.bd_addr}: {event.link_key.hex()}, ' f'type={hci.HCI_Constant.link_key_type_name(event.key_type)}' ) self.emit('link_key', event.bd_addr, event.link_key, event.key_type) def on_hci_simple_pairing_complete_event(self, event): logger.debug( f'simple pairing complete for {event.bd_addr}: ' f'status={hci.HCI_Constant.status_name(event.status)}' ) if event.status == hci.HCI_SUCCESS: self.emit('classic_pairing', event.bd_addr) else: self.emit('classic_pairing_failure', event.bd_addr, event.status) def on_hci_pin_code_request_event(self, event): self.emit('pin_code_request', event.bd_addr) def on_hci_link_key_request_event(self, event): async def send_link_key(): if self.link_key_provider is None: logger.debug('no link key provider') link_key = None else: link_key = await utils.cancel_on_event( self, 'flush', # pylint: disable-next=not-callable self.link_key_provider(event.bd_addr), ) if link_key: response = hci.HCI_Link_Key_Request_Reply_Command( bd_addr=event.bd_addr, link_key=link_key ) else: response = hci.HCI_Link_Key_Request_Negative_Reply_Command( bd_addr=event.bd_addr ) await self.send_command(response) asyncio.create_task(send_link_key()) def on_hci_io_capability_request_event(self, event): self.emit('authentication_io_capability_request', event.bd_addr) def on_hci_io_capability_response_event(self, event): self.emit( 'authentication_io_capability_response', event.bd_addr, event.io_capability, event.authentication_requirements, ) def on_hci_user_confirmation_request_event(self, event): self.emit( 'authentication_user_confirmation_request', event.bd_addr, event.numeric_value, ) def on_hci_user_passkey_request_event(self, event): self.emit('authentication_user_passkey_request', event.bd_addr) def on_hci_user_passkey_notification_event(self, event): self.emit( 'authentication_user_passkey_notification', event.bd_addr, event.passkey ) def on_hci_inquiry_complete_event(self, _event): self.emit('inquiry_complete') def on_hci_inquiry_result_with_rssi_event(self, event): for bd_addr, class_of_device, rssi in zip( event.bd_addr, event.class_of_device, event.rssi ): self.emit( 'inquiry_result', bd_addr, class_of_device, b'', rssi, ) def on_hci_extended_inquiry_result_event(self, event): self.emit( 'inquiry_result', event.bd_addr, event.class_of_device, event.extended_inquiry_response, event.rssi, ) def on_hci_remote_name_request_complete_event(self, event): if event.status != hci.HCI_SUCCESS: self.emit('remote_name_failure', event.bd_addr, event.status) else: utf8_name = event.remote_name terminator = utf8_name.find(0) if terminator >= 0: utf8_name = utf8_name[0:terminator] self.emit('remote_name', event.bd_addr, utf8_name) def on_hci_remote_host_supported_features_notification_event(self, event): self.emit( 'remote_host_supported_features', event.bd_addr, event.host_supported_features, ) def on_hci_le_read_remote_features_complete_event(self, event): if event.status != hci.HCI_SUCCESS: self.emit( 'le_remote_features_failure', event.connection_handle, event.status ) else: self.emit( 'le_remote_features', event.connection_handle, int.from_bytes(event.le_features, 'little'), ) def on_hci_le_cs_read_remote_supported_capabilities_complete_event(self, event): self.emit('cs_remote_supported_capabilities', event) def on_hci_le_cs_security_enable_complete_event(self, event): self.emit('cs_security', event) def on_hci_le_cs_config_complete_event(self, event): self.emit('cs_config', event) def on_hci_le_cs_procedure_enable_complete_event(self, event): self.emit('cs_procedure', event) def on_hci_le_cs_subevent_result_event(self, event): self.emit('cs_subevent_result', event) def on_hci_le_cs_subevent_result_continue_event(self, event): self.emit('cs_subevent_result_continue', event) def on_hci_vendor_event(self, event): self.emit('vendor_event', event)