Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 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 | # ----------------------------------------------------------------------------- |
| 18 | import asyncio |
| 19 | import sys |
| 20 | import os |
| 21 | import logging |
| 22 | |
uael | d21da78 | 2023-02-23 20:16:33 +0000 | [diff] [blame] | 23 | from bumble.colors import color |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 24 | |
| 25 | import bumble.core |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 26 | from bumble.device import Device |
| 27 | from bumble.transport import open_transport_or_link |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 28 | from bumble.core import ( |
| 29 | BT_HANDSFREE_SERVICE, |
| 30 | BT_RFCOMM_PROTOCOL_ID, |
| 31 | BT_BR_EDR_TRANSPORT, |
| 32 | ) |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 33 | from bumble.rfcomm import Client |
| 34 | from bumble.sdp import ( |
| 35 | Client as SDP_Client, |
| 36 | DataElement, |
| 37 | ServiceAttribute, |
| 38 | SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, |
| 39 | SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 40 | SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 41 | ) |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 42 | from bumble.hfp import HfpProtocol |
| 43 | |
| 44 | |
| 45 | # ----------------------------------------------------------------------------- |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 46 | # pylint: disable-next=too-many-nested-blocks |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 47 | async def list_rfcomm_channels(device, connection): |
| 48 | # Connect to the SDP Server |
| 49 | sdp_client = SDP_Client(device) |
| 50 | await sdp_client.connect(connection) |
| 51 | |
| 52 | # Search for services that support the Handsfree Profile |
| 53 | search_result = await sdp_client.search_attributes( |
| 54 | [BT_HANDSFREE_SERVICE], |
| 55 | [ |
| 56 | SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, |
| 57 | SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 58 | SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, |
| 59 | ], |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 60 | ) |
| 61 | print(color('==================================', 'blue')) |
| 62 | print(color('Handsfree Services:', 'yellow')) |
| 63 | rfcomm_channels = [] |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 64 | # pylint: disable-next=too-many-nested-blocks |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 65 | for attribute_list in search_result: |
| 66 | # Look for the RFCOMM Channel number |
| 67 | protocol_descriptor_list = ServiceAttribute.find_attribute_in_list( |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 68 | attribute_list, SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 69 | ) |
| 70 | if protocol_descriptor_list: |
| 71 | for protocol_descriptor in protocol_descriptor_list.value: |
| 72 | if len(protocol_descriptor.value) >= 2: |
| 73 | if protocol_descriptor.value[0].value == BT_RFCOMM_PROTOCOL_ID: |
| 74 | print(color('SERVICE:', 'green')) |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 75 | print( |
| 76 | color(' RFCOMM Channel:', 'cyan'), |
| 77 | protocol_descriptor.value[1].value, |
| 78 | ) |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 79 | rfcomm_channels.append(protocol_descriptor.value[1].value) |
| 80 | |
| 81 | # List profiles |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 82 | bluetooth_profile_descriptor_list = ( |
| 83 | ServiceAttribute.find_attribute_in_list( |
| 84 | attribute_list, |
| 85 | SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, |
| 86 | ) |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 87 | ) |
| 88 | if bluetooth_profile_descriptor_list: |
| 89 | if bluetooth_profile_descriptor_list.value: |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 90 | if ( |
| 91 | bluetooth_profile_descriptor_list.value[0].type |
| 92 | == DataElement.SEQUENCE |
| 93 | ): |
| 94 | bluetooth_profile_descriptors = ( |
| 95 | bluetooth_profile_descriptor_list.value |
| 96 | ) |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 97 | else: |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 98 | # Sometimes, instead of a list of lists, we just |
| 99 | # find a list. Fix that |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 100 | bluetooth_profile_descriptors = [ |
| 101 | bluetooth_profile_descriptor_list |
| 102 | ] |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 103 | |
| 104 | print(color(' Profiles:', 'green')) |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 105 | for ( |
| 106 | bluetooth_profile_descriptor |
| 107 | ) in bluetooth_profile_descriptors: |
| 108 | version_major = ( |
| 109 | bluetooth_profile_descriptor.value[1].value >> 8 |
| 110 | ) |
| 111 | version_minor = ( |
| 112 | bluetooth_profile_descriptor.value[1].value |
| 113 | & 0xFF |
| 114 | ) |
| 115 | print( |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 116 | ' ' |
| 117 | f'{bluetooth_profile_descriptor.value[0].value}' |
| 118 | f' - version {version_major}.{version_minor}' |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 119 | ) |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 120 | |
| 121 | # List service classes |
| 122 | service_class_id_list = ServiceAttribute.find_attribute_in_list( |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 123 | attribute_list, SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 124 | ) |
| 125 | if service_class_id_list: |
| 126 | if service_class_id_list.value: |
| 127 | print(color(' Service Classes:', 'green')) |
| 128 | for service_class_id in service_class_id_list.value: |
| 129 | print(' ', service_class_id.value) |
| 130 | |
| 131 | await sdp_client.disconnect() |
| 132 | return rfcomm_channels |
| 133 | |
| 134 | |
| 135 | # ----------------------------------------------------------------------------- |
| 136 | async def main(): |
| 137 | if len(sys.argv) < 4: |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 138 | print( |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 139 | 'Usage: run_hfp_gateway.py <device-config> <transport-spec> ' |
| 140 | '<bluetooth-address>' |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 141 | ) |
| 142 | print( |
| 143 | ' specifying a channel number, or "discover" to list all RFCOMM channels' |
| 144 | ) |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 145 | print('example: run_hfp_gateway.py hfp_gateway.json usb:0 E1:CA:72:48:C4:E8') |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 146 | return |
| 147 | |
| 148 | print('<<< connecting to HCI...') |
| 149 | async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink): |
| 150 | print('<<< connected') |
| 151 | |
| 152 | # Create a device |
| 153 | device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink) |
| 154 | device.classic_enabled = True |
| 155 | await device.power_on() |
| 156 | |
| 157 | # Connect to a peer |
| 158 | target_address = sys.argv[3] |
| 159 | print(f'=== Connecting to {target_address}...') |
| 160 | connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT) |
| 161 | print(f'=== Connected to {connection.peer_address}!') |
| 162 | |
| 163 | # Get a list of all the Handsfree services (should only be 1) |
| 164 | channels = await list_rfcomm_channels(device, connection) |
| 165 | if len(channels) == 0: |
| 166 | print('!!! no service found') |
| 167 | return |
| 168 | |
| 169 | # Pick the first one |
| 170 | channel = channels[0] |
| 171 | |
| 172 | # Request authentication |
| 173 | print('*** Authenticating...') |
| 174 | await connection.authenticate() |
| 175 | print('*** Authenticated') |
| 176 | |
| 177 | # Enable encryption |
| 178 | print('*** Enabling encryption...') |
| 179 | await connection.encrypt() |
| 180 | print('*** Encryption on') |
| 181 | |
| 182 | # Create a client and start it |
| 183 | print('@@@ Starting to RFCOMM client...') |
| 184 | rfcomm_client = Client(device, connection) |
| 185 | rfcomm_mux = await rfcomm_client.start() |
| 186 | print('@@@ Started') |
| 187 | |
| 188 | print(f'### Opening session for channel {channel}...') |
| 189 | try: |
| 190 | session = await rfcomm_mux.open_dlc(channel) |
| 191 | print('### Session open', session) |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 192 | except bumble.core.ConnectionError as error: |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 193 | print(f'### Session open failed: {error}') |
| 194 | await rfcomm_mux.disconnect() |
| 195 | print('@@@ Disconnected from RFCOMM server') |
| 196 | return |
| 197 | |
| 198 | # Protocol loop (just for testing at this point) |
| 199 | protocol = HfpProtocol(session) |
| 200 | while True: |
| 201 | line = await protocol.next_line() |
| 202 | |
| 203 | if line.startswith('AT+BRSF='): |
| 204 | protocol.send_response_line('+BRSF: 30') |
| 205 | protocol.send_response_line('OK') |
| 206 | elif line.startswith('AT+CIND=?'): |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 207 | protocol.send_response_line( |
Gilles Boccon-Gibod | c2959da | 2022-12-10 09:29:51 -0800 | [diff] [blame] | 208 | '+CIND: ("call",(0,1)),("callsetup",(0-3)),("service",(0-1)),' |
| 209 | '("signal",(0-5)),("roam",(0,1)),("battchg",(0-5)),' |
| 210 | '("callheld",(0-2))' |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 211 | ) |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 212 | protocol.send_response_line('OK') |
| 213 | elif line.startswith('AT+CIND?'): |
| 214 | protocol.send_response_line('+CIND: 0,0,1,4,1,5,0') |
| 215 | protocol.send_response_line('OK') |
| 216 | elif line.startswith('AT+CMER='): |
| 217 | protocol.send_response_line('OK') |
| 218 | elif line.startswith('AT+CHLD=?'): |
| 219 | protocol.send_response_line('+CHLD: 0') |
| 220 | protocol.send_response_line('OK') |
| 221 | elif line.startswith('AT+BTRH?'): |
| 222 | protocol.send_response_line('+BTRH: 0') |
| 223 | protocol.send_response_line('OK') |
| 224 | elif line.startswith('AT+CLIP='): |
| 225 | protocol.send_response_line('OK') |
| 226 | elif line.startswith('AT+VGS='): |
| 227 | protocol.send_response_line('OK') |
| 228 | elif line.startswith('AT+BIA='): |
| 229 | protocol.send_response_line('OK') |
| 230 | elif line.startswith('AT+BVRA='): |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 231 | protocol.send_response_line( |
| 232 | '+BVRA: 1,1,12AA,1,1,"Message 1 from Janina"' |
| 233 | ) |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 234 | elif line.startswith('AT+XEVENT='): |
| 235 | protocol.send_response_line('OK') |
| 236 | elif line.startswith('AT+XAPL='): |
| 237 | protocol.send_response_line('OK') |
| 238 | else: |
| 239 | print(color('UNSUPPORTED AT COMMAND', 'red')) |
| 240 | protocol.send_response_line('ERROR') |
| 241 | |
| 242 | await hci_source.wait_for_termination() |
| 243 | |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 244 | |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 245 | # ----------------------------------------------------------------------------- |
Gilles Boccon-Gibod | 135df0d | 2022-12-10 08:53:51 -0800 | [diff] [blame] | 246 | logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper()) |
Gilles Boccon-Gibod | 6ac91f7 | 2022-05-16 19:42:31 -0700 | [diff] [blame] | 247 | asyncio.run(main()) |