1# Copyright 2021-2022 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15# ----------------------------------------------------------------------------- 16# Imports 17# ----------------------------------------------------------------------------- 18import asyncio 19import sys 20import os 21import logging 22import json 23import websockets 24 25 26from bumble.device import Device 27from bumble.transport import open_transport_or_link 28from bumble.rfcomm import Server as RfcommServer 29from bumble.sdp import ( 30 DataElement, 31 ServiceAttribute, 32 SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, 33 SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, 34 SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, 35 SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, 36) 37from bumble.core import ( 38 BT_GENERIC_AUDIO_SERVICE, 39 BT_HANDSFREE_SERVICE, 40 BT_L2CAP_PROTOCOL_ID, 41 BT_RFCOMM_PROTOCOL_ID, 42) 43from bumble.hfp import HfpProtocol 44 45 46# ----------------------------------------------------------------------------- 47def make_sdp_records(rfcomm_channel): 48 return { 49 0x00010001: [ 50 ServiceAttribute( 51 SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, 52 DataElement.unsigned_integer_32(0x00010001), 53 ), 54 ServiceAttribute( 55 SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, 56 DataElement.sequence( 57 [ 58 DataElement.uuid(BT_HANDSFREE_SERVICE), 59 DataElement.uuid(BT_GENERIC_AUDIO_SERVICE), 60 ] 61 ), 62 ), 63 ServiceAttribute( 64 SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, 65 DataElement.sequence( 66 [ 67 DataElement.sequence([DataElement.uuid(BT_L2CAP_PROTOCOL_ID)]), 68 DataElement.sequence( 69 [ 70 DataElement.uuid(BT_RFCOMM_PROTOCOL_ID), 71 DataElement.unsigned_integer_8(rfcomm_channel), 72 ] 73 ), 74 ] 75 ), 76 ), 77 ServiceAttribute( 78 SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, 79 DataElement.sequence( 80 [ 81 DataElement.sequence( 82 [ 83 DataElement.uuid(BT_HANDSFREE_SERVICE), 84 DataElement.unsigned_integer_16(0x0105), 85 ] 86 ) 87 ] 88 ), 89 ), 90 ] 91 } 92 93 94# ----------------------------------------------------------------------------- 95class UiServer: 96 protocol = None 97 98 async def start(self): 99 # Start a Websocket server to receive events from a web page 100 async def serve(websocket, _path): 101 while True: 102 try: 103 message = await websocket.recv() 104 print('Received: ', str(message)) 105 106 parsed = json.loads(message) 107 message_type = parsed['type'] 108 if message_type == 'at_command': 109 if self.protocol is not None: 110 self.protocol.send_command_line(parsed['command']) 111 112 except websockets.exceptions.ConnectionClosedOK: 113 pass 114 115 # pylint: disable=no-member 116 await websockets.serve(serve, 'localhost', 8989) 117 118 119# ----------------------------------------------------------------------------- 120async def protocol_loop(protocol): 121 await protocol.initialize_service() 122 123 while True: 124 await (protocol.next_line()) 125 126 127# ----------------------------------------------------------------------------- 128def on_dlc(dlc): 129 print('*** DLC connected', dlc) 130 protocol = HfpProtocol(dlc) 131 UiServer.protocol = protocol 132 asyncio.create_task(protocol_loop(protocol)) 133 134 135# ----------------------------------------------------------------------------- 136async def main(): 137 if len(sys.argv) < 3: 138 print('Usage: run_classic_hfp.py <device-config> <transport-spec>') 139 print('example: run_classic_hfp.py classic2.json usb:04b4:f901') 140 return 141 142 print('<<< connecting to HCI...') 143 async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink): 144 print('<<< connected') 145 146 # Create a device 147 device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink) 148 device.classic_enabled = True 149 150 # Create and register a server 151 rfcomm_server = RfcommServer(device) 152 153 # Listen for incoming DLC connections 154 channel_number = rfcomm_server.listen(on_dlc) 155 print(f'### Listening for connection on channel {channel_number}') 156 157 # Advertise the HFP RFComm channel in the SDP 158 device.sdp_service_records = make_sdp_records(channel_number) 159 160 # Let's go! 161 await device.power_on() 162 163 # Start being discoverable and connectable 164 await device.set_discoverable(True) 165 await device.set_connectable(True) 166 167 # Start the UI websocket server to offer a few buttons and input boxes 168 ui_server = UiServer() 169 await ui_server.start() 170 171 await hci_source.wait_for_termination() 172 173 174# ----------------------------------------------------------------------------- 175logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper()) 176asyncio.run(main()) 177