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

Commit c0daea82 authored by Charlie Boutier's avatar Charlie Boutier Committed by Gerrit Code Review
Browse files

Merge "Pandora: Make asha tests into experimental"

parents 253f782e ee0f3641
Loading
Loading
Loading
Loading
+12 −3
Original line number Diff line number Diff line
@@ -18,15 +18,17 @@ import enum
import grpc
import logging

from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices, asynchronous
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 mobly import base_test, signals, test_runner
from mobly.asserts import assert_equal  # type: ignore
from mobly.asserts import assert_in  # 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_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()
@@ -54,6 +56,11 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]
    ref_right: PandoraDevice

    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)
        )

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

@@ -84,7 +91,8 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]
        :return: Ref device's advertise stream
        """
        # Ref starts advertising with ASHA service data
        await ref_device.aio.asha.Register(capability=CAPABILITY, hisyncid=HISYCNID)
        asha = AioAsha(ref_device.aio.channel)
        await asha.Register(capability=CAPABILITY, hisyncid=HISYCNID)
        return ref_device.aio.host.Advertise(
            legacy=True,
            connectable=True,
@@ -171,7 +179,8 @@ class ASHATest(base_test.BaseTestClass): # type: ignore[misc]
        protocol_version = 0x01
        truncated_hisyncid = HISYCNID[:4]

        await self.ref_left.aio.asha.Register(capability=CAPABILITY, hisyncid=HISYCNID)
        asha = AioAsha(self.ref_left.aio.channel)
        await asha.Register(capability=CAPABILITY, hisyncid=HISYCNID)

        # advertise with ASHA service data in scan response
        advertisement = self.ref_left.aio.host.Advertise(
+87 −0
Original line number Diff line number Diff line
// Copyright 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.

syntax = "proto3";

option java_outer_classname = "AshaProto";

package pandora.asha;

import "google/protobuf/empty.proto";
import "pandora/host.proto";

// Service to trigger Audio Streaming for Hearing Aid (ASHA) procedures.
// ASHA uses connection-oriented L2CAP channels (CoC) and GATT.
service Asha {
  // Register ASHA Service.
  rpc Register(RegisterRequest) returns (google.protobuf.Empty);
  // Capture Audio.
  rpc CaptureAudio(CaptureAudioRequest) returns (stream CaptureAudioResponse);
  // Start a suspended stream.
  rpc Start(StartRequest) returns (StartResponse);
  // Playback audio
  rpc PlaybackAudio(stream PlaybackAudioRequest) returns (PlaybackAudioResponse);
  // Stop a started stream.
  rpc Stop(stream StopRequest) returns (StopResponse);
}

// Request of the `Register` method.
message RegisterRequest {
  uint32 capability = 1; // left or right device, monaural or binaural device.
  repeated uint32 hisyncid = 2; // id identifying two hearing aids as one pair.
}

// Request of the `CaptureAudio` method.
message CaptureAudioRequest {
  // Low Energy connection.
  Connection connection = 1;
}

// Response of the `CaptureAudio` method.
message CaptureAudioResponse {
  // Audio data received on peripheral side.
  // `data` is decoded by G722 decoder.
  bytes data = 1;
}

// Request of the `Start` method.
message StartRequest {
  // Low Energy connection.
  Connection connection = 1;
}

// Response of the `Start` method.
message StartResponse {}

// Request of the `PlaybackAudio` method.
message PlaybackAudioRequest {
  // Low Energy connection.
  Connection connection = 1;
  // Audio data to playback.
  // `data` should be interleaved stereo frames with 16-bit signed little-endian
  // linear PCM samples at 44100Hz sample rate
  bytes data = 2;
}

// Response of the `PlaybackAudio` method.
message PlaybackAudioResponse {}

// Request of the `Stop` method.
message StopRequest {
  // Low Energy connection.
  Connection connection = 1;
}

// Response of the `Stop` method.
message StopResponse {}
 No newline at end of file
+5 −0
Original line number Diff line number Diff line
@@ -34,6 +34,10 @@ genrule {
        "pandora_experimental/a2dp_grpc_aio.py",
        "pandora_experimental/a2dp_pb2.py",
        "pandora_experimental/a2dp_pb2.pyi",
        "pandora_experimental/asha_grpc.py",
        "pandora_experimental/asha_grpc_aio.py",
        "pandora_experimental/asha_pb2.py",
        "pandora_experimental/asha_pb2.pyi",
        "pandora_experimental/avrcp_grpc.py",
        "pandora_experimental/avrcp_grpc_aio.py",
        "pandora_experimental/avrcp_pb2.py",
@@ -89,6 +93,7 @@ filegroup {
        ":pandora_experimental-python-gen-src{pandora_experimental/py.typed}",
        ":pandora_experimental-python-gen-src{pandora_experimental/_android_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/a2dp_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/asha_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/avrcp_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/gatt_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/hfp_pb2.pyi}",
+3 −0
Original line number Diff line number Diff line
@@ -26,4 +26,7 @@ python_library_host {
        "pandora_experimental-python",
        "bumble",
    ],
    data: [
        "bumble_experimental/py.typed",
    ]
}
+67 −0
Original line number Diff line number Diff line
# Copyright 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.

import asyncio
import grpc
import logging

from avatar.bumble_server import utils
from bumble.device import Connection as BumbleConnection, Device
from bumble.profiles.asha_service import AshaService
from google.protobuf.empty_pb2 import Empty  # pytype: disable=pyi-error
from pandora_experimental.asha_grpc_aio import AshaServicer
from pandora_experimental.asha_pb2 import CaptureAudioRequest, CaptureAudioResponse, RegisterRequest
from typing import AsyncGenerator, Optional


class ASHAService(AshaServicer):
    device: Device
    asha_service: Optional[AshaService]

    def __init__(self, device: Device) -> None:
        self.log = utils.BumbleServerLoggerAdapter(logging.getLogger(), {'service_name': 'Asha', 'device': device})
        self.device = device
        self.asha_service = None

    @utils.rpc
    async def Register(self, request: RegisterRequest, context: grpc.ServicerContext) -> Empty:
        logging.info('Register')
        # asha service from bumble profile
        self.asha_service = AshaService(request.capability, request.hisyncid, self.device)
        self.device.add_service(self.asha_service)  # type: ignore[no-untyped-call]
        return Empty()

    @utils.rpc
    async def CaptureAudio(
        self, request: CaptureAudioRequest, context: grpc.ServicerContext
    ) -> AsyncGenerator[CaptureAudioResponse, None]:
        connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
        logging.info(f'CaptureAudioData connection_handle:{connection_handle}')

        if not (connection := self.device.lookup_connection(connection_handle)):
            raise RuntimeError(f"Unknown connection for connection_handle:{connection_handle}")

        queue: asyncio.Queue[bytes] = asyncio.Queue()

        def on_data(asha_connection: BumbleConnection, data: bytes) -> None:
            if asha_connection == connection:
                queue.put_nowait(data)

        self.asha_service.on('data', on_data)  # type: ignore

        try:
            while data := await queue.get():
                yield CaptureAudioResponse(data=data)
        finally:
            self.asha_service.remove_listener('data', on_data)  # type: ignore