diff --git a/bumble/gatt.py b/bumble/gatt.py index ea65116d..6b00a7e5 100644 --- a/bumble/gatt.py +++ b/bumble/gatt.py @@ -275,6 +275,13 @@ GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC = UUID.from_16_bits(0x2BCD, 'Available Audio Contexts') GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC = UUID.from_16_bits(0x2BCE, 'Supported Audio Contexts') +# Gaming Audio Service (GMAS) +GATT_GMAP_ROLE_CHARACTERISTIC = UUID.from_16_bits(0x2C00, 'GMAP Role') +GATT_UGG_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2C01, 'UGG Features') +GATT_UGT_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2C02, 'UGT Features') +GATT_BGS_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2C03, 'BGS Features') +GATT_BGR_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2C04, 'BGR Features') + # Hearing Access Service GATT_HEARING_AID_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2BDA, 'Hearing Aid Features') GATT_HEARING_AID_PRESET_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BDB, 'Hearing Aid Preset Control Point') diff --git a/bumble/profiles/gmap.py b/bumble/profiles/gmap.py new file mode 100644 index 00000000..6723f70c --- /dev/null +++ b/bumble/profiles/gmap.py @@ -0,0 +1,193 @@ +# Copyright 2024 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. + +"""LE Audio - Gaming Audio Profile""" + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import struct +from typing import Optional + +from bumble.gatt import ( + TemplateService, + DelegatedCharacteristicAdapter, + Characteristic, + GATT_GAMING_AUDIO_SERVICE, + GATT_GMAP_ROLE_CHARACTERISTIC, + GATT_UGG_FEATURES_CHARACTERISTIC, + GATT_UGT_FEATURES_CHARACTERISTIC, + GATT_BGS_FEATURES_CHARACTERISTIC, + GATT_BGR_FEATURES_CHARACTERISTIC, + InvalidServiceError, +) +from bumble.gatt_client import ProfileServiceProxy, ServiceProxy +from bumble.utils import OpenIntEnum + + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- +class GmapRole(OpenIntEnum): + UNICAST_GAME_GATEWAY = 1 << 0 + UNICAST_GAME_TERMINAL = 1 << 1 + BROADCAST_GAME_SENDER = 1 << 2 + BROADCAST_GAME_RECEIVER = 1 << 3 + + +class UggFeatures(OpenIntEnum): + UGG_MULTIPLEX = 1 << 0 + UGG_96_KBPS_SOURCE = 1 << 1 + UGG_MULTISINK = 1 << 2 + + +class UgtFeatures(OpenIntEnum): + UGT_SOURCE = 1 << 0 + UGT_80_KBPS_SOURCE = 1 << 1 + UGT_SINK = 1 << 2 + UGT_64_KBPS_SINK = 1 << 3 + UGT_MULTIPLEX = 1 << 4 + UGT_MULTISINK = 1 << 5 + UGT_MULTISOURCE = 1 << 6 + + +class BgsFeatures(OpenIntEnum): + BGS_96_KBPS = 1 << 0 + + +class BgrFeatures(OpenIntEnum): + BGR_MULTISINK = 1 << 0 + BGR_MULTIPLEX = 1 << 1 + + +# ----------------------------------------------------------------------------- +# Server +# ----------------------------------------------------------------------------- +class GamingAudioService(TemplateService): + UUID = GATT_GAMING_AUDIO_SERVICE + + gmap_role: Characteristic + ugg_features: Optional[Characteristic] + ugt_features: Optional[Characteristic] + bgs_features: Optional[Characteristic] + bgr_features: Optional[Characteristic] + + def __init__( + self, + gmap_role: GmapRole, + ugg_features: Optional[UggFeatures] = None, + ugt_features: Optional[UgtFeatures] = None, + bgs_features: Optional[BgsFeatures] = None, + bgr_features: Optional[BgrFeatures] = None, + ) -> None: + characteristics = [] + + self.gmap_role = Characteristic( + uuid=GATT_GMAP_ROLE_CHARACTERISTIC, + properties=Characteristic.Properties.READ, + permissions=Characteristic.Permissions.READABLE, + value=struct.pack(' None: + self.service_proxy = service_proxy + + if not ( + characteristics := service_proxy.get_characteristics_by_uuid( + GATT_GMAP_ROLE_CHARACTERISTIC + ) + ): + raise InvalidServiceError("GMAP Role Characteristic not found") + self.gmap_role = DelegatedCharacteristicAdapter( + characteristic=characteristics[0], + decode=lambda value: GmapRole(value[0]), + ) + + if characteristics := service_proxy.get_characteristics_by_uuid( + GATT_UGG_FEATURES_CHARACTERISTIC + ): + self.ugg_features = DelegatedCharacteristicAdapter( + characteristic=characteristics[0], + decode=lambda value: UggFeatures(value[0]), + ) + + if characteristics := service_proxy.get_characteristics_by_uuid( + GATT_UGT_FEATURES_CHARACTERISTIC + ): + self.ugt_features = DelegatedCharacteristicAdapter( + characteristic=characteristics[0], + decode=lambda value: UgtFeatures(value[0]), + ) + + if characteristics := service_proxy.get_characteristics_by_uuid( + GATT_BGS_FEATURES_CHARACTERISTIC + ): + self.bgs_features = DelegatedCharacteristicAdapter( + characteristic=characteristics[0], + decode=lambda value: BgsFeatures(value[0]), + ) + + if characteristics := service_proxy.get_characteristics_by_uuid( + GATT_BGR_FEATURES_CHARACTERISTIC + ): + self.bgr_features = DelegatedCharacteristicAdapter( + characteristic=characteristics[0], + decode=lambda value: BgrFeatures(value[0]), + ) diff --git a/tests/gmap_test.py b/tests/gmap_test.py new file mode 100644 index 00000000..36308881 --- /dev/null +++ b/tests/gmap_test.py @@ -0,0 +1,75 @@ +# Copyright 2024 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 pytest +import pytest_asyncio + +from bumble import device +from bumble.profiles.gmap import ( + GamingAudioService, + GamingAudioServiceProxy, + GmapRole, + UggFeatures, + UgtFeatures, + BgrFeatures, + BgsFeatures, +) + +from .test_utils import TwoDevices + +# ----------------------------------------------------------------------------- +# Tests +# ----------------------------------------------------------------------------- +gmas_service = GamingAudioService( + gmap_role=GmapRole.UNICAST_GAME_GATEWAY, + ugg_features=UggFeatures.UGG_MULTISINK, + ugt_features=UgtFeatures.UGT_SOURCE, + bgr_features=BgrFeatures.BGR_MULTISINK, + bgs_features=BgsFeatures.BGS_96_KBPS, +) + + +@pytest_asyncio.fixture +async def gmap_client(): + devices = TwoDevices() + devices[0].add_service(gmas_service) + + await devices.setup_connection() + + assert devices.connections[0] + assert devices.connections[1] + + devices.connections[0].encryption = 1 + devices.connections[1].encryption = 1 + + peer = device.Peer(devices.connections[1]) + + gmap_client = await peer.discover_service_and_create_proxy(GamingAudioServiceProxy) + + assert gmap_client + yield gmap_client + + +# ----------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_init_service(gmap_client: GamingAudioServiceProxy): + assert await gmap_client.gmap_role.read_value() == GmapRole.UNICAST_GAME_GATEWAY + assert await gmap_client.ugg_features.read_value() == UggFeatures.UGG_MULTISINK + assert await gmap_client.ugt_features.read_value() == UgtFeatures.UGT_SOURCE + assert await gmap_client.bgr_features.read_value() == BgrFeatures.BGR_MULTISINK + assert await gmap_client.bgs_features.read_value() == BgsFeatures.BGS_96_KBPS