Loading android/pandora/server/src/Hap.kt +63 −11 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.pandora import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.TRANSPORT_LE import android.bluetooth.BluetoothHapClient Loading @@ -24,7 +25,9 @@ import android.bluetooth.BluetoothHapPresetInfo import android.bluetooth.BluetoothLeAudio import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED import android.content.Context import android.content.IntentFilter import android.media.AudioManager import android.media.AudioTrack import android.util.Log Loading @@ -39,8 +42,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import pandora.HAPGrpc.HAPImplBase import pandora.HapProto.* import pandora.HostProto.Connection Loading @@ -61,12 +68,22 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { private val bluetoothLeAudio = getProfileProxy<BluetoothLeAudio>(context, BluetoothProfile.LE_AUDIO) private val flow = intentFlow( context, IntentFilter().apply { addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED) }, scope, ) .shareIn(scope, SharingStarted.Eagerly) private var audioTrack: AudioTrack? = null private class PresetInfoChanged( var connection: Connection, var presetInfoList: List<BluetoothHapPresetInfo>, var reason: Int var reason: Int, ) {} private val mPresetChanged = callbackFlow { Loading @@ -75,7 +92,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun onPresetSelected( device: BluetoothDevice, presetIndex: Int, reason: Int reason: Int, ) { Log.i(TAG, "$device preset info changed") } Loading @@ -91,7 +108,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun onPresetInfoChanged( device: BluetoothDevice, presetInfoList: List<BluetoothHapPresetInfo>, reason: Int reason: Int, ) { Log.i(TAG, "$device preset info changed") Loading Loading @@ -120,9 +137,22 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { scope.cancel() } override fun getFeatures( request: GetFeaturesRequest, responseObserver: StreamObserver<GetFeaturesResponse>, ) { grpcUnary<GetFeaturesResponse>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Log.i(TAG, "getFeatures(${device})") GetFeaturesResponse.newBuilder() .setFeatures(bluetoothHapClient.getFeatures(device)) .build() } } override fun getPresetRecord( request: GetPresetRecordRequest, responseObserver: StreamObserver<GetPresetRecordResponse> responseObserver: StreamObserver<GetPresetRecordResponse>, ) { grpcUnary<GetPresetRecordResponse>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading Loading @@ -150,7 +180,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun getAllPresetRecords( request: GetAllPresetRecordsRequest, responseObserver: StreamObserver<GetAllPresetRecordsResponse> responseObserver: StreamObserver<GetAllPresetRecordsResponse>, ) { grpcUnary<GetAllPresetRecordsResponse>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading @@ -177,7 +207,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun writePresetName( request: WritePresetNameRequest, responseObserver: StreamObserver<Empty> responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading @@ -192,7 +222,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun setActivePreset( request: SetActivePresetRequest, responseObserver: StreamObserver<Empty> responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading @@ -207,7 +237,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun setNextPreset( request: SetNextPresetRequest, responseObserver: StreamObserver<Empty> responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading @@ -222,7 +252,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun setPreviousPreset( request: SetPreviousPresetRequest, responseObserver: StreamObserver<Empty> responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading Loading @@ -264,7 +294,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { audioManager.setStreamVolume( AudioManager.STREAM_MUSIC, maxVolume, AudioManager.FLAG_SHOW_UI AudioManager.FLAG_SHOW_UI, ) } } Loading Loading @@ -298,7 +328,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun waitPresetChanged( request: Empty, responseObserver: StreamObserver<WaitPresetChangedResponse> responseObserver: StreamObserver<WaitPresetChangedResponse>, ) { grpcUnary<WaitPresetChangedResponse>(scope, responseObserver) { val presetChangedReceived = mPresetChanged.first()!! Loading @@ -322,4 +352,26 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { .build() } } override fun waitPeripheral( request: WaitPeripheralRequest, responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Log.i(TAG, "waitPeripheral(${device}") if (bluetoothHapClient.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { Log.d(TAG, "Manual call to setConnectionPolicy") bluetoothHapClient.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED) Log.d(TAG, "now waiting for bluetoothHapClient profile connection") flow .filter { it.getBluetoothDeviceExtra() == device } .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) } .filter { it == BluetoothProfile.STATE_CONNECTED } .first() } Empty.getDefaultInstance() } } } android/pandora/test/AndroidTest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ <option name="dep-module" value="grpcio==1.51.1" /> <option name="dep-module" value="cryptography==35" /> <option name="dep-module" value="numpy" /> <option name="dep-module" value="pytruth" /> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> <option name="set-property" key="persist.log.tag.bluetooth" value="VERBOSE"/> Loading android/pandora/test/hap_test.py 0 → 100644 +145 −0 Original line number Diff line number Diff line # Copyright (C) 2024 The Android Open Source Project # 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 # http://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. import asyncio from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices, asynchronous from bumble.gatt import GATT_HEARING_ACCESS_SERVICE, GATT_AUDIO_STREAM_CONTROL_SERVICE, GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE from bumble.profiles import hap from bumble.profiles.hap import DynamicPresets, HearingAccessService, HearingAidFeatures, HearingAidType, IndependentPresets, PresetRecord, PresetSynchronizationSupport, WritablePresetsSupport from pandora_experimental.gatt_grpc_aio import GATT from pandora_experimental.hap_grpc_aio import HAP from pandora._utils import AioStream from pandora.security_pb2 import LE_LEVEL3 from pandora.host_pb2 import RANDOM, AdvertiseResponse, Connection, DataTypes, ScanningResponse from mobly import base_test, signals from truth.truth import AssertThat # type: ignore from typing import Tuple COMPLETE_LOCAL_NAME: str = "Bumble" HAP_UUID = GATT_HEARING_ACCESS_SERVICE.to_hex_str('-') ASCS_UUID = GATT_AUDIO_STREAM_CONTROL_SERVICE.to_hex_str('-') PACS_UUID = GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE.to_hex_str('-') long_name = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." foo_preset = PresetRecord(1, "foo preset") bar_preset = PresetRecord(50, "bar preset") longname_preset = PresetRecord(5, f'[{long_name[:38]}]') unavailable_preset = PresetRecord( 7, "unavailable preset", PresetRecord.Property(PresetRecord.Property.Writable.CANNOT_BE_WRITTEN, PresetRecord.Property.IsAvailable.IS_UNAVAILABLE)) class HapTest(base_test.BaseTestClass): devices: PandoraDevices dut: PandoraDevice ref_left: BumblePandoraDevice hap_grpc: HAP has: HearingAccessService def setup_class(self): self.devices = PandoraDevices(self) dut, ref_left, *_ = self.devices if isinstance(dut, BumblePandoraDevice): raise signals.TestAbortClass('DUT Bumble does not support HAP') self.dut = dut if not isinstance(ref_left, BumblePandoraDevice): raise signals.TestAbortClass('Test require Bumble as reference device(s)') self.ref_left = ref_left def teardown_class(self): self.devices.stop_all() @asynchronous async def setup_test(self) -> None: await asyncio.gather(self.dut.reset(), self.ref_left.reset()) self.hap_grpc = HAP(channel=self.dut.aio.channel) device_features = HearingAidFeatures(HearingAidType.MONAURAL_HEARING_AID, PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED, IndependentPresets.IDENTICAL_PRESET_RECORD, DynamicPresets.PRESET_RECORDS_DOES_NOT_CHANGE, WritablePresetsSupport.WRITABLE_PRESET_RECORDS_SUPPORTED) self.has = HearingAccessService(self.ref_left.device, device_features, [foo_preset, bar_preset, longname_preset, unavailable_preset]) self.dut_gatt = GATT(channel=self.dut.aio.channel) self.ref_left.device.add_service(self.has) # type: ignore async def advertise_hap(self, device: PandoraDevice) -> AioStream[AdvertiseResponse]: return device.aio.host.Advertise( legacy=True, connectable=True, own_address_type=RANDOM, data=DataTypes( complete_local_name=COMPLETE_LOCAL_NAME, incomplete_service_class_uuids16=[HAP_UUID], ), ) async def dut_scan_for_hap(self) -> ScanningResponse: """ DUT starts to scan for the Ref device. :return: ScanningResponse for ASHA """ dut_scan = self.dut.aio.host.Scan(RANDOM) # type: ignore scan_response = await anext((x async for x in dut_scan if HAP_UUID in x.data.incomplete_service_class_uuids16)) dut_scan.cancel() return scan_response async def dut_connect_to_ref(self, advertisement: AioStream[AdvertiseResponse], ref: ScanningResponse) -> Tuple[Connection, Connection]: """ Helper method for Dut connects to Ref :return: a Tuple (DUT to REF connection, REF to DUT connection) """ (dut_ref_res, ref_dut_res) = await asyncio.gather( self.dut.aio.host.ConnectLE(own_address_type=RANDOM, **ref.address_asdict()), anext(aiter(advertisement)), ) AssertThat(dut_ref_res.result_variant()).IsEqualTo('connection') # type: ignore dut_ref, ref_dut = dut_ref_res.connection, ref_dut_res.connection AssertThat(dut_ref).IsNotNone() # type: ignore assert dut_ref advertisement.cancel() return dut_ref, ref_dut async def setupHapConnection(self): advertisement = await self.advertise_hap(self.ref_left) scan_response = await self.dut_scan_for_hap() dut_connection_to_ref, ref_connection_to_dut = await self.dut_connect_to_ref(advertisement, scan_response) await self.dut_gatt.ExchangeMTU(mtu=512, connection=dut_connection_to_ref) (secure, wait_security) = await asyncio.gather( self.dut.aio.security.Secure(connection=dut_connection_to_ref, le=LE_LEVEL3), self.ref_left.aio.security.WaitSecurity(connection=ref_connection_to_dut, le=LE_LEVEL3), ) AssertThat(secure.result_variant()).IsEqualTo('success') # type: ignore AssertThat(wait_security.result_variant()).IsEqualTo('success') # type: ignore await self.hap_grpc.WaitPeripheral(connection=dut_connection_to_ref) # type: ignore advertisement.cancel() return dut_connection_to_ref @asynchronous async def test_get_features(self) -> None: dut_connection_to_ref = await self.setupHapConnection() features = hap.HearingAidFeatures_from_bytes( (await self.hap_grpc.GetFeatures(connection=dut_connection_to_ref)).features) AssertThat(features).IsEqualTo(self.has.server_features) # type: ignore android/pandora/test/main.py +2 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import avatar.cases.le_host_test import avatar.cases.le_security_test import avatar.cases.security_test import gatt_test import hap_test import hfpclient_test from pairing import _test_class_list as _pairing_test_class_list import sdp_test Loading @@ -36,6 +37,7 @@ _TEST_CLASSES_LIST = [ aics_test.AicsTest, sdp_test.SdpTest, gatt_test.GattTest, hap_test.HapTest, asha_test.AshaTest, hfpclient_test.HfpClientTest, ] + _pairing_test_class_list Loading pandora/interfaces/pandora_experimental/hap.proto +17 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ option java_outer_classname = "HapProto"; import "google/protobuf/empty.proto"; service HAP { // get the Hearing aid features rpc GetFeatures(GetFeaturesRequest) returns (GetFeaturesResponse); // Set active preset by index rpc SetActivePreset(SetActivePresetRequest) returns (google.protobuf.Empty); // Set next preset Loading @@ -34,6 +36,16 @@ service HAP { rpc GetAllPresetRecords(GetAllPresetRecordsRequest) returns (GetAllPresetRecordsResponse); // Wait for Preset Changed event rpc WaitPresetChanged(google.protobuf.Empty) returns (WaitPresetChangedResponse); // Wait for HAP device to be connected. rpc WaitPeripheral(WaitPeripheralRequest) returns (google.protobuf.Empty); } message GetFeaturesRequest{ Connection connection = 1; } message GetFeaturesResponse{ int32 features = 1; } // Request of the `PlaybackAudio` method. Loading Loading @@ -123,3 +135,8 @@ message WaitPresetChangedResponse { // Reason why the presets were changed uint32 reason = 3; } message WaitPeripheralRequest { Connection connection = 1; } Loading
android/pandora/server/src/Hap.kt +63 −11 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.pandora import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.TRANSPORT_LE import android.bluetooth.BluetoothHapClient Loading @@ -24,7 +25,9 @@ import android.bluetooth.BluetoothHapPresetInfo import android.bluetooth.BluetoothLeAudio import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED import android.content.Context import android.content.IntentFilter import android.media.AudioManager import android.media.AudioTrack import android.util.Log Loading @@ -39,8 +42,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import pandora.HAPGrpc.HAPImplBase import pandora.HapProto.* import pandora.HostProto.Connection Loading @@ -61,12 +68,22 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { private val bluetoothLeAudio = getProfileProxy<BluetoothLeAudio>(context, BluetoothProfile.LE_AUDIO) private val flow = intentFlow( context, IntentFilter().apply { addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED) }, scope, ) .shareIn(scope, SharingStarted.Eagerly) private var audioTrack: AudioTrack? = null private class PresetInfoChanged( var connection: Connection, var presetInfoList: List<BluetoothHapPresetInfo>, var reason: Int var reason: Int, ) {} private val mPresetChanged = callbackFlow { Loading @@ -75,7 +92,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun onPresetSelected( device: BluetoothDevice, presetIndex: Int, reason: Int reason: Int, ) { Log.i(TAG, "$device preset info changed") } Loading @@ -91,7 +108,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun onPresetInfoChanged( device: BluetoothDevice, presetInfoList: List<BluetoothHapPresetInfo>, reason: Int reason: Int, ) { Log.i(TAG, "$device preset info changed") Loading Loading @@ -120,9 +137,22 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { scope.cancel() } override fun getFeatures( request: GetFeaturesRequest, responseObserver: StreamObserver<GetFeaturesResponse>, ) { grpcUnary<GetFeaturesResponse>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Log.i(TAG, "getFeatures(${device})") GetFeaturesResponse.newBuilder() .setFeatures(bluetoothHapClient.getFeatures(device)) .build() } } override fun getPresetRecord( request: GetPresetRecordRequest, responseObserver: StreamObserver<GetPresetRecordResponse> responseObserver: StreamObserver<GetPresetRecordResponse>, ) { grpcUnary<GetPresetRecordResponse>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading Loading @@ -150,7 +180,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun getAllPresetRecords( request: GetAllPresetRecordsRequest, responseObserver: StreamObserver<GetAllPresetRecordsResponse> responseObserver: StreamObserver<GetAllPresetRecordsResponse>, ) { grpcUnary<GetAllPresetRecordsResponse>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading @@ -177,7 +207,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun writePresetName( request: WritePresetNameRequest, responseObserver: StreamObserver<Empty> responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading @@ -192,7 +222,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun setActivePreset( request: SetActivePresetRequest, responseObserver: StreamObserver<Empty> responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading @@ -207,7 +237,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun setNextPreset( request: SetNextPresetRequest, responseObserver: StreamObserver<Empty> responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading @@ -222,7 +252,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun setPreviousPreset( request: SetPreviousPresetRequest, responseObserver: StreamObserver<Empty> responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Loading Loading @@ -264,7 +294,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { audioManager.setStreamVolume( AudioManager.STREAM_MUSIC, maxVolume, AudioManager.FLAG_SHOW_UI AudioManager.FLAG_SHOW_UI, ) } } Loading Loading @@ -298,7 +328,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { override fun waitPresetChanged( request: Empty, responseObserver: StreamObserver<WaitPresetChangedResponse> responseObserver: StreamObserver<WaitPresetChangedResponse>, ) { grpcUnary<WaitPresetChangedResponse>(scope, responseObserver) { val presetChangedReceived = mPresetChanged.first()!! Loading @@ -322,4 +352,26 @@ class Hap(val context: Context) : HAPImplBase(), Closeable { .build() } } override fun waitPeripheral( request: WaitPeripheralRequest, responseObserver: StreamObserver<Empty>, ) { grpcUnary<Empty>(scope, responseObserver) { val device = request.connection.toBluetoothDevice(bluetoothAdapter) Log.i(TAG, "waitPeripheral(${device}") if (bluetoothHapClient.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { Log.d(TAG, "Manual call to setConnectionPolicy") bluetoothHapClient.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED) Log.d(TAG, "now waiting for bluetoothHapClient profile connection") flow .filter { it.getBluetoothDeviceExtra() == device } .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) } .filter { it == BluetoothProfile.STATE_CONNECTED } .first() } Empty.getDefaultInstance() } } }
android/pandora/test/AndroidTest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ <option name="dep-module" value="grpcio==1.51.1" /> <option name="dep-module" value="cryptography==35" /> <option name="dep-module" value="numpy" /> <option name="dep-module" value="pytruth" /> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> <option name="set-property" key="persist.log.tag.bluetooth" value="VERBOSE"/> Loading
android/pandora/test/hap_test.py 0 → 100644 +145 −0 Original line number Diff line number Diff line # Copyright (C) 2024 The Android Open Source Project # 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 # http://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. import asyncio from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices, asynchronous from bumble.gatt import GATT_HEARING_ACCESS_SERVICE, GATT_AUDIO_STREAM_CONTROL_SERVICE, GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE from bumble.profiles import hap from bumble.profiles.hap import DynamicPresets, HearingAccessService, HearingAidFeatures, HearingAidType, IndependentPresets, PresetRecord, PresetSynchronizationSupport, WritablePresetsSupport from pandora_experimental.gatt_grpc_aio import GATT from pandora_experimental.hap_grpc_aio import HAP from pandora._utils import AioStream from pandora.security_pb2 import LE_LEVEL3 from pandora.host_pb2 import RANDOM, AdvertiseResponse, Connection, DataTypes, ScanningResponse from mobly import base_test, signals from truth.truth import AssertThat # type: ignore from typing import Tuple COMPLETE_LOCAL_NAME: str = "Bumble" HAP_UUID = GATT_HEARING_ACCESS_SERVICE.to_hex_str('-') ASCS_UUID = GATT_AUDIO_STREAM_CONTROL_SERVICE.to_hex_str('-') PACS_UUID = GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE.to_hex_str('-') long_name = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." foo_preset = PresetRecord(1, "foo preset") bar_preset = PresetRecord(50, "bar preset") longname_preset = PresetRecord(5, f'[{long_name[:38]}]') unavailable_preset = PresetRecord( 7, "unavailable preset", PresetRecord.Property(PresetRecord.Property.Writable.CANNOT_BE_WRITTEN, PresetRecord.Property.IsAvailable.IS_UNAVAILABLE)) class HapTest(base_test.BaseTestClass): devices: PandoraDevices dut: PandoraDevice ref_left: BumblePandoraDevice hap_grpc: HAP has: HearingAccessService def setup_class(self): self.devices = PandoraDevices(self) dut, ref_left, *_ = self.devices if isinstance(dut, BumblePandoraDevice): raise signals.TestAbortClass('DUT Bumble does not support HAP') self.dut = dut if not isinstance(ref_left, BumblePandoraDevice): raise signals.TestAbortClass('Test require Bumble as reference device(s)') self.ref_left = ref_left def teardown_class(self): self.devices.stop_all() @asynchronous async def setup_test(self) -> None: await asyncio.gather(self.dut.reset(), self.ref_left.reset()) self.hap_grpc = HAP(channel=self.dut.aio.channel) device_features = HearingAidFeatures(HearingAidType.MONAURAL_HEARING_AID, PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED, IndependentPresets.IDENTICAL_PRESET_RECORD, DynamicPresets.PRESET_RECORDS_DOES_NOT_CHANGE, WritablePresetsSupport.WRITABLE_PRESET_RECORDS_SUPPORTED) self.has = HearingAccessService(self.ref_left.device, device_features, [foo_preset, bar_preset, longname_preset, unavailable_preset]) self.dut_gatt = GATT(channel=self.dut.aio.channel) self.ref_left.device.add_service(self.has) # type: ignore async def advertise_hap(self, device: PandoraDevice) -> AioStream[AdvertiseResponse]: return device.aio.host.Advertise( legacy=True, connectable=True, own_address_type=RANDOM, data=DataTypes( complete_local_name=COMPLETE_LOCAL_NAME, incomplete_service_class_uuids16=[HAP_UUID], ), ) async def dut_scan_for_hap(self) -> ScanningResponse: """ DUT starts to scan for the Ref device. :return: ScanningResponse for ASHA """ dut_scan = self.dut.aio.host.Scan(RANDOM) # type: ignore scan_response = await anext((x async for x in dut_scan if HAP_UUID in x.data.incomplete_service_class_uuids16)) dut_scan.cancel() return scan_response async def dut_connect_to_ref(self, advertisement: AioStream[AdvertiseResponse], ref: ScanningResponse) -> Tuple[Connection, Connection]: """ Helper method for Dut connects to Ref :return: a Tuple (DUT to REF connection, REF to DUT connection) """ (dut_ref_res, ref_dut_res) = await asyncio.gather( self.dut.aio.host.ConnectLE(own_address_type=RANDOM, **ref.address_asdict()), anext(aiter(advertisement)), ) AssertThat(dut_ref_res.result_variant()).IsEqualTo('connection') # type: ignore dut_ref, ref_dut = dut_ref_res.connection, ref_dut_res.connection AssertThat(dut_ref).IsNotNone() # type: ignore assert dut_ref advertisement.cancel() return dut_ref, ref_dut async def setupHapConnection(self): advertisement = await self.advertise_hap(self.ref_left) scan_response = await self.dut_scan_for_hap() dut_connection_to_ref, ref_connection_to_dut = await self.dut_connect_to_ref(advertisement, scan_response) await self.dut_gatt.ExchangeMTU(mtu=512, connection=dut_connection_to_ref) (secure, wait_security) = await asyncio.gather( self.dut.aio.security.Secure(connection=dut_connection_to_ref, le=LE_LEVEL3), self.ref_left.aio.security.WaitSecurity(connection=ref_connection_to_dut, le=LE_LEVEL3), ) AssertThat(secure.result_variant()).IsEqualTo('success') # type: ignore AssertThat(wait_security.result_variant()).IsEqualTo('success') # type: ignore await self.hap_grpc.WaitPeripheral(connection=dut_connection_to_ref) # type: ignore advertisement.cancel() return dut_connection_to_ref @asynchronous async def test_get_features(self) -> None: dut_connection_to_ref = await self.setupHapConnection() features = hap.HearingAidFeatures_from_bytes( (await self.hap_grpc.GetFeatures(connection=dut_connection_to_ref)).features) AssertThat(features).IsEqualTo(self.has.server_features) # type: ignore
android/pandora/test/main.py +2 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import avatar.cases.le_host_test import avatar.cases.le_security_test import avatar.cases.security_test import gatt_test import hap_test import hfpclient_test from pairing import _test_class_list as _pairing_test_class_list import sdp_test Loading @@ -36,6 +37,7 @@ _TEST_CLASSES_LIST = [ aics_test.AicsTest, sdp_test.SdpTest, gatt_test.GattTest, hap_test.HapTest, asha_test.AshaTest, hfpclient_test.HfpClientTest, ] + _pairing_test_class_list Loading
pandora/interfaces/pandora_experimental/hap.proto +17 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ option java_outer_classname = "HapProto"; import "google/protobuf/empty.proto"; service HAP { // get the Hearing aid features rpc GetFeatures(GetFeaturesRequest) returns (GetFeaturesResponse); // Set active preset by index rpc SetActivePreset(SetActivePresetRequest) returns (google.protobuf.Empty); // Set next preset Loading @@ -34,6 +36,16 @@ service HAP { rpc GetAllPresetRecords(GetAllPresetRecordsRequest) returns (GetAllPresetRecordsResponse); // Wait for Preset Changed event rpc WaitPresetChanged(google.protobuf.Empty) returns (WaitPresetChangedResponse); // Wait for HAP device to be connected. rpc WaitPeripheral(WaitPeripheralRequest) returns (google.protobuf.Empty); } message GetFeaturesRequest{ Connection connection = 1; } message GetFeaturesResponse{ int32 features = 1; } // Request of the `PlaybackAudio` method. Loading Loading @@ -123,3 +135,8 @@ message WaitPresetChangedResponse { // Reason why the presets were changed uint32 reason = 3; } message WaitPeripheralRequest { Connection connection = 1; }