• 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 struct
20import sys
21import os
22import logging
23
24from bumble.core import AdvertisingData
25from bumble.device import Device
26from bumble.transport import open_transport_or_link
27from bumble.core import UUID
28from bumble.gatt import Service, Characteristic, CharacteristicValue
29
30
31# -----------------------------------------------------------------------------
32# Constants
33# -----------------------------------------------------------------------------
34ASHA_SERVICE = UUID.from_16_bits(0xFDF0, 'Audio Streaming for Hearing Aid')
35ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC = UUID(
36    '6333651e-c481-4a3e-9169-7c902aad37bb', 'ReadOnlyProperties'
37)
38ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC = UUID(
39    'f0d4de7e-4a88-476c-9d9f-1937b0996cc0', 'AudioControlPoint'
40)
41ASHA_AUDIO_STATUS_CHARACTERISTIC = UUID(
42    '38663f1a-e711-4cac-b641-326b56404837', 'AudioStatus'
43)
44ASHA_VOLUME_CHARACTERISTIC = UUID('00e4ca9e-ab14-41e4-8823-f9e70c7e91df', 'Volume')
45ASHA_LE_PSM_OUT_CHARACTERISTIC = UUID(
46    '2d410339-82b6-42aa-b34e-e2e01df8cc1a', 'LE_PSM_OUT'
47)
48
49
50# -----------------------------------------------------------------------------
51async def main():
52    if len(sys.argv) != 4:
53        print(
54            'Usage: python run_asha_sink.py <device-config> <transport-spec> '
55            '<audio-file>'
56        )
57        print('example: python run_asha_sink.py device1.json usb:0 audio_out.g722')
58        return
59
60    audio_out = open(sys.argv[3], 'wb')
61
62    async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
63        device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
64
65        # Handler for audio control commands
66        def on_audio_control_point_write(_connection, value):
67            print('--- AUDIO CONTROL POINT Write:', value.hex())
68            opcode = value[0]
69            if opcode == 1:
70                # Start
71                audio_type = ('Unknown', 'Ringtone', 'Phone Call', 'Media')[value[2]]
72                print(
73                    f'### START: codec={value[1]}, audio_type={audio_type}, '
74                    f'volume={value[3]}, otherstate={value[4]}'
75                )
76            elif opcode == 2:
77                print('### STOP')
78            elif opcode == 3:
79                print(f'### STATUS: connected={value[1]}')
80
81            # Respond with a status
82            asyncio.create_task(
83                device.notify_subscribers(audio_status_characteristic, force=True)
84            )
85
86        # Handler for volume control
87        def on_volume_write(_connection, value):
88            print('--- VOLUME Write:', value[0])
89
90        # Register an L2CAP CoC server
91        def on_coc(channel):
92            def on_data(data):
93                print('<<< Voice data received:', data.hex())
94                audio_out.write(data)
95
96            channel.sink = on_data
97
98        psm = device.register_l2cap_channel_server(0, on_coc, 8)
99        print(f'### LE_PSM_OUT = {psm}')
100
101        # Add the ASHA service to the GATT server
102        read_only_properties_characteristic = Characteristic(
103            ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
104            Characteristic.READ,
105            Characteristic.READABLE,
106            bytes(
107                [
108                    0x01,  # Version
109                    0x00,  # Device Capabilities [Left, Monaural]
110                    0x01,
111                    0x02,
112                    0x03,
113                    0x04,
114                    0x05,
115                    0x06,
116                    0x07,
117                    0x08,  # HiSyncId
118                    0x01,  # Feature Map [LE CoC audio output streaming supported]
119                    0x00,
120                    0x00,  # Render Delay
121                    0x00,
122                    0x00,  # RFU
123                    0x02,
124                    0x00,  # Codec IDs [G.722 at 16 kHz]
125                ]
126            ),
127        )
128        audio_control_point_characteristic = Characteristic(
129            ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
130            Characteristic.WRITE | Characteristic.WRITE_WITHOUT_RESPONSE,
131            Characteristic.WRITEABLE,
132            CharacteristicValue(write=on_audio_control_point_write),
133        )
134        audio_status_characteristic = Characteristic(
135            ASHA_AUDIO_STATUS_CHARACTERISTIC,
136            Characteristic.READ | Characteristic.NOTIFY,
137            Characteristic.READABLE,
138            bytes([0]),
139        )
140        volume_characteristic = Characteristic(
141            ASHA_VOLUME_CHARACTERISTIC,
142            Characteristic.WRITE_WITHOUT_RESPONSE,
143            Characteristic.WRITEABLE,
144            CharacteristicValue(write=on_volume_write),
145        )
146        le_psm_out_characteristic = Characteristic(
147            ASHA_LE_PSM_OUT_CHARACTERISTIC,
148            Characteristic.READ,
149            Characteristic.READABLE,
150            struct.pack('<H', psm),
151        )
152        device.add_service(
153            Service(
154                ASHA_SERVICE,
155                [
156                    read_only_properties_characteristic,
157                    audio_control_point_characteristic,
158                    audio_status_characteristic,
159                    volume_characteristic,
160                    le_psm_out_characteristic,
161                ],
162            )
163        )
164
165        # Set the advertising data
166        device.advertising_data = bytes(
167            AdvertisingData(
168                [
169                    (AdvertisingData.COMPLETE_LOCAL_NAME, bytes(device.name, 'utf-8')),
170                    (AdvertisingData.FLAGS, bytes([0x06])),
171                    (
172                        AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
173                        bytes(ASHA_SERVICE),
174                    ),
175                    (
176                        AdvertisingData.SERVICE_DATA_16_BIT_UUID,
177                        bytes(ASHA_SERVICE)
178                        + bytes(
179                            [
180                                0x01,  # Protocol Version
181                                0x00,  # Capability
182                                0x01,
183                                0x02,
184                                0x03,
185                                0x04,  # Truncated HiSyncID
186                            ]
187                        ),
188                    ),
189                ]
190            )
191        )
192
193        # Go!
194        await device.power_on()
195        await device.start_advertising(auto_restart=True)
196
197        await hci_source.wait_for_termination()
198
199
200# -----------------------------------------------------------------------------
201logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
202asyncio.run(main())
203