Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e87b1f27 authored by Treehugger Robot's avatar Treehugger Robot Committed by Automerger Merge Worker
Browse files

Merge "[Pandora] Add ASHA music streaming test" am: 851cda35 am: 7029d359

parents e1125834 7029d359
Loading
Loading
Loading
Loading
+258 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.
 */

package com.android.pandora

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothHearingAid
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioRouting
import android.media.AudioTrack
import android.os.Handler
import android.os.Looper
import android.util.Log
import io.grpc.Status
import io.grpc.stub.StreamObserver
import java.io.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
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.asha.AshaGrpc.AshaImplBase
import pandora.asha.AshaProto.*

@kotlinx.coroutines.ExperimentalCoroutinesApi
class Asha(val context: Context) : AshaImplBase(), Closeable {
  private val TAG = "PandoraAsha"
  private val scope: CoroutineScope
  private val flow: Flow<Intent>

  private val bluetoothManager =
    context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
  private val bluetoothHearingAid =
    getProfileProxy<BluetoothHearingAid>(context, BluetoothProfile.HEARING_AID)
  private val bluetoothAdapter = bluetoothManager.adapter
  private val audioManager = context.getSystemService(AudioManager::class.java)!!

  private var audioTrack: AudioTrack? = null

  init {
    // Init the CoroutineScope
    scope = CoroutineScope(Dispatchers.Default)
    val intentFilter = IntentFilter()
    intentFilter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED)
    flow = intentFlow(context, intentFilter).shareIn(scope, SharingStarted.Eagerly)
  }

  override fun close() {
    // Deinit the CoroutineScope
    scope.cancel()
  }

  override fun waitPeripheral(
    request: WaitPeripheralRequest,
    responseObserver: StreamObserver<WaitPeripheralResponse>
  ) {
    grpcUnary<WaitPeripheralResponse>(scope, responseObserver) {
      Log.i(TAG, "waitPeripheral")

      val device = request.connection.toBluetoothDevice(bluetoothAdapter)
      Log.d(TAG, "connection address ${device.getAddress()}")

      if (bluetoothHearingAid.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
        Log.d(TAG, "wait for bluetoothHearingAid profile connection")
        flow
          .filter { it.getAction() == BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED }
          .filter { it.getBluetoothDeviceExtra() == device }
          .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) }
          .filter { it == BluetoothProfile.STATE_CONNECTED }
          .first()
      }

      WaitPeripheralResponse.getDefaultInstance()
    }
  }

  override fun start(request: StartRequest, responseObserver: StreamObserver<StartResponse>) {
    grpcUnary<StartResponse>(scope, responseObserver) {
      Log.i(TAG, "play")

      // wait until BluetoothHearingAid profile is connected
      val device = request.connection.toBluetoothDevice(bluetoothAdapter)
      Log.d(TAG, "connection address ${device.getAddress()}")

      if (bluetoothHearingAid.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
        throw RuntimeException("Hearing aid device is not connected, cannot start")
      }

      // wait for hearing aid is added as an audio device
      val audioDeviceAddedFlow = callbackFlow {
        val outputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
        for (outputDevice in outputDevices) {
          if (
            outputDevice.type == AudioDeviceInfo.TYPE_HEARING_AID &&
              outputDevice.address.equals(device.getAddress())
          ) {
            trySendBlocking(null)
          }
        }

        val audioDeviceCallback =
          object : AudioDeviceCallback() {
            override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
              for (addedDevice in addedDevices) {
                if (
                  addedDevice.type == AudioDeviceInfo.TYPE_HEARING_AID &&
                    addedDevice.address.equals(device.getAddress())
                ) {
                  Log.d(TAG, "TYPE_HEARING_AID added with address: ${addedDevice.address}")
                  trySendBlocking(null)
                }
              }
            }
          }

        audioManager.registerAudioDeviceCallback(
          audioDeviceCallback,
          Handler(Looper.getMainLooper())
        )
        awaitClose { audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) }
      }
      audioDeviceAddedFlow.first()

      if (audioTrack == null) {
        audioTrack = buildAudioTrack()
        Log.i(TAG, "buildAudioTrack")
      }
      audioTrack!!.play()

      // wait for hearing aid is selected as routed device
      val audioRoutingFlow = callbackFlow {
        if (audioTrack!!.routedDevice.type == AudioDeviceInfo.TYPE_HEARING_AID) {
          Log.d(TAG, "already route to TYPE_HEARING_AID")
          trySendBlocking(null)
        }

        val audioRoutingListener =
          object : AudioRouting.OnRoutingChangedListener {
            override fun onRoutingChanged(router: AudioRouting) {
              if (router.routedDevice.type == AudioDeviceInfo.TYPE_HEARING_AID) {
                Log.d(TAG, "Route to TYPE_HEARING_AID")
                trySendBlocking(null)
              } else {
                val outputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
                for (outputDevice in outputDevices) {
                  Log.d(TAG, "available output device in listener:${outputDevice.type}")
                  if (outputDevice.type == AudioDeviceInfo.TYPE_HEARING_AID) {
                    val result = router.setPreferredDevice(outputDevice)
                    Log.d(TAG, "setPreferredDevice result:$result")
                    trySendBlocking(null)
                  }
                }
              }
            }
          }

        audioTrack!!.addOnRoutingChangedListener(
          audioRoutingListener,
          Handler(Looper.getMainLooper())
        )
        awaitClose { audioTrack!!.removeOnRoutingChangedListener(audioRoutingListener) }
      }
      audioRoutingFlow.first()

      val minVolume = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)
      audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, minVolume, AudioManager.FLAG_SHOW_UI)

      StartResponse.getDefaultInstance()
    }
  }

  override fun stop(request: StopRequest, responseObserver: StreamObserver<StopResponse>) {
    grpcUnary<StopResponse>(scope, responseObserver) {
      Log.i(TAG, "stop")
      audioTrack!!.pause()
      audioTrack!!.flush()

      StopResponse.getDefaultInstance()
    }
  }

  override fun playbackAudio(
    responseObserver: StreamObserver<PlaybackAudioResponse>
  ): StreamObserver<PlaybackAudioRequest> {
    Log.i(TAG, "playbackAudio")
    if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
      responseObserver.onError(
        Status.UNKNOWN.withDescription("AudioTrack is not started").asException()
      )
    }

    // Volume is maxed out to avoid any amplitude modification of the provided audio data,
    // enabling the test runner to do comparisons between input and output audio signal.
    // Any volume modification should be done before providing the audio data.
    if (audioManager.isVolumeFixed) {
      Log.w(TAG, "Volume is fixed, cannot max out the volume")
    } else {
      val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
      if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) {
        audioManager.setStreamVolume(
          AudioManager.STREAM_MUSIC,
          maxVolume,
          AudioManager.FLAG_SHOW_UI
        )
      }
    }

    return object : StreamObserver<PlaybackAudioRequest> {
      override fun onNext(request: PlaybackAudioRequest) {
        val data = request.data.toByteArray()
        Log.d(TAG, "audio track writes data=$data")
        val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) }
        if (written != data.size) {
          Log.e(TAG, "AudioTrack write failed")
          responseObserver.onError(
            Status.UNKNOWN.withDescription("AudioTrack write failed").asException()
          )
        }
      }
      override fun onError(t: Throwable?) {
        Log.e(TAG, t.toString())
        responseObserver.onError(t)
      }
      override fun onCompleted() {
        Log.i(TAG, "onCompleted")
        responseObserver.onNext(PlaybackAudioResponse.getDefaultInstance())
        responseObserver.onCompleted()
      }
    }
  }
}
+1 −0
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ class Server(context: Context) {
        mapOf(
            BluetoothProfile.A2DP to ::A2dp,
            BluetoothProfile.A2DP_SINK to ::A2dpSink,
            BluetoothProfile.HEARING_AID to ::Asha,
            BluetoothProfile.AVRCP to ::Avrcp,
            BluetoothProfile.GATT to ::Gatt,
            BluetoothProfile.HEADSET to ::Hfp,
+243 −21
Original line number Diff line number Diff line
@@ -21,19 +21,22 @@ import logging
from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices, asynchronous, bumble_server
from bumble.gatt import GATT_ASHA_SERVICE
from bumble.smp import PairingDelegate
from bumble_experimental.asha import ASHAService
from bumble_experimental.asha import AshaGattService, AshaService
from mobly import base_test, signals, test_runner
from mobly.asserts import assert_equal  # type: ignore
from mobly.asserts import assert_in  # type: ignore
from mobly.asserts import skip  # type: ignore
from pandora._utils import AioStream
from pandora.host_pb2 import PUBLIC, RANDOM, AdvertiseResponse, Connection, DataTypes, OwnAddressType, ScanningResponse
from pandora.security_pb2 import LE_LEVEL3, LESecurityLevel
from pandora.security_pb2 import LE_LEVEL3
from pandora_experimental.asha_grpc_aio import Asha as AioAsha, add_AshaServicer_to_server
from typing import List, Optional, Tuple

ASHA_UUID = GATT_ASHA_SERVICE.to_hex_str()
HISYCNID: List[int] = [0x01, 0x02, 0x03, 0x04, 0x5, 0x6, 0x7, 0x8]
COMPLETE_LOCAL_NAME: str = "Bumble"
AUDIO_SIGNAL_AMPLITUDE = 0.8
AUDIO_SIGNAL_SAMPLING_RATE = 44100


class Ear(enum.IntEnum):
@@ -51,17 +54,26 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]

    # pandora devices.
    dut: PandoraDevice
    ref_left: PandoraDevice
    ref_right: PandoraDevice
    ref_left: BumblePandoraDevice
    ref_right: BumblePandoraDevice

    def setup_class(self) -> None:
        # Register experimental bumble servicers hook.
        bumble_server.register_servicer_hook(
            lambda bumble, server: add_AshaServicer_to_server(ASHAService(bumble.device), server)
            lambda bumble, server: add_AshaServicer_to_server(AshaService(bumble.device), server)
        )

        self.devices = PandoraDevices(self)
        self.dut, self.ref_left, self.ref_right, *_ = self.devices
        self.dut, ref_left, ref_right, *_ = self.devices

        if isinstance(self.dut, BumblePandoraDevice):
            raise signals.TestAbortAll('DUT Bumble does not support Asha source')
        if not isinstance(ref_left, BumblePandoraDevice):
            raise signals.TestAbortAll('Test require Bumble as reference device(s)')
        if not isinstance(ref_right, BumblePandoraDevice):
            raise signals.TestAbortAll('Test require Bumble as reference device(s)')

        self.ref_left, self.ref_right = ref_left, ref_right

    def teardown_class(self) -> None:
        if self.devices:
@@ -71,13 +83,6 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]
    async def setup_test(self) -> None:
        await asyncio.gather(self.dut.reset(), self.ref_left.reset(), self.ref_right.reset())

        if isinstance(self.dut, BumblePandoraDevice):
            raise signals.TestSkip('DUT Bumble does not support Asha source')
        if not isinstance(self.ref_left, BumblePandoraDevice):
            raise signals.TestSkip('Test require Bumble as reference device(s)')
        if not isinstance(self.ref_right, BumblePandoraDevice):
            raise signals.TestSkip('Test require Bumble as reference device(s)')

        # ASHA hearing aid's IO capability is NO_OUTPUT_NO_INPUT
        setattr(self.ref_left.device, "io_capability", PairingDelegate.NO_OUTPUT_NO_INPUT)
        setattr(self.ref_right.device, "io_capability", PairingDelegate.NO_OUTPUT_NO_INPUT)
@@ -149,7 +154,7 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]
            assert e.code() == grpc.StatusCode.DEADLINE_EXCEEDED  # type: ignore
            return True

    def get_expected_advertisement_data(self, ear: Ear):
    def get_expected_advertisement_data(self, ear: Ear) -> str:
        protocol_version = 0x01
        truncated_hisyncid = HISYCNID[:4]
        return (
@@ -268,7 +273,7 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]
        Verify that DUT and Ref are bonded and connected.
        """

        async def ref_device_connect(ref_device, ear):
        async def ref_device_connect(ref_device: BumblePandoraDevice, ear: Ear) -> Tuple[Connection, Connection]:
            advertisement = await self.ref_advertise_asha(
                ref_device=ref_device, ref_address_type=ref_address_type, ear=ear
            )
@@ -335,16 +340,13 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]

        dut_ref, ref_dut = await self.dut_connect_to_ref(advertisement, ref, dut_address_type)

        secure = self.dut.security.Secure(connection=dut_ref, le=LESecurityLevel.LE_LEVEL3)
        secure = self.dut.security.Secure(connection=dut_ref, le=LE_LEVEL3)

        assert_equal(secure.WhichOneof("result"), "success")
        await self.dut.aio.host.Disconnect(dut_ref)
        await self.ref_left.aio.host.WaitDisconnection(ref_dut)

        # delete the bond
        if dut_address_type == OwnAddressType.PUBLIC:
            await self.dut.aio.security_storage.DeleteBond(public=self.ref_left.address)
        else:
        await self.dut.aio.security_storage.DeleteBond(random=self.ref_left.random_address)

        # DUT connect to REF again
@@ -357,7 +359,7 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]
        advertisement.cancel()
        assert dut_ref

        secure = await self.dut.aio.security.Secure(connection=dut_ref, le=LESecurityLevel.LE_LEVEL3)
        secure = await self.dut.aio.security.Secure(connection=dut_ref, le=LE_LEVEL3)

        assert_equal(secure.WhichOneof("result"), "success")

@@ -641,6 +643,226 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]
            advertisement_right.cancel()
            assert ref_right_dut

    @asynchronous
    async def test_music_start(self) -> None:
        """
        DUT discovers Ref.
        DUT initiates connection to Ref.
        Verify that DUT and Ref are bonded and connected.
        DUT starts media streaming.
        Verify that DUT sends a correct AudioControlPoint `Start` command (codec=1,
        audiotype=0, volume=<volume set on DUT>, otherstate=<state of Ref aux if dual devices>).
        """
        advertisement = await self.ref_advertise_asha(ref_device=self.ref_left, ref_address_type=RANDOM, ear=Ear.LEFT)

        ref = await self.dut_scan_for_asha(dut_address_type=RANDOM, ear=Ear.LEFT)

        # DUT initiates connection to Ref.
        dut_ref, ref_dut = await self.dut_connect_to_ref(advertisement, ref, RANDOM)
        assert dut_ref, ref_dut

        asha_service = next((x for x in self.ref_left.device.gatt_server.attributes if isinstance(x, AshaGattService)))

        # check DUT read le_psm
        le_psm_future = asyncio.get_running_loop().create_future()

        def le_psm_handler(connection: Connection, data: int) -> None:
            le_psm_future.set_result(data)

        asha_service.on('le_psm_out', le_psm_handler)

        # check DUT read read_only_properties
        read_only_properties_future = asyncio.get_running_loop().create_future()

        def read_only_properties_handler(connection: Connection, data: bytes) -> None:
            read_only_properties_future.set_result(data)

        asha_service.on('read_only_properties', read_only_properties_handler)

        # DUT starts pairing with the Ref.
        # FIXME: assert the security Level on ref side
        await self.dut.aio.security.Secure(connection=dut_ref, le=LE_LEVEL3)

        le_psm_out_result = await asyncio.wait_for(le_psm_future, timeout=3.0)
        assert le_psm_out_result is not None

        read_only_properties_result = await asyncio.wait_for(read_only_properties_future, timeout=3.0)
        assert read_only_properties_result is not None

        dut_asha = AioAsha(self.dut.aio.channel)

        start_future = asyncio.get_running_loop().create_future()

        def start_command_handler(connection: Connection, data: dict[str, int]) -> None:
            start_future.set_result(data)

        asha_service.on('start', start_command_handler)

        logging.info("send start")
        await dut_asha.WaitPeripheral(connection=dut_ref)
        _, start_result = await asyncio.gather(
            dut_asha.Start(connection=dut_ref), asyncio.wait_for(start_future, timeout=3.0)
        )

        logging.info(f"start_result:{start_result}")
        assert start_result is not None
        assert isinstance(start_result, dict)
        assert start_result['codec'] == 1
        assert start_result['audiotype'] == 0
        assert start_result['volume'] is not None
        assert start_result['otherstate'] == 0

    @asynchronous
    async def test_set_volume(self) -> None:
        """
        DUT discovers Ref.
        DUT initiates connection to Ref.
        Verify that DUT and Ref are bonded and connected.
        DUT is streaming media to Ref.
        Change volume on DUT.
        Verify DUT writes the correct value to ASHA `Volume` characteristic.
        """
        raise signals.TestSkip("TODO: update bt test interface for SetVolume to retry")

        advertisement = await self.ref_advertise_asha(ref_device=self.ref_left, ref_address_type=RANDOM, ear=Ear.LEFT)

        ref = await self.dut_scan_for_asha(dut_address_type=RANDOM, ear=Ear.LEFT)

        # DUT initiates connection to Ref.
        dut_ref, ref_dut = await self.dut_connect_to_ref(advertisement, ref, RANDOM)
        assert dut_ref, ref_dut

        # DUT starts pairing with the Ref.
        # FIXME: assert the security Level on ref side
        await self.dut.aio.security.Secure(connection=dut_ref, le=LE_LEVEL3)

        asha_service = next((x for x in self.ref_left.device.gatt_server.attributes if isinstance(x, AshaGattService)))
        dut_asha = AioAsha(self.dut.aio.channel)

        volume_future = asyncio.get_running_loop().create_future()

        def volume_command_handler(connection: Connection, data: int):
            volume_future.set_result(data)

        asha_service.on('volume', volume_command_handler)

        await dut_asha.WaitPeripheral(connection=dut_ref)
        await dut_asha.Start(connection=dut_ref)
        # set volume to max volume
        _, volume_result = await asyncio.gather(dut_asha.SetVolume(1), asyncio.wait_for(volume_future, timeout=3.0))

        logging.info(f"start_result:{volume_result}")
        assert volume_result is not None
        assert volume_result == 0  # Android max volume's value is 0

    @asynchronous
    async def test_music_stop(self) -> None:
        """
        DUT discovers Ref.
        DUT initiates connection to Ref.
        Verify that DUT and Ref are bonded and connected.
        DUT is streaming media to Ref.
        DUT stops media streaming on Ref.
        Verify that DUT sends a correct AudioControlPoint `Stop` command.
        """
        advertisement = await self.ref_advertise_asha(ref_device=self.ref_left, ref_address_type=RANDOM, ear=Ear.LEFT)

        ref = await self.dut_scan_for_asha(dut_address_type=RANDOM, ear=Ear.LEFT)

        # DUT initiates connection to Ref.
        dut_ref, ref_dut = await self.dut_connect_to_ref(advertisement, ref, RANDOM)
        assert dut_ref, ref_dut

        # DUT starts pairing with the Ref.
        # FIXME: assert the security Level on ref side
        await self.dut.aio.security.Secure(connection=dut_ref, le=LE_LEVEL3)

        asha_service = next((x for x in self.ref_left.device.gatt_server.attributes if isinstance(x, AshaGattService)))
        dut_asha = AioAsha(self.dut.aio.channel)

        stop_future = asyncio.get_running_loop().create_future()

        def stop_command_handler(connection: Connection) -> None:
            stop_future.set_result(connection)

        asha_service.on('stop', stop_command_handler)

        await dut_asha.WaitPeripheral(connection=dut_ref)
        await dut_asha.Start(connection=dut_ref)
        logging.info("send stop")
        _, stop_result = await asyncio.gather(dut_asha.Stop(), asyncio.wait_for(stop_future, timeout=10.0))

        logging.info(f"stop_result:{stop_result}")
        assert stop_result is not None

        ref_asha = AioAsha(self.ref_left.aio.channel)
        try:
            ref_asha.CaptureAudio(connection=ref_dut, timeout=2)
        except grpc.aio.AioRpcError as e:
            if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
                logging.info("no audio data, work as expected")
            else:
                raise e

    @asynchronous
    async def test_music_restart(self) -> None:
        """
        DUT discovers Ref.
        DUT initiates connection to Ref.
        Verify that DUT and Ref are bonded and connected.
        DUT starts media streaming.
        DUT stops media streaming.
        Verify that DUT sends a correct AudioControlPoint `Stop` command.
        DUT starts media streaming again.
        Verify that DUT sends a correct AudioControlPoint `Start` command.
        """
        advertisement = await self.ref_advertise_asha(ref_device=self.ref_left, ref_address_type=RANDOM, ear=Ear.LEFT)

        ref = await self.dut_scan_for_asha(dut_address_type=RANDOM, ear=Ear.LEFT)

        # DUT initiates connection to Ref.
        dut_ref, ref_dut = await self.dut_connect_to_ref(advertisement, ref, RANDOM)
        assert dut_ref, ref_dut

        # DUT starts pairing with the Ref.
        # FIXME: assert the security Level on ref side
        await self.dut.aio.security.Secure(connection=dut_ref, le=LE_LEVEL3)

        asha_service = next((x for x in self.ref_left.device.gatt_server.attributes if isinstance(x, AshaGattService)))
        dut_asha = AioAsha(self.dut.aio.channel)

        stop_future = asyncio.get_running_loop().create_future()

        def stop_command_handler(connection: Connection) -> None:
            stop_future.set_result(connection)

        asha_service.on('stop', stop_command_handler)

        await dut_asha.WaitPeripheral(connection=dut_ref)
        await dut_asha.Start(connection=dut_ref)
        _, stop_result = await asyncio.gather(dut_asha.Stop(), asyncio.wait_for(stop_future, timeout=10.0))

        logging.info(f"stop_result:{stop_result}")
        assert stop_result is not None

        # restart music streaming
        logging.info("restart music streaming")

        start_future = asyncio.get_running_loop().create_future()

        def start_command_handler(connection: Connection, data: dict[str, int]) -> None:
            start_future.set_result(data)

        asha_service.on('start', start_command_handler)

        await dut_asha.WaitPeripheral(connection=dut_ref)
        _, start_result = await asyncio.gather(
            dut_asha.Start(connection=dut_ref), asyncio.wait_for(start_future, timeout=3.0)
        )

        logging.info(f"start_result:{start_result}")
        assert start_result is not None


if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG)
+17 −2

File changed.

Preview size limit exceeded, changes collapsed.

+183 −12

File changed.

Preview size limit exceeded, changes collapsed.