From b0336adf1cebb511e3eb3163fa792a843c58c1ed Mon Sep 17 00:00:00 2001 From: Yuyang Huang Date: Mon, 28 Nov 2022 15:42:19 -0800 Subject: [PATCH 1/6] add ASHA GATT UUID --- bumble/gatt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bumble/gatt.py b/bumble/gatt.py index 889eaa44..cef42aa1 100644 --- a/bumble/gatt.py +++ b/bumble/gatt.py @@ -151,6 +151,14 @@ GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2A39, 'Heart # Battery Service GATT_BATTERY_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A19, 'Battery Level') +# ASHA Service +GATT_ASHA_SERVICE = UUID.from_16_bits(0xFDF0, 'Audio Streaming for Hearing Aid') +GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC = UUID('6333651e-c481-4a3e-9169-7c902aad37bb', 'ReadOnlyProperties') +GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC = UUID('f0d4de7e-4a88-476c-9d9f-1937b0996cc0', 'AudioControlPoint') +GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC = UUID('38663f1a-e711-4cac-b641-326b56404837', 'AudioStatus') +GATT_ASHA_VOLUME_CHARACTERISTIC = UUID('00e4ca9e-ab14-41e4-8823-f9e70c7e91df', 'Volume') +GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC = UUID('2d410339-82b6-42aa-b34e-e2e01df8cc1a', 'LE_PSM_OUT') + # Misc GATT_DEVICE_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2A00, 'Device Name') GATT_APPEARANCE_CHARACTERISTIC = UUID.from_16_bits(0x2A01, 'Appearance') From d92b7e9b7480f5b51b823bde75b83a2579e68722 Mon Sep 17 00:00:00 2001 From: Yuyang Huang Date: Tue, 29 Nov 2022 10:34:59 -0800 Subject: [PATCH 2/6] add ASHA Profile --- bumble/profiles/asha_service.py | 138 ++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 bumble/profiles/asha_service.py diff --git a/bumble/profiles/asha_service.py b/bumble/profiles/asha_service.py new file mode 100644 index 00000000..96dec633 --- /dev/null +++ b/bumble/profiles/asha_service.py @@ -0,0 +1,138 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import struct +from ..device import ( + Device, AdvertisingType +) +from ..core import AdvertisingData +from ..gatt import ( + GATT_ASHA_SERVICE, + GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC, + GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC, + GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC, + GATT_ASHA_VOLUME_CHARACTERISTIC, + GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC, + TemplateService, + Characteristic, + CharacteristicValue, + PackedCharacteristicAdapter +) + + +# ----------------------------------------------------------------------------- +class AshaService(TemplateService): + UUID = GATT_ASHA_SERVICE + + def __init__(self,device:Device): + self.device=device + + # Handler for volume control + def on_volume_write(connection, value): + print('--- VOLUME Write:', value[0]) + + # Handler for audio control commands + def on_audio_control_point_write(connection, value): + print('--- AUDIO CONTROL POINT Write:', value.hex()) + opcode = value[0] + if opcode == 1: + # Start + audio_type = ('Unknown', 'Ringtone', 'Phone Call', 'Media')[value[2]] + print( + f'### START: codec={value[1]}, audio_type={audio_type}, volume={value[3]}, otherstate={value[4]}') + elif opcode == 2: + print('### STOP') + elif opcode == 3: + print(f'### STATUS: connected={value[1]}') + + # TODO Respond with a status + # asyncio.create_task(device.notify_subscribers(audio_status_characteristic, force=True)) + + self.read_only_properties_characteristic = Characteristic( + GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC, + Characteristic.READ, + Characteristic.READABLE, + bytes([ + 0x01, # Version + 0x00, # Device Capabilities [Left, Monaural] + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, # HiSyncId + 0x01, # Feature Map [LE CoC audio output streaming supported] + 0x00, 0x00, # Render Delay + 0x00, 0x00, # RFU + 0x02, 0x00 # Codec IDs [G.722 at 16 kHz] + ]) + ) + + self.audio_control_point_characteristic = Characteristic( + GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC, + Characteristic.WRITE | Characteristic.WRITE_WITHOUT_RESPONSE, + Characteristic.WRITEABLE, + CharacteristicValue(write=on_audio_control_point_write) + ) + self.audio_status_characteristic = Characteristic( + GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC, + Characteristic.READ | Characteristic.NOTIFY, + Characteristic.READABLE, + bytes([0]) + ) + self.volume_characteristic = Characteristic( + GATT_ASHA_VOLUME_CHARACTERISTIC, + Characteristic.WRITE_WITHOUT_RESPONSE, + Characteristic.WRITEABLE, + CharacteristicValue(write=on_volume_write) + ) + + # TODO add real psm value + self.psm=0x0080 + # self.psm = device.register_l2cap_channel_server(0, on_coc, 8) + self.le_psm_out_characteristic = Characteristic( + GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC, + Characteristic.READ, + Characteristic.READABLE, + struct.pack(' Date: Thu, 1 Dec 2022 10:40:30 -0800 Subject: [PATCH 3/6] add ASHA advertising factory method --- bumble/device.py | 6 + bumble/gatt.py | 9 ++ bumble/profiles/asha_service.py | 207 +++++++++++++++++--------------- 3 files changed, 122 insertions(+), 100 deletions(-) diff --git a/bumble/device.py b/bumble/device.py index 300afd20..f9d62b6f 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -940,6 +940,12 @@ class Device(CompositeEventEmitter): if self.advertising: await self.stop_advertising() + # add each Service's advertising data + for attribute in self.gatt_server.attributes: + if isinstance(attribute, Service): + self.advertising_data += attribute.get_service_advertising_data() + + # Set/update the advertising data if the advertising type allows it if advertising_type.has_data: await self.send_command(HCI_LE_Set_Advertising_Data_Command( diff --git a/bumble/gatt.py b/bumble/gatt.py index cef42aa1..ef944fd4 100644 --- a/bumble/gatt.py +++ b/bumble/gatt.py @@ -210,6 +210,15 @@ class Service(Attribute): self.characteristics = characteristics[:] self.primary = primary + + def get_service_advertising_data(self): + """ + Get Service specific advertising data + Defined by each Service, default value is empty + """ + return b'' + + def __str__(self): return f'Service(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}){"" if self.primary else "*"}' diff --git a/bumble/profiles/asha_service.py b/bumble/profiles/asha_service.py index 96dec633..06fae720 100644 --- a/bumble/profiles/asha_service.py +++ b/bumble/profiles/asha_service.py @@ -17,122 +17,129 @@ # Imports # ----------------------------------------------------------------------------- import struct -from ..device import ( - Device, AdvertisingType -) +import logging from ..core import AdvertisingData from ..gatt import ( - GATT_ASHA_SERVICE, - GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC, - GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC, - GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC, - GATT_ASHA_VOLUME_CHARACTERISTIC, - GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC, - TemplateService, - Characteristic, - CharacteristicValue, - PackedCharacteristicAdapter + GATT_ASHA_SERVICE, + GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC, + GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC, + GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC, + GATT_ASHA_VOLUME_CHARACTERISTIC, + GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC, + TemplateService, + Characteristic, + CharacteristicValue, + PackedCharacteristicAdapter ) +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + # ----------------------------------------------------------------------------- class AshaService(TemplateService): - UUID = GATT_ASHA_SERVICE + UUID = GATT_ASHA_SERVICE + OPCODE_START = 1 + OPCODE_STOP = 2 + OPCODE_STATUS = 3 + PROTOCOL_VERSION = 0x01 + RESERVED_FOR_FUTURE_USE = [00, 00] + FEATURE_MAP = [0x01] # [LE CoC audio output streaming supported] + SUPPORTED_CODEDC_ID = [0x02, 0x01] # Codec IDs [G.722 at 16 kHz] - def __init__(self,device:Device): - self.device=device + def __init__(self, capability: int, hisyncid: []): + self.hisyncid = hisyncid + self.capability = capability # Device Capabilities [Left, Monaural] + self.render_delay = [00, 00] + self.reserved_for_future_use = [00, 00] - # Handler for volume control - def on_volume_write(connection, value): - print('--- VOLUME Write:', value[0]) + # Four least significant bytes of the HiSyncId. + self.truncated_hisyncid = self.hisyncid[:4] - # Handler for audio control commands - def on_audio_control_point_write(connection, value): - print('--- AUDIO CONTROL POINT Write:', value.hex()) - opcode = value[0] - if opcode == 1: - # Start - audio_type = ('Unknown', 'Ringtone', 'Phone Call', 'Media')[value[2]] - print( - f'### START: codec={value[1]}, audio_type={audio_type}, volume={value[3]}, otherstate={value[4]}') - elif opcode == 2: - print('### STOP') - elif opcode == 3: - print(f'### STATUS: connected={value[1]}') + # Handler for volume control + def on_volume_write(connection, value): + logger.info(f'--- VOLUME Write:{value[0]}') - # TODO Respond with a status - # asyncio.create_task(device.notify_subscribers(audio_status_characteristic, force=True)) + # Handler for audio control commands + def on_audio_control_point_write(connection, value): + logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}') + opcode = value[0] + if opcode == AshaService.OPCODE_START: + # Start + audio_type = ('Unknown', 'Ringtone', 'Phone Call', 'Media')[value[2]] + logger.info( + f'### START: codec={value[1]}, audio_type={audio_type}, volume={value[3]}, otherstate={value[4]}') + elif opcode == AshaService.OPCODE_STOP: + logger.info('### STOP') + elif opcode == AshaService.OPCODE_STATUS: + logger.info(f'### STATUS: connected={value[1]}') - self.read_only_properties_characteristic = Characteristic( - GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC, - Characteristic.READ, - Characteristic.READABLE, - bytes([ - 0x01, # Version - 0x00, # Device Capabilities [Left, Monaural] - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, # HiSyncId - 0x01, # Feature Map [LE CoC audio output streaming supported] - 0x00, 0x00, # Render Delay - 0x00, 0x00, # RFU - 0x02, 0x00 # Codec IDs [G.722 at 16 kHz] - ]) - ) + # TODO Respond with a status + # asyncio.create_task(device.notify_subscribers(audio_status_characteristic, force=True)) - self.audio_control_point_characteristic = Characteristic( - GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC, - Characteristic.WRITE | Characteristic.WRITE_WITHOUT_RESPONSE, - Characteristic.WRITEABLE, - CharacteristicValue(write=on_audio_control_point_write) - ) - self.audio_status_characteristic = Characteristic( - GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC, - Characteristic.READ | Characteristic.NOTIFY, - Characteristic.READABLE, - bytes([0]) - ) - self.volume_characteristic = Characteristic( - GATT_ASHA_VOLUME_CHARACTERISTIC, - Characteristic.WRITE_WITHOUT_RESPONSE, - Characteristic.WRITEABLE, - CharacteristicValue(write=on_volume_write) - ) + self.read_only_properties_characteristic = Characteristic( + GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC, + Characteristic.READ, + Characteristic.READABLE, + bytes([ + AshaService.PROTOCOL_VERSION, # Version + self.capability, + ]) + + bytes(self.hisyncid) + + bytes(AshaService.FEATURE_MAP)+ + bytes(self.render_delay)+ + bytes(AshaService.RESERVED_FOR_FUTURE_USE)+ + bytes(AshaService.SUPPORTED_CODEDC_ID) + ) - # TODO add real psm value - self.psm=0x0080 - # self.psm = device.register_l2cap_channel_server(0, on_coc, 8) - self.le_psm_out_characteristic = Characteristic( - GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC, - Characteristic.READ, - Characteristic.READABLE, - struct.pack(' Date: Thu, 1 Dec 2022 10:50:15 -0800 Subject: [PATCH 4/6] code style update --- bumble/device.py | 1 - bumble/gatt.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/bumble/device.py b/bumble/device.py index f9d62b6f..60129ff7 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -945,7 +945,6 @@ class Device(CompositeEventEmitter): if isinstance(attribute, Service): self.advertising_data += attribute.get_service_advertising_data() - # Set/update the advertising data if the advertising type allows it if advertising_type.has_data: await self.send_command(HCI_LE_Set_Advertising_Data_Command( diff --git a/bumble/gatt.py b/bumble/gatt.py index ef944fd4..48baa51d 100644 --- a/bumble/gatt.py +++ b/bumble/gatt.py @@ -210,7 +210,6 @@ class Service(Attribute): self.characteristics = characteristics[:] self.primary = primary - def get_service_advertising_data(self): """ Get Service specific advertising data @@ -218,7 +217,6 @@ class Service(Attribute): """ return b'' - def __str__(self): return f'Service(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}){"" if self.primary else "*"}' From 29f9a79502b00566c956fcbbc34b4761c0f96ba6 Mon Sep 17 00:00:00 2001 From: Yuyang Huang Date: Mon, 5 Dec 2022 11:21:20 -0800 Subject: [PATCH 5/6] improve get service advertising data --- bumble/device.py | 5 ----- bumble/gatt.py | 5 +++-- bumble/gatt_server.py | 9 +++++++++ bumble/profiles/asha_service.py | 5 ++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/bumble/device.py b/bumble/device.py index 60129ff7..300afd20 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -940,11 +940,6 @@ class Device(CompositeEventEmitter): if self.advertising: await self.stop_advertising() - # add each Service's advertising data - for attribute in self.gatt_server.attributes: - if isinstance(attribute, Service): - self.advertising_data += attribute.get_service_advertising_data() - # Set/update the advertising data if the advertising type allows it if advertising_type.has_data: await self.send_command(HCI_LE_Set_Advertising_Data_Command( diff --git a/bumble/gatt.py b/bumble/gatt.py index 48baa51d..fda89bbe 100644 --- a/bumble/gatt.py +++ b/bumble/gatt.py @@ -210,12 +210,13 @@ class Service(Attribute): self.characteristics = characteristics[:] self.primary = primary - def get_service_advertising_data(self): + def get_advertising_data(self): """ Get Service specific advertising data Defined by each Service, default value is empty + :return Service data for advertising """ - return b'' + return None def __str__(self): return f'Service(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}){"" if self.primary else "*"}' diff --git a/bumble/gatt_server.py b/bumble/gatt_server.py index 5b6fea31..cb4934b7 100644 --- a/bumble/gatt_server.py +++ b/bumble/gatt_server.py @@ -66,6 +66,15 @@ class Server(EventEmitter): def next_handle(self): return 1 + len(self.attributes) + def get_advertising_service_data(self): + service_advertising_data_map = dict[Service, bytes]() + for attribute in self.attributes: + if isinstance(attribute, Service): + if not attribute.get_advertising_data(): + continue + service_advertising_data_map[attribute] = attribute.get_advertising_data() + return service_advertising_data_map + def get_attribute(self, handle): attribute = self.attributes_by_handle.get(handle) if attribute: diff --git a/bumble/profiles/asha_service.py b/bumble/profiles/asha_service.py index 06fae720..8edab97b 100644 --- a/bumble/profiles/asha_service.py +++ b/bumble/profiles/asha_service.py @@ -49,7 +49,7 @@ class AshaService(TemplateService): FEATURE_MAP = [0x01] # [LE CoC audio output streaming supported] SUPPORTED_CODEDC_ID = [0x02, 0x01] # Codec IDs [G.722 at 16 kHz] - def __init__(self, capability: int, hisyncid: []): + def __init__(self, capability: int, hisyncid: [int]): self.hisyncid = hisyncid self.capability = capability # Device Capabilities [Left, Monaural] self.render_delay = [00, 00] @@ -131,8 +131,7 @@ class AshaService(TemplateService): super().__init__(characteristics) - - def get_service_advertising_data(self): + def get_advertising_data(self): return bytes( AdvertisingData([ (AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, bytes(GATT_ASHA_SERVICE)), From 52db1cfcc1c3d79fc080a97e664a1b5e469abd27 Mon Sep 17 00:00:00 2001 From: Yuyang Huang Date: Tue, 6 Dec 2022 07:38:05 -0800 Subject: [PATCH 6/6] improve code style --- bumble/gatt_server.py | 11 ++++------- bumble/profiles/asha_service.py | 19 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/bumble/gatt_server.py b/bumble/gatt_server.py index cb4934b7..7e0c5eff 100644 --- a/bumble/gatt_server.py +++ b/bumble/gatt_server.py @@ -67,13 +67,10 @@ class Server(EventEmitter): return 1 + len(self.attributes) def get_advertising_service_data(self): - service_advertising_data_map = dict[Service, bytes]() - for attribute in self.attributes: - if isinstance(attribute, Service): - if not attribute.get_advertising_data(): - continue - service_advertising_data_map[attribute] = attribute.get_advertising_data() - return service_advertising_data_map + return { + attribute: data for attribute in self.attributes + if isinstance(attribute, Service) and (data := attribute.get_advertising_data()) + } def get_attribute(self, handle): attribute = self.attributes_by_handle.get(handle) diff --git a/bumble/profiles/asha_service.py b/bumble/profiles/asha_service.py index 8edab97b..becfde49 100644 --- a/bumble/profiles/asha_service.py +++ b/bumble/profiles/asha_service.py @@ -47,16 +47,12 @@ class AshaService(TemplateService): PROTOCOL_VERSION = 0x01 RESERVED_FOR_FUTURE_USE = [00, 00] FEATURE_MAP = [0x01] # [LE CoC audio output streaming supported] - SUPPORTED_CODEDC_ID = [0x02, 0x01] # Codec IDs [G.722 at 16 kHz] + SUPPORTED_CODEC_ID = [0x02, 0x01] # Codec IDs [G.722 at 16 kHz] + RENDER_DELAY = [00, 00] def __init__(self, capability: int, hisyncid: [int]): self.hisyncid = hisyncid self.capability = capability # Device Capabilities [Left, Monaural] - self.render_delay = [00, 00] - self.reserved_for_future_use = [00, 00] - - # Four least significant bytes of the HiSyncId. - self.truncated_hisyncid = self.hisyncid[:4] # Handler for volume control def on_volume_write(connection, value): @@ -88,10 +84,10 @@ class AshaService(TemplateService): self.capability, ]) + bytes(self.hisyncid) + - bytes(AshaService.FEATURE_MAP)+ - bytes(self.render_delay)+ - bytes(AshaService.RESERVED_FOR_FUTURE_USE)+ - bytes(AshaService.SUPPORTED_CODEDC_ID) + bytes(AshaService.FEATURE_MAP) + + bytes(AshaService.RENDER_DELAY) + + bytes(AshaService.RESERVED_FOR_FUTURE_USE) + + bytes(AshaService.SUPPORTED_CODEC_ID) ) self.audio_control_point_characteristic = Characteristic( @@ -132,13 +128,14 @@ class AshaService(TemplateService): super().__init__(characteristics) def get_advertising_data(self): + # Advertisement only uses 4 least significant bytes of the HiSyncId. return bytes( AdvertisingData([ (AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, bytes(GATT_ASHA_SERVICE)), (AdvertisingData.SERVICE_DATA_16_BIT_UUID, bytes(GATT_ASHA_SERVICE) + bytes([ AshaService.PROTOCOL_VERSION, self.capability, - ]) + bytes(self.truncated_hisyncid)) + ]) + bytes(self.hisyncid[:4])) ]) )