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