# 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. """Map Bluetooth PTS Man Machine Interface to Pandora gRPC calls.""" __version__ = "0.0.1" from typing import List import time import sys import grpc from mmi2grpc.a2dp import A2DPProxy from mmi2grpc._helpers import format_proxy from pandora.host_grpc import Host GRPC_PORT = 8999 MAX_RETRIES = 10 class IUT: """IUT class. Handles MMI calls from the PTS and routes them to corresponding profile proxy which translates MMI calls to gRPC calls to the IUT. """ def __init__( self, test: str, args: List[str], port: int = GRPC_PORT, **kwargs): """Init IUT class for a given test. Args: test: PTS test id. args: test arguments. port: gRPC port exposed by the IUT test server. """ self.port = port self.test = test # Profile proxies. self._a2dp = None def __enter__(self): """Resets the IUT when starting a PTS test.""" # Note: we don't keep a single gRPC channel instance in the IUT class # because reset is allowed to close the gRPC server. with grpc.insecure_channel(f'localhost:{self.port}') as channel: Host(channel).Reset(wait_for_ready=True) def __exit__(self, exc_type, exc_value, exc_traceback): self._a2dp = None @property def address(self) -> bytes: """Bluetooth MAC address of the IUT.""" with grpc.insecure_channel(f'localhost:{self.port}') as channel: tries = 0 while True: try: return Host(channel).ReadLocalAddress( wait_for_ready=True).address except grpc.RpcError or grpc._channel._InactiveRpcError: tries += 1 if tries >= MAX_RETRIES: raise else: print('Retry', tries, 'of', MAX_RETRIES) time.sleep(1) def interact(self, pts_address: bytes, profile: str, test: str, interaction: str, description: str, style: str, **kwargs) -> str: """Routes MMI calls to corresponding profile proxy. Args: pts_address: Bluetooth MAC addres of the PTS in bytes. profile: Bluetooth profile. test: PTS test id. interaction: MMI name. description: MMI description. style: MMI popup style, unused for now. """ print(f'{profile} mmi: {interaction}', file=sys.stderr) # Handles A2DP and AVDTP MMIs. if profile in ('A2DP', 'AVDTP'): if not self._a2dp: self._a2dp = A2DPProxy( grpc.insecure_channel(f'localhost:{self.port}')) return self._a2dp.interact( test, interaction, description, pts_address) # Handles unsupported profiles. code = format_proxy(profile, interaction, description) error_msg = ( f'Missing {profile} proxy and mmi: {interaction}\n' f'Create a {profile.lower()}.py in mmi2grpc/:\n\n{code}\n' f'Then, instantiate the corresponding proxy in __init__.py\n' f'Finally, create a {profile.lower()}.proto in proto/pandora/' f'and generate the corresponding interface.') assert False, error_msg