diff --git a/bumble/controller.py b/bumble/controller.py index a3127d0f..1153ec00 100644 --- a/bumble/controller.py +++ b/bumble/controller.py @@ -121,6 +121,72 @@ class Connection: # ----------------------------------------------------------------------------- class Controller: + hci_sink: Optional[TransportSink] = None + + central_connections: dict[ + Address, Connection + ] # Connections where this controller is the central + peripheral_connections: dict[ + Address, Connection + ] # Connections where this controller is the peripheral + classic_connections: dict[Address, Connection] # Connections in BR/EDR + central_cis_links: dict[int, CisLink] # CIS links by handle + peripheral_cis_links: dict[int, CisLink] # CIS links by handle + + hci_version: int = HCI_VERSION_BLUETOOTH_CORE_5_0 + hci_revision: int = 0 + lmp_version: int = HCI_VERSION_BLUETOOTH_CORE_5_0 + lmp_subversion: int = 0 + lmp_features: bytes = bytes.fromhex( + '0000000060000000' + ) # BR/EDR Not Supported, LE Supported (Controller) + manufacturer_name: int = 0xFFFF + acl_data_packet_length: int = 27 + total_num_acl_data_packets: int = 64 + le_acl_data_packet_length: int = 27 + total_num_le_acl_data_packets: int = 64 + iso_data_packet_length: int = 960 + total_num_iso_data_packets: int = 64 + event_mask: int = 0 + event_mask_page_2: int = 0 + supported_commands: bytes = bytes.fromhex( + '2000800000c000000000e4000000a822000000000000040000f7ffff7f000000' + '30f0f9ff01008004002000000000000000000000000000000000000000000000' + ) + le_event_mask: int = 0 + advertising_parameters: Optional[hci.HCI_LE_Set_Advertising_Parameters_Command] = ( + None + ) + le_features: bytes = bytes.fromhex('ff49010000000000') + le_states: bytes = bytes.fromhex('ffff3fffff030000') + advertising_channel_tx_power: int = 0 + filter_accept_list_size: int = 8 + filter_duplicates: bool = False + resolving_list_size: int = 8 + supported_max_tx_octets: int = 27 + supported_max_tx_time: int = 10000 + supported_max_rx_octets: int = 27 + supported_max_rx_time: int = 10000 + suggested_max_tx_octets: int = 27 + suggested_max_tx_time: int = 0x0148 + default_phy: dict[str, int] + le_scan_type: int = 0 + le_scan_interval: int = 0x10 + le_scan_window: int = 0x10 + le_scan_enable: int = 0 + le_scan_own_address_type: int = Address.RANDOM_DEVICE_ADDRESS + le_scanning_filter_policy: int = 0 + le_scan_response_data: Optional[bytes] = None + le_address_resolution: bool = False + le_rpa_timeout: int = 0 + sync_flow_control: bool = False + local_name: str = 'Bumble' + advertising_interval: int = 2000 + advertising_data: Optional[bytes] = None + advertising_timer_handle: Optional[asyncio.Handle] = None + + _random_address: 'Address' = Address('00:00:00:00:00:00') + def __init__( self, name: str, @@ -130,77 +196,18 @@ class Controller: public_address: Optional[Union[bytes, str, Address]] = None, ) -> None: self.name = name - self.hci_sink: Optional[TransportSink] = None self.link = link - - self.central_connections: dict[Address, Connection] = ( - {} - ) # Connections where this controller is the central - self.peripheral_connections: dict[Address, Connection] = ( - {} - ) # Connections where this controller is the peripheral - self.classic_connections: dict[Address, Connection] = ( - {} - ) # Connections in BR/EDR - self.central_cis_links: dict[int, CisLink] = {} # CIS links by handle - self.peripheral_cis_links: dict[int, CisLink] = {} # CIS links by handle - - self.hci_version = HCI_VERSION_BLUETOOTH_CORE_5_0 - self.hci_revision = 0 - self.lmp_version = HCI_VERSION_BLUETOOTH_CORE_5_0 - self.lmp_subversion = 0 - self.lmp_features = bytes.fromhex( - '0000000060000000' - ) # BR/EDR Not Supported, LE Supported (Controller) - self.manufacturer_name = 0xFFFF - self.acl_data_packet_length = 27 - self.total_num_acl_data_packets = 64 - self.le_acl_data_packet_length = 27 - self.total_num_le_acl_data_packets = 64 - self.iso_data_packet_length = 960 - self.total_num_iso_data_packets = 64 - self.event_mask = b'\x00' - self.event_mask_page_2 = b'\x00' - self.supported_commands = bytes.fromhex( - '2000800000c000000000e4000000a822000000000000040000f7ffff7f000000' - '30f0f9ff01008004002000000000000000000000000000000000000000000000' - ) - self.le_event_mask = b'\x00' - self.advertising_parameters = None - self.le_features = bytes.fromhex('ff49010000000000') - self.le_states = bytes.fromhex('ffff3fffff030000') - self.advertising_channel_tx_power = 0 - self.filter_accept_list_size = 8 - self.filter_duplicates = False - self.resolving_list_size = 8 - self.supported_max_tx_octets = 27 - self.supported_max_tx_time = 10000 # microseconds - self.supported_max_rx_octets = 27 - self.supported_max_rx_time = 10000 # microseconds - self.suggested_max_tx_octets = 27 - self.suggested_max_tx_time = 0x0148 # microseconds + self.central_connections = {} + self.peripheral_connections = {} + self.classic_connections = {} + self.central_cis_links = {} + self.peripheral_cis_links = {} self.default_phy = { 'all_phys': 0, 'tx_phys': 0, 'rx_phys': 0, } - self.le_scan_type = 0 - self.le_scan_interval = 0x10 - self.le_scan_window = 0x10 - self.le_scan_enable = 0 - self.le_scan_own_address_type = Address.RANDOM_DEVICE_ADDRESS - self.le_scanning_filter_policy = 0 - self.le_scan_response_data = None - self.le_address_resolution = False - self.le_rpa_timeout = 0 - self.sync_flow_control = False - self.local_name = 'Bumble' - self.advertising_interval = 2000 # Fixed for now - self.advertising_data: Optional[bytes] = None - self.advertising_timer_handle: Optional[asyncio.Handle] = None - - self._random_address = Address('00:00:00:00:00:00') if isinstance(public_address, Address): self._public_address = public_address elif public_address is not None: @@ -489,8 +496,8 @@ class Controller: logger.debug( f'New CENTRAL connection handle: 0x{connection_handle:04X}' ) - else: - connection = None + else: + connection = None # Say that the connection has completed self.send_hci_packet( @@ -1117,7 +1124,9 @@ class Controller: ''' See Bluetooth spec Vol 4, Part E - 7.3.1 Set Event Mask Command ''' - self.event_mask = command.event_mask + self.event_mask = int.from_bytes( + command.event_mask, byteorder='little', signed=False + ) return bytes([HCI_SUCCESS]) def on_hci_reset_command(self, _command: hci.HCI_Reset_Command) -> Optional[bytes]: @@ -1245,7 +1254,9 @@ class Controller: ''' See Bluetooth spec Vol 4, Part E - 7.3.69 Set Event Mask Page 2 Command ''' - self.event_mask_page_2 = command.event_mask_page_2 + self.event_mask_page_2 = int.from_bytes( + command.event_mask_page_2, byteorder='little', signed=False + ) return bytes([HCI_SUCCESS]) def on_hci_read_le_host_support_command( @@ -1414,7 +1425,9 @@ class Controller: ''' See Bluetooth spec Vol 4, Part E - 7.8.1 LE Set Event Mask Command ''' - self.le_event_mask = command.le_event_mask + self.le_event_mask = int.from_bytes( + command.le_event_mask, byteorder='little', signed=False + ) return bytes([HCI_SUCCESS]) def on_hci_le_read_buffer_size_command( @@ -1940,7 +1953,8 @@ class Controller: ''' # Remove old CIG implicitly. - for handle, cis_link in list(self.central_cis_links.items()): + cis_links = list(self.central_cis_links.items()) + for handle, cis_link in cis_links: if cis_link.cig_id == command.cig_id: self.central_cis_links.pop(handle) @@ -2004,7 +2018,8 @@ class Controller: status = HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR - for cis_handle, cis_link in list(self.central_cis_links.items()): + cis_links = list(self.central_cis_links.items()) + for cis_handle, cis_link in cis_links: if cis_link.cig_id == command.cig_id: self.central_cis_links.pop(cis_handle) status = HCI_SUCCESS diff --git a/bumble/transport/common.py b/bumble/transport/common.py index b0f3c8ac..208241c2 100644 --- a/bumble/transport/common.py +++ b/bumble/transport/common.py @@ -22,6 +22,7 @@ import contextlib import io import logging import struct +from collections.abc import Awaitable, Callable from typing import Any, ContextManager, Optional, Protocol from bumble import core, hci @@ -389,15 +390,17 @@ class PumpedPacketSource(ParserSource): # ----------------------------------------------------------------------------- class PumpedPacketSink: - def __init__(self, send): + pump_task: Optional[asyncio.Task[None]] + + def __init__(self, send: Callable[[bytes], Awaitable[Any]]): self.send_function = send - self.packet_queue = asyncio.Queue() + self.packet_queue = asyncio.Queue[bytes]() self.pump_task = None def on_packet(self, packet: bytes) -> None: self.packet_queue.put_nowait(packet) - def start(self): + def start(self) -> None: async def pump_packets(): while True: try: diff --git a/bumble/transport/ws_client.py b/bumble/transport/ws_client.py index 555b0138..022c2c49 100644 --- a/bumble/transport/ws_client.py +++ b/bumble/transport/ws_client.py @@ -17,7 +17,7 @@ # ----------------------------------------------------------------------------- import logging -import websockets.client +import websockets.asyncio.client from bumble.transport.common import ( PumpedPacketSink, @@ -42,7 +42,7 @@ async def open_ws_client_transport(spec: str) -> Transport: Example: ws://localhost:7681/v1/websocket/bt ''' - websocket = await websockets.client.connect(spec) + websocket = await websockets.asyncio.client.connect(spec) class WsTransport(PumpedTransport): async def close(self): diff --git a/bumble/transport/ws_server.py b/bumble/transport/ws_server.py index 519d0ab4..c2340907 100644 --- a/bumble/transport/ws_server.py +++ b/bumble/transport/ws_server.py @@ -16,8 +16,9 @@ # Imports # ----------------------------------------------------------------------------- import logging +from typing import Optional -import websockets +import websockets.asyncio.server from bumble.transport.common import ParserSource, PumpedPacketSink, Transport @@ -40,7 +41,12 @@ async def open_ws_server_transport(spec: str) -> Transport: ''' class WsServerTransport(Transport): - def __init__(self): + sink: PumpedPacketSink + source: ParserSource + connection: Optional[websockets.asyncio.server.ServerConnection] + server: Optional[websockets.asyncio.server.Server] + + def __init__(self) -> None: source = ParserSource() sink = PumpedPacketSink(self.send_packet) self.connection = None @@ -48,17 +54,19 @@ async def open_ws_server_transport(spec: str) -> Transport: super().__init__(source, sink) - async def serve(self, local_host, local_port): + async def serve(self, local_host: str, local_port: str) -> None: self.sink.start() # pylint: disable-next=no-member - self.server = await websockets.serve( - ws_handler=self.on_connection, + self.server = await websockets.asyncio.server.serve( + handler=self.on_connection, host=local_host if local_host != '_' else None, port=int(local_port), ) logger.debug(f'websocket server ready on port {local_port}') - async def on_connection(self, connection): + async def on_connection( + self, connection: websockets.asyncio.server.ServerConnection + ) -> None: logger.debug( f'new connection on {connection.local_address} ' f'from {connection.remote_address}' @@ -77,11 +85,11 @@ async def open_ws_server_transport(spec: str) -> Transport: # We're now disconnected self.connection = None - async def send_packet(self, packet): + async def send_packet(self, packet: bytes) -> None: if self.connection is None: logger.debug('no connection, dropping packet') return - return await self.connection.send(packet) + await self.connection.send(packet) local_host, local_port = spec.rsplit(':', maxsplit=1) transport = WsServerTransport() diff --git a/examples/keyboard.py b/examples/keyboard.py index 75146bad..e93de2c3 100644 --- a/examples/keyboard.py +++ b/examples/keyboard.py @@ -20,7 +20,7 @@ import json import struct import sys -import websockets +import websockets.asyncio.server import bumble.logging from bumble import data_types @@ -367,7 +367,7 @@ async def keyboard_device(device, command): if command == 'web': # Start a Websocket server to receive events from a web page - async def serve(websocket, _path): + async def serve(websocket: websockets.asyncio.server.ServerConnection): while True: try: message = await websocket.recv() @@ -398,7 +398,7 @@ async def keyboard_device(device, command): pass # pylint: disable-next=no-member - await websockets.serve(serve, 'localhost', 8989) + await websockets.asyncio.server.serve(serve, 'localhost', 8989) await asyncio.get_event_loop().create_future() else: message = bytes('hello', 'ascii') diff --git a/examples/run_asha_sink.py b/examples/run_asha_sink.py index f2e04562..252de06e 100644 --- a/examples/run_asha_sink.py +++ b/examples/run_asha_sink.py @@ -20,7 +20,7 @@ import logging import sys from typing import Optional -import websockets +import websockets.asyncio.server import bumble.logging from bumble import data_types, decoder, gatt @@ -29,12 +29,11 @@ from bumble.device import AdvertisingParameters, Device from bumble.profiles import asha from bumble.transport import open_transport -ws_connection: Optional[websockets.WebSocketServerProtocol] = None +ws_connection: Optional[websockets.asyncio.server.ServerConnection] = None g722_decoder = decoder.G722Decoder() -async def ws_server(ws_client: websockets.WebSocketServerProtocol, path: str): - del path +async def ws_server(ws_client: websockets.asyncio.server.ServerConnection): global ws_connection ws_connection = ws_client @@ -100,7 +99,7 @@ async def main() -> None: ), ) - await websockets.serve(ws_server, port=8888) + await websockets.asyncio.server.serve(ws_server, port=8888) await hci_transport.source.terminated diff --git a/examples/run_avrcp.py b/examples/run_avrcp.py index 592e21ab..21895fdc 100644 --- a/examples/run_avrcp.py +++ b/examples/run_avrcp.py @@ -21,8 +21,9 @@ import asyncio import json import logging import sys +from typing import Optional -import websockets +import websockets.asyncio.server import bumble.logging from bumble import a2dp, avc, avdtp, avrcp, utils @@ -217,6 +218,8 @@ def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketSe # ----------------------------------------------------------------------------- class WebSocketServer: + socket: Optional[websockets.asyncio.server.ServerConnection] + def __init__( self, avrcp_protocol: avrcp.Protocol, avrcp_delegate: Delegate ) -> None: @@ -227,9 +230,9 @@ class WebSocketServer: async def start(self) -> None: # pylint: disable-next=no-member - await websockets.serve(self.serve, 'localhost', 8989) # type: ignore + await websockets.asyncio.server.serve(self.serve, 'localhost', 8989) # type: ignore - async def serve(self, socket, _path) -> None: + async def serve(self, socket: websockets.asyncio.server.ServerConnection) -> None: print('### WebSocket connected') self.socket = socket while True: diff --git a/examples/run_hfp_gateway.py b/examples/run_hfp_gateway.py index f92013e8..90984671 100644 --- a/examples/run_hfp_gateway.py +++ b/examples/run_hfp_gateway.py @@ -22,7 +22,7 @@ import logging import sys from typing import Iterable, Optional -import websockets +import websockets.asyncio.server import bumble.core import bumble.logging @@ -33,7 +33,7 @@ from bumble.transport import open_transport logger = logging.getLogger(__name__) -ws: Optional[websockets.WebSocketServerProtocol] = None +ws: Optional[websockets.asyncio.server.ServerConnection] = None ag_protocol: Optional[hfp.AgProtocol] = None source_file: Optional[io.BufferedReader] = None @@ -114,8 +114,7 @@ def on_hfp_state_change(connected: bool): send_message(type='hfp_state_change', connected=connected) -async def ws_server(ws_client: websockets.WebSocketServerProtocol, path: str): - del path +async def ws_server(ws_client: websockets.asyncio.server.ServerConnection): global ws ws = ws_client @@ -273,7 +272,7 @@ async def main() -> None: on_dlc(session) - await websockets.serve(ws_server, port=8888) + await websockets.asyncio.server.serve(ws_server, port=8888) if len(sys.argv) >= 5: global source_file diff --git a/examples/run_hfp_handsfree.py b/examples/run_hfp_handsfree.py index 1093dfc3..ff0b6a16 100644 --- a/examples/run_hfp_handsfree.py +++ b/examples/run_hfp_handsfree.py @@ -22,7 +22,7 @@ import json import sys from typing import Optional -import websockets +import websockets.asyncio.server import bumble.logging from bumble import hci, hfp, rfcomm @@ -30,7 +30,7 @@ from bumble.device import Connection, Device from bumble.hfp import HfProtocol from bumble.transport import open_transport -ws: Optional[websockets.WebSocketServerProtocol] = None +ws: Optional[websockets.asyncio.server.ServerConnection] = None hf_protocol: Optional[HfProtocol] = None @@ -143,7 +143,7 @@ async def main() -> None: await device.set_connectable(True) # Start the UI websocket server to offer a few buttons and input boxes - async def serve(websocket: websockets.WebSocketServerProtocol, _path): + async def serve(websocket: websockets.asyncio.server.ServerConnection): global ws ws = websocket async for message in websocket: @@ -166,7 +166,7 @@ async def main() -> None: response = str(await hf_protocol.query_current_calls()) await websocket.send(response) - await websockets.serve(serve, 'localhost', 8989) + await websockets.asyncio.server.serve(serve, 'localhost', 8989) await hci_transport.source.wait_for_termination() diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index a67a20ab..887c493c 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -20,7 +20,7 @@ import json import struct import sys -import websockets +import websockets.asyncio.server import bumble.logging from bumble.core import ( @@ -425,7 +425,7 @@ deviceData = DeviceData() async def keyboard_device(hid_device: HID_Device): # Start a Websocket server to receive events from a web page - async def serve(websocket, _path): + async def serve(websocket: websockets.asyncio.server.ServerConnection): global deviceData while True: try: @@ -476,7 +476,7 @@ async def keyboard_device(hid_device: HID_Device): pass # pylint: disable-next=no-member - await websockets.serve(serve, 'localhost', 8989) + await websockets.asyncio.server.serve(serve, 'localhost', 8989) await asyncio.get_event_loop().create_future() diff --git a/examples/run_mcp_client.py b/examples/run_mcp_client.py index fb5c34a1..71611303 100644 --- a/examples/run_mcp_client.py +++ b/examples/run_mcp_client.py @@ -20,7 +20,7 @@ import json import sys from typing import Optional -import websockets +import websockets.asyncio.server import bumble.logging from bumble import data_types @@ -101,7 +101,7 @@ async def main() -> None: ) device.add_service(AudioStreamControlService(device, sink_ase_id=[1])) - ws: Optional[websockets.WebSocketServerProtocol] = None + ws: Optional[websockets.asyncio.server.ServerConnection] = None mcp: Optional[MediaControlServiceProxy] = None advertising_data = bytes( @@ -162,7 +162,7 @@ async def main() -> None: device.on('connection', on_connection) - async def serve(websocket: websockets.WebSocketServerProtocol, _path): + async def serve(websocket: websockets.asyncio.server.ServerConnection): nonlocal ws ws = websocket async for message in websocket: @@ -173,7 +173,7 @@ async def main() -> None: ) ws = None - await websockets.serve(serve, 'localhost', 8989) + await websockets.asyncio.server.serve(serve, 'localhost', 8989) await hci_transport.source.terminated diff --git a/examples/run_vcp_renderer.py b/examples/run_vcp_renderer.py index 4217ac8b..42343489 100644 --- a/examples/run_vcp_renderer.py +++ b/examples/run_vcp_renderer.py @@ -21,7 +21,7 @@ import secrets import sys from typing import Optional -import websockets +import websockets.asyncio.server import bumble.logging from bumble import data_types @@ -110,7 +110,7 @@ async def main() -> None: vcs = VolumeControlService() device.add_service(vcs) - ws: Optional[websockets.WebSocketServerProtocol] = None + ws: Optional[websockets.asyncio.server.ServerConnection] = None def on_volume_state_change(): if ws: @@ -152,7 +152,7 @@ async def main() -> None: advertising_data=advertising_data, ) - async def serve(websocket: websockets.WebSocketServerProtocol, _path): + async def serve(websocket: websockets.asyncio.server.ServerConnection): nonlocal ws await websocket.send( dumps_volume_state(vcs.volume_setting, vcs.muted, vcs.change_counter) @@ -166,7 +166,7 @@ async def main() -> None: await device.notify_subscribers(vcs.volume_state) ws = None - await websockets.serve(serve, 'localhost', 8989) + await websockets.asyncio.server.serve(serve, 'localhost', 8989) await hci_transport.source.terminated diff --git a/pyproject.toml b/pyproject.toml index 184ccf07..37f450cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "pyserial-asyncio >= 0.5; platform_system!='Emscripten'", "pyserial >= 3.5; platform_system!='Emscripten'", "pyusb >= 1.2; platform_system!='Emscripten'", - "websockets == 13.1; platform_system!='Emscripten'", + "websockets >= 15.0.1; platform_system!='Emscripten'", ] [project.optional-dependencies]