• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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