Make all event emitters async

* Also remove AbortableEventEmitter
This commit is contained in:
Josh Wu
2025-04-12 22:53:32 +08:00
parent 6cecc16519
commit 55801bc2ca
32 changed files with 432 additions and 395 deletions

View File

@@ -49,7 +49,6 @@ from typing import (
)
from typing_extensions import Self
from pyee import EventEmitter
from bumble.colors import color
from bumble.att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
@@ -71,15 +70,7 @@ from bumble.core import (
OutOfResourcesError,
UnreachableError,
)
from bumble.utils import (
AsyncRunner,
CompositeEventEmitter,
EventWatcher,
setup_event_forwarding,
composite_listener,
deprecated,
experimental,
)
from bumble import utils
from bumble.keys import (
KeyStore,
PairingKeys,
@@ -576,7 +567,7 @@ class PeriodicAdvertisingParameters:
# -----------------------------------------------------------------------------
@dataclass
class AdvertisingSet(EventEmitter):
class AdvertisingSet(utils.EventEmitter):
device: Device
advertising_handle: int
auto_restart: bool
@@ -810,7 +801,7 @@ class AdvertisingSet(EventEmitter):
# -----------------------------------------------------------------------------
class PeriodicAdvertisingSync(EventEmitter):
class PeriodicAdvertisingSync(utils.EventEmitter):
class State(Enum):
INIT = 0
PENDING = 1
@@ -939,7 +930,7 @@ class PeriodicAdvertisingSync(EventEmitter):
"received established event for cancelled sync, will terminate"
)
self.state = self.State.ESTABLISHED
AsyncRunner.spawn(self.terminate())
utils.AsyncRunner.spawn(self.terminate())
return
if status == hci.HCI_SUCCESS:
@@ -1025,7 +1016,7 @@ class BigParameters:
# -----------------------------------------------------------------------------
@dataclass
class Big(EventEmitter):
class Big(utils.EventEmitter):
class State(IntEnum):
PENDING = 0
ACTIVE = 1
@@ -1065,7 +1056,7 @@ class Big(EventEmitter):
logger.error('BIG %d is not active.', self.big_handle)
return
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
terminated = asyncio.Event()
watcher.once(self, Big.Event.TERMINATION, lambda _: terminated.set())
await self.device.send_command(
@@ -1088,7 +1079,7 @@ class BigSyncParameters:
# -----------------------------------------------------------------------------
@dataclass
class BigSync(EventEmitter):
class BigSync(utils.EventEmitter):
class State(IntEnum):
PENDING = 0
ACTIVE = 1
@@ -1123,7 +1114,7 @@ class BigSync(EventEmitter):
logger.error('BIG Sync %d is not active.', self.big_handle)
return
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
terminated = asyncio.Event()
watcher.once(self, BigSync.Event.TERMINATION, lambda _: terminated.set())
await self.device.send_command(
@@ -1392,7 +1383,7 @@ ConnectionParametersPreferences.default = ConnectionParametersPreferences()
# -----------------------------------------------------------------------------
@dataclass
class ScoLink(CompositeEventEmitter):
class ScoLink(utils.CompositeEventEmitter):
device: Device
acl_connection: Connection
handle: int
@@ -1483,7 +1474,7 @@ class _IsoLink:
# -----------------------------------------------------------------------------
@dataclass
class CisLink(CompositeEventEmitter, _IsoLink):
class CisLink(utils.EventEmitter, _IsoLink):
class State(IntEnum):
PENDING = 0
ESTABLISHED = 1
@@ -1560,7 +1551,7 @@ class IsoPacketStream:
# -----------------------------------------------------------------------------
class Connection(CompositeEventEmitter):
class Connection(utils.CompositeEventEmitter):
device: Device
handle: int
transport: core.PhysicalTransport
@@ -1580,7 +1571,7 @@ class Connection(CompositeEventEmitter):
cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures
@composite_listener
@utils.composite_listener
class Listener:
def on_disconnection(self, reason):
pass
@@ -1699,7 +1690,7 @@ class Connection(CompositeEventEmitter):
def send_l2cap_pdu(self, cid: int, pdu: bytes) -> None:
self.device.send_l2cap_pdu(self.handle, cid, pdu)
@deprecated("Please use create_l2cap_channel()")
@utils.deprecated("Please use create_l2cap_channel()")
async def open_l2cap_channel(
self,
psm,
@@ -1753,7 +1744,9 @@ class Connection(CompositeEventEmitter):
self.on('disconnection_failure', abort.set_exception)
try:
await asyncio.wait_for(self.device.abort_on('flush', abort), timeout)
await asyncio.wait_for(
utils.cancel_on_event(self.device, 'flush', abort), timeout
)
finally:
self.remove_listener('disconnection', abort.set_result)
self.remove_listener('disconnection_failure', abort.set_exception)
@@ -2037,7 +2030,7 @@ device_host_event_handlers: list[str] = []
# -----------------------------------------------------------------------------
class Device(CompositeEventEmitter):
class Device(utils.CompositeEventEmitter):
# Incomplete list of fields.
random_address: hci.Address # Random private address that may change periodically
public_address: (
@@ -2069,7 +2062,7 @@ class Device(CompositeEventEmitter):
_pending_cis: Dict[int, tuple[int, int]]
gatt_service: gatt_service.GenericAttributeProfileService | None = None
@composite_listener
@utils.composite_listener
class Listener:
def on_advertisement(self, advertisement):
pass
@@ -2290,7 +2283,9 @@ class Device(CompositeEventEmitter):
self.l2cap_channel_manager.register_fixed_channel(ATT_CID, self.on_gatt_pdu)
# Forward some events
setup_event_forwarding(self.gatt_server, self, 'characteristic_subscription')
utils.setup_event_forwarding(
self.gatt_server, self, 'characteristic_subscription'
)
# Set the initial host
if host:
@@ -2379,11 +2374,11 @@ class Device(CompositeEventEmitter):
None,
)
@deprecated("Please use create_l2cap_server()")
@utils.deprecated("Please use create_l2cap_server()")
def register_l2cap_server(self, psm, server) -> int:
return self.l2cap_channel_manager.register_server(psm, server)
@deprecated("Please use create_l2cap_server()")
@utils.deprecated("Please use create_l2cap_server()")
def register_l2cap_channel_server(
self,
psm,
@@ -2396,7 +2391,7 @@ class Device(CompositeEventEmitter):
psm, server, max_credits, mtu, mps
)
@deprecated("Please use create_l2cap_channel()")
@utils.deprecated("Please use create_l2cap_channel()")
async def open_l2cap_channel(
self,
connection,
@@ -3237,7 +3232,7 @@ class Device(CompositeEventEmitter):
advertiser_clock_accuracy,
)
AsyncRunner.spawn(self._update_periodic_advertising_syncs())
utils.AsyncRunner.spawn(self._update_periodic_advertising_syncs())
return
@@ -3667,7 +3662,7 @@ class Device(CompositeEventEmitter):
self.le_connecting = True
if timeout is None:
return await self.abort_on('flush', pending_connection)
return await utils.cancel_on_event(self, 'flush', pending_connection)
try:
return await asyncio.wait_for(
@@ -3684,7 +3679,9 @@ class Device(CompositeEventEmitter):
)
try:
return await self.abort_on('flush', pending_connection)
return await utils.cancel_on_event(
self, 'flush', pending_connection
)
except core.ConnectionError as error:
raise core.TimeoutError() from error
finally:
@@ -3740,7 +3737,7 @@ class Device(CompositeEventEmitter):
try:
# Wait for a request or a completed connection
pending_request = self.abort_on('flush', pending_request_fut)
pending_request = utils.cancel_on_event(self, 'flush', pending_request_fut)
result = await (
asyncio.wait_for(pending_request, timeout)
if timeout
@@ -3802,7 +3799,7 @@ class Device(CompositeEventEmitter):
)
# Wait for connection complete
return await self.abort_on('flush', pending_connection)
return await utils.cancel_on_event(self, 'flush', pending_connection)
finally:
self.remove_listener('connection', on_connection)
@@ -3876,7 +3873,7 @@ class Device(CompositeEventEmitter):
# Wait for the disconnection process to complete
self.disconnecting = True
return await self.abort_on('flush', pending_disconnection)
return await utils.cancel_on_event(self, 'flush', pending_disconnection)
finally:
connection.remove_listener(
'disconnection', pending_disconnection.set_result
@@ -4075,7 +4072,7 @@ class Device(CompositeEventEmitter):
else:
return None
return await self.abort_on('flush', peer_address)
return await utils.cancel_on_event(self, 'flush', peer_address)
finally:
if listener is not None:
self.remove_listener(event_name, listener)
@@ -4125,7 +4122,7 @@ class Device(CompositeEventEmitter):
if not self.scanning:
await self.start_scanning(filter_duplicates=True)
return await self.abort_on('flush', peer_address)
return await utils.cancel_on_event(self, 'flush', peer_address)
finally:
if listener is not None:
self.remove_listener(event_name, listener)
@@ -4229,7 +4226,9 @@ class Device(CompositeEventEmitter):
raise hci.HCI_StatusError(result)
# Wait for the authentication to complete
await connection.abort_on('disconnection', pending_authentication)
await utils.cancel_on_event(
connection, 'disconnection', pending_authentication
)
finally:
connection.remove_listener('connection_authentication', on_authentication)
connection.remove_listener(
@@ -4309,7 +4308,7 @@ class Device(CompositeEventEmitter):
raise hci.HCI_StatusError(result)
# Wait for the result
await connection.abort_on('disconnection', pending_encryption)
await utils.cancel_on_event(connection, 'disconnection', pending_encryption)
finally:
connection.remove_listener(
'connection_encryption_change', on_encryption_change
@@ -4353,7 +4352,9 @@ class Device(CompositeEventEmitter):
f'{hci.HCI_Constant.error_name(result.status)}'
)
raise hci.HCI_StatusError(result)
await connection.abort_on('disconnection', pending_role_change)
await utils.cancel_on_event(
connection, 'disconnection', pending_role_change
)
finally:
connection.remove_listener('role_change', on_role_change)
connection.remove_listener('role_change_failure', on_role_change_failure)
@@ -4402,13 +4403,13 @@ class Device(CompositeEventEmitter):
raise hci.HCI_StatusError(result)
# Wait for the result
return await self.abort_on('flush', pending_name)
return await utils.cancel_on_event(self, 'flush', pending_name)
finally:
self.remove_listener('remote_name', handler)
self.remove_listener('remote_name_failure', failure_handler)
# [LE only]
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def setup_cig(
self,
cig_id: int,
@@ -4466,7 +4467,7 @@ class Device(CompositeEventEmitter):
return cis_handles
# [LE only]
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def create_cis(
self, cis_acl_pairs: Sequence[tuple[int, int]]
) -> list[CisLink]:
@@ -4482,7 +4483,7 @@ class Device(CompositeEventEmitter):
cig_id=cig_id,
)
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
pending_cis_establishments = {
cis_handle: asyncio.get_running_loop().create_future()
for cis_handle, _ in cis_acl_pairs
@@ -4509,7 +4510,7 @@ class Device(CompositeEventEmitter):
return await asyncio.gather(*pending_cis_establishments.values())
# [LE only]
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def accept_cis_request(self, handle: int) -> CisLink:
"""[LE Only] Accepts an incoming CIS request.
@@ -4531,7 +4532,7 @@ class Device(CompositeEventEmitter):
if cis_link.state == CisLink.State.ESTABLISHED:
return cis_link
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
pending_establishment = asyncio.get_running_loop().create_future()
def on_establishment() -> None:
@@ -4555,7 +4556,7 @@ class Device(CompositeEventEmitter):
raise UnreachableError()
# [LE only]
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def reject_cis_request(
self,
handle: int,
@@ -4569,14 +4570,14 @@ class Device(CompositeEventEmitter):
)
# [LE only]
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def create_big(
self, advertising_set: AdvertisingSet, parameters: BigParameters
) -> Big:
if (big_handle := self.next_big_handle()) is None:
raise core.OutOfResourcesError("All valid BIG handles already in use")
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
big = Big(
big_handle=big_handle,
parameters=parameters,
@@ -4619,7 +4620,7 @@ class Device(CompositeEventEmitter):
return big
# [LE only]
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def create_big_sync(
self, pa_sync: PeriodicAdvertisingSync, parameters: BigSyncParameters
) -> BigSync:
@@ -4629,7 +4630,7 @@ class Device(CompositeEventEmitter):
if (pa_sync_handle := pa_sync.sync_handle) is None:
raise core.InvalidStateError("PA Sync is not established")
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
big_sync = BigSync(
big_handle=big_handle,
parameters=parameters,
@@ -4677,7 +4678,7 @@ class Device(CompositeEventEmitter):
Returns:
LE features supported by the remote device.
"""
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
read_feature_future: asyncio.Future[hci.LeFeatureMask] = (
asyncio.get_running_loop().create_future()
)
@@ -4700,7 +4701,7 @@ class Device(CompositeEventEmitter):
)
return await read_feature_future
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def get_remote_cs_capabilities(
self, connection: Connection
) -> ChannelSoundingCapabilities:
@@ -4708,7 +4709,7 @@ class Device(CompositeEventEmitter):
asyncio.get_running_loop().create_future()
)
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
watcher.once(
connection, 'channel_sounding_capabilities', complete_future.set_result
)
@@ -4725,7 +4726,7 @@ class Device(CompositeEventEmitter):
)
return await complete_future
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def set_default_cs_settings(
self,
connection: Connection,
@@ -4745,7 +4746,7 @@ class Device(CompositeEventEmitter):
check_result=True,
)
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def create_cs_config(
self,
connection: Connection,
@@ -4782,7 +4783,7 @@ class Device(CompositeEventEmitter):
if config_id is None:
raise OutOfResourcesError("No available config ID on this connection!")
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
watcher.once(
connection, 'channel_sounding_config', complete_future.set_result
)
@@ -4816,12 +4817,12 @@ class Device(CompositeEventEmitter):
)
return await complete_future
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def enable_cs_security(self, connection: Connection) -> None:
complete_future: asyncio.Future[None] = (
asyncio.get_running_loop().create_future()
)
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
def on_event(event: hci.HCI_LE_CS_Security_Enable_Complete_Event) -> None:
if event.connection_handle != connection.handle:
@@ -4840,7 +4841,7 @@ class Device(CompositeEventEmitter):
)
return await complete_future
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def set_cs_procedure_parameters(
self,
connection: Connection,
@@ -4878,7 +4879,7 @@ class Device(CompositeEventEmitter):
check_result=True,
)
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def enable_cs_procedure(
self,
connection: Connection,
@@ -4888,7 +4889,7 @@ class Device(CompositeEventEmitter):
complete_future: asyncio.Future[ChannelSoundingProcedure] = (
asyncio.get_running_loop().create_future()
)
with closing(EventWatcher()) as watcher:
with closing(utils.EventWatcher()) as watcher:
watcher.once(
connection, 'channel_sounding_procedure', complete_future.set_result
)
@@ -4928,7 +4929,9 @@ class Device(CompositeEventEmitter):
value=link_key, authenticated=authenticated
)
self.abort_on('flush', self.update_keys(str(bd_addr), pairing_keys))
utils.cancel_on_event(
self, 'flush', self.update_keys(str(bd_addr), pairing_keys)
)
if connection := self.find_connection_by_bd_addr(
bd_addr, transport=PhysicalTransport.BR_EDR
@@ -5197,7 +5200,7 @@ class Device(CompositeEventEmitter):
if advertising_set.auto_restart:
connection.once(
'disconnection',
lambda _: self.abort_on('flush', advertising_set.start()),
lambda _: utils.cancel_on_event(self, 'flush', advertising_set.start()),
)
self.emit('connection', connection)
@@ -5306,7 +5309,7 @@ class Device(CompositeEventEmitter):
advertiser = self.legacy_advertiser
connection.once(
'disconnection',
lambda _: self.abort_on('flush', advertiser.start()),
lambda _: utils.cancel_on_event(self, 'flush', advertiser.start()),
)
else:
self.legacy_advertiser = None
@@ -5433,7 +5436,7 @@ class Device(CompositeEventEmitter):
connection.emit('disconnection_failure', error)
@host_event_handler
@AsyncRunner.run_in_task()
@utils.AsyncRunner.run_in_task()
async def on_inquiry_complete(self):
if self.auto_restart_inquiry:
# Inquire again
@@ -5565,7 +5568,7 @@ class Device(CompositeEventEmitter):
async def reply() -> None:
try:
if await connection.abort_on('disconnection', method()):
if await utils.cancel_on_event(connection, 'disconnection', method()):
await self.host.send_command(
hci.HCI_User_Confirmation_Request_Reply_Command(
bd_addr=connection.peer_address
@@ -5581,7 +5584,7 @@ class Device(CompositeEventEmitter):
)
)
AsyncRunner.spawn(reply())
utils.AsyncRunner.spawn(reply())
# [Classic only]
@host_event_handler
@@ -5592,8 +5595,8 @@ class Device(CompositeEventEmitter):
async def reply() -> None:
try:
number = await connection.abort_on(
'disconnection', pairing_config.delegate.get_number()
number = await utils.cancel_on_event(
connection, 'disconnection', pairing_config.delegate.get_number()
)
if number is not None:
await self.host.send_command(
@@ -5611,7 +5614,7 @@ class Device(CompositeEventEmitter):
)
)
AsyncRunner.spawn(reply())
utils.AsyncRunner.spawn(reply())
# [Classic only]
@host_event_handler
@@ -5626,8 +5629,8 @@ class Device(CompositeEventEmitter):
if io_capability == hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY:
# Ask the user to enter a string
async def get_pin_code():
pin_code = await connection.abort_on(
'disconnection', pairing_config.delegate.get_string(16)
pin_code = await utils.cancel_on_event(
connection, 'disconnection', pairing_config.delegate.get_string(16)
)
if pin_code is not None:
@@ -5665,8 +5668,8 @@ class Device(CompositeEventEmitter):
pairing_config = self.pairing_config_factory(connection)
# Show the passkey to the user
connection.abort_on(
'disconnection', pairing_config.delegate.display_number(passkey)
utils.cancel_on_event(
connection, 'disconnection', pairing_config.delegate.display_number(passkey)
)
# [Classic only]
@@ -5698,7 +5701,7 @@ class Device(CompositeEventEmitter):
# [Classic only]
@host_event_handler
@with_connection_from_address
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
def on_sco_connection(
self, acl_connection: Connection, sco_handle: int, link_type: int
) -> None:
@@ -5718,7 +5721,7 @@ class Device(CompositeEventEmitter):
# [Classic only]
@host_event_handler
@with_connection_from_address
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
def on_sco_connection_failure(
self, acl_connection: Connection, status: int
) -> None:
@@ -5727,7 +5730,7 @@ class Device(CompositeEventEmitter):
# [Classic only]
@host_event_handler
@experimental('Only for testing')
@utils.experimental('Only for testing')
def on_sco_packet(
self, sco_handle: int, packet: hci.HCI_SynchronousDataPacket
) -> None:
@@ -5737,7 +5740,7 @@ class Device(CompositeEventEmitter):
# [LE only]
@host_event_handler
@with_connection_from_handle
@experimental('Only for testing')
@utils.experimental('Only for testing')
def on_cis_request(
self,
acl_connection: Connection,
@@ -5764,7 +5767,7 @@ class Device(CompositeEventEmitter):
# [LE only]
@host_event_handler
@experimental('Only for testing')
@utils.experimental('Only for testing')
def on_cis_establishment(self, cis_handle: int) -> None:
cis_link = self.cis_links[cis_handle]
cis_link.state = CisLink.State.ESTABLISHED
@@ -5784,7 +5787,7 @@ class Device(CompositeEventEmitter):
# [LE only]
@host_event_handler
@experimental('Only for testing')
@utils.experimental('Only for testing')
def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
if cis_link := self.cis_links.pop(cis_handle):
@@ -5793,7 +5796,7 @@ class Device(CompositeEventEmitter):
# [LE only]
@host_event_handler
@experimental('Only for testing')
@utils.experimental('Only for testing')
def on_iso_packet(self, handle: int, packet: hci.HCI_IsoDataPacket) -> None:
if (cis_link := self.cis_links.get(handle)) and cis_link.sink:
cis_link.sink(packet)