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