blob: 63a2a7c3316376fb1ea50f7baa5917b57ab9e4d9 [file] [log] [blame]
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001# 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
22
uaeld21da782023-02-23 20:16:33 +000023from bumble.colors import color
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -080024
25import bumble.core
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070026from bumble.device import Device
27from bumble.transport import open_transport_or_link
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -080028from bumble.core import (
29 BT_HANDSFREE_SERVICE,
30 BT_RFCOMM_PROTOCOL_ID,
31 BT_BR_EDR_TRANSPORT,
32)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070033from bumble.rfcomm import Client
34from 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-Gibod135df0d2022-12-10 08:53:51 -080040 SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070041)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070042from bumble.hfp import HfpProtocol
43
44
45# -----------------------------------------------------------------------------
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -080046# pylint: disable-next=too-many-nested-blocks
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070047async 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-Gibod135df0d2022-12-10 08:53:51 -080058 SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
59 ],
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070060 )
61 print(color('==================================', 'blue'))
62 print(color('Handsfree Services:', 'yellow'))
63 rfcomm_channels = []
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -080064 # pylint: disable-next=too-many-nested-blocks
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070065 for attribute_list in search_result:
66 # Look for the RFCOMM Channel number
67 protocol_descriptor_list = ServiceAttribute.find_attribute_in_list(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -080068 attribute_list, SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070069 )
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-Gibod135df0d2022-12-10 08:53:51 -080075 print(
76 color(' RFCOMM Channel:', 'cyan'),
77 protocol_descriptor.value[1].value,
78 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070079 rfcomm_channels.append(protocol_descriptor.value[1].value)
80
81 # List profiles
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -080082 bluetooth_profile_descriptor_list = (
83 ServiceAttribute.find_attribute_in_list(
84 attribute_list,
85 SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
86 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070087 )
88 if bluetooth_profile_descriptor_list:
89 if bluetooth_profile_descriptor_list.value:
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -080090 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-Gibod6ac91f72022-05-16 19:42:31 -070097 else:
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -080098 # Sometimes, instead of a list of lists, we just
99 # find a list. Fix that
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800100 bluetooth_profile_descriptors = [
101 bluetooth_profile_descriptor_list
102 ]
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700103
104 print(color(' Profiles:', 'green'))
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800105 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-Gibodc2959da2022-12-10 09:29:51 -0800116 ' '
117 f'{bluetooth_profile_descriptor.value[0].value}'
118 f' - version {version_major}.{version_minor}'
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800119 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700120
121 # List service classes
122 service_class_id_list = ServiceAttribute.find_attribute_in_list(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800123 attribute_list, SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700124 )
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# -----------------------------------------------------------------------------
136async def main():
137 if len(sys.argv) < 4:
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800138 print(
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800139 'Usage: run_hfp_gateway.py <device-config> <transport-spec> '
140 '<bluetooth-address>'
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800141 )
142 print(
143 ' specifying a channel number, or "discover" to list all RFCOMM channels'
144 )
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800145 print('example: run_hfp_gateway.py hfp_gateway.json usb:0 E1:CA:72:48:C4:E8')
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700146 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-Gibodc2959da2022-12-10 09:29:51 -0800192 except bumble.core.ConnectionError as error:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700193 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-Gibod135df0d2022-12-10 08:53:51 -0800207 protocol.send_response_line(
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800208 '+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-Gibod135df0d2022-12-10 08:53:51 -0800211 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700212 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-Gibod135df0d2022-12-10 08:53:51 -0800231 protocol.send_response_line(
232 '+BVRA: 1,1,12AA,1,1,"Message 1 from Janina"'
233 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700234 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-Gibod135df0d2022-12-10 08:53:51 -0800244
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700245# -----------------------------------------------------------------------------
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800246logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700247asyncio.run(main())