aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Duarte <licorne@google.com>2022-07-28 14:48:45 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-07-28 14:48:45 +0000
commit0fc3c9c39fba60b9b3a7b7aba1d575cc75c8cde6 (patch)
treee0395eeef542f9798738fc338eb3d206ea3984b9
parente1059bc527c96d0f3b79262876d67211be0a7243 (diff)
parentb64f9712e1047406212889659f2b4abe056e6d97 (diff)
downloadmmi2grpc-0fc3c9c39fba60b9b3a7b7aba1d575cc75c8cde6.tar.gz
Merge upstream commit '6029c0575763ef237440a9bb7447a922eecf47fb' into master am: 8b4d56f505 am: 2fe5b84081 am: 440eae6d5f am: 1f7cc85b8e am: b64f9712e1android-platform-13.0.0_r9android-platform-13.0.0_r8android-platform-13.0.0_r7android-platform-13.0.0_r6android-platform-13.0.0_r5android-platform-13.0.0_r4android-platform-13.0.0_r3android-platform-13.0.0_r18android-platform-13.0.0_r17android-platform-13.0.0_r16android-platform-13.0.0_r15android-platform-13.0.0_r14android-platform-13.0.0_r13android-platform-13.0.0_r12android-platform-13.0.0_r11android-platform-13.0.0_r10android-13.0.0_r83android-13.0.0_r82android-13.0.0_r81android-13.0.0_r80android-13.0.0_r79android-13.0.0_r78android-13.0.0_r77android-13.0.0_r76android-13.0.0_r75android-13.0.0_r74android-13.0.0_r73android-13.0.0_r72android-13.0.0_r71android-13.0.0_r70android-13.0.0_r69android-13.0.0_r68android-13.0.0_r67android-13.0.0_r66android-13.0.0_r65android-13.0.0_r64android-13.0.0_r63android-13.0.0_r62android-13.0.0_r61android-13.0.0_r60android-13.0.0_r59android-13.0.0_r58android-13.0.0_r57android-13.0.0_r56android-13.0.0_r55android-13.0.0_r54android-13.0.0_r53android-13.0.0_r52android-13.0.0_r51android-13.0.0_r50android-13.0.0_r49android-13.0.0_r48android-13.0.0_r47android-13.0.0_r46android-13.0.0_r45android-13.0.0_r44android-13.0.0_r43android-13.0.0_r42android-13.0.0_r41android-13.0.0_r40android-13.0.0_r39android-13.0.0_r38android-13.0.0_r37android-13.0.0_r36android-13.0.0_r35android-13.0.0_r34android-13.0.0_r33android-13.0.0_r32android-13.0.0_r30android-13.0.0_r29android-13.0.0_r28android-13.0.0_r27android-13.0.0_r24android-13.0.0_r23android-13.0.0_r22android-13.0.0_r21android-13.0.0_r20android-13.0.0_r19android-13.0.0_r18android-13.0.0_r17android-13.0.0_r16android13-qpr3-s9-releaseandroid13-qpr3-s8-releaseandroid13-qpr3-s7-releaseandroid13-qpr3-s6-releaseandroid13-qpr3-s5-releaseandroid13-qpr3-s4-releaseandroid13-qpr3-s3-releaseandroid13-qpr3-s2-releaseandroid13-qpr3-s14-releaseandroid13-qpr3-s13-releaseandroid13-qpr3-s12-releaseandroid13-qpr3-s11-releaseandroid13-qpr3-s10-releaseandroid13-qpr3-s1-releaseandroid13-qpr3-releaseandroid13-qpr3-c-s8-releaseandroid13-qpr3-c-s7-releaseandroid13-qpr3-c-s6-releaseandroid13-qpr3-c-s5-releaseandroid13-qpr3-c-s4-releaseandroid13-qpr3-c-s3-releaseandroid13-qpr3-c-s2-releaseandroid13-qpr3-c-s12-releaseandroid13-qpr3-c-s11-releaseandroid13-qpr3-c-s10-releaseandroid13-qpr3-c-s1-releaseandroid13-qpr2-s9-releaseandroid13-qpr2-s8-releaseandroid13-qpr2-s7-releaseandroid13-qpr2-s6-releaseandroid13-qpr2-s5-releaseandroid13-qpr2-s3-releaseandroid13-qpr2-s2-releaseandroid13-qpr2-s12-releaseandroid13-qpr2-s11-releaseandroid13-qpr2-s10-releaseandroid13-qpr2-s1-releaseandroid13-qpr2-releaseandroid13-qpr2-b-s1-releaseandroid13-qpr1-s8-releaseandroid13-qpr1-s7-releaseandroid13-qpr1-s6-releaseandroid13-qpr1-s5-releaseandroid13-qpr1-s4-releaseandroid13-qpr1-s3-releaseandroid13-qpr1-s2-releaseandroid13-qpr1-s1-releaseandroid13-qpr1-releaseandroid13-platform-releaseandroid13-d4-s2-releaseandroid13-d4-s1-releaseandroid13-d4-releaseandroid13-d3-s1-releaseandroid13-d2-release
Original change: https://android-review.googlesource.com/c/platform/external/pandora/mmi2grpc/+/2168183 Change-Id: I20fcb7aeac53388ea4ca9a7f28a18b14bb0a818f Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.gitignore1
-rw-r--r--CONTRIBUTING.md30
-rw-r--r--LICENSE202
-rw-r--r--README.md17
-rw-r--r--__init__.py0
-rw-r--r--mmi2grpc/__init__.py118
-rw-r--r--mmi2grpc/_audio.py107
-rw-r--r--mmi2grpc/_helpers.py94
-rw-r--r--mmi2grpc/_proxy.py42
-rw-r--r--mmi2grpc/a2dp.py590
-rw-r--r--pyproject.toml15
11 files changed, 1216 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bee8a64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..97c24f3
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,30 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Style Guide
+
+Every contributions must follow [Google Python style guide](
+https://google.github.io/styleguide/pyguide.html).
+
+## Code Reviews
+
+All submissions, including submissions by project members, require review.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google/conduct/).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c9fec3a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# mmi2grpc
+
+## Install
+
+```bash
+git submodule update --init
+
+pip install [-e] bt-test-interfaces/python
+pip install [-e] .
+```
+
+## Rebuild gRPC Bluetooth test interfaces
+
+```bash
+pip install grpcio-tools==1.46.3
+./bt-test-interfaces/python/_build/grpc.py
+```
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/__init__.py
diff --git a/mmi2grpc/__init__.py b/mmi2grpc/__init__.py
new file mode 100644
index 0000000..f897f46
--- /dev/null
+++ b/mmi2grpc/__init__.py
@@ -0,0 +1,118 @@
+# 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
diff --git a/mmi2grpc/_audio.py b/mmi2grpc/_audio.py
new file mode 100644
index 0000000..8e83c67
--- /dev/null
+++ b/mmi2grpc/_audio.py
@@ -0,0 +1,107 @@
+# 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.
+
+"""Audio tools."""
+
+import itertools
+import math
+import os
+from threading import Thread
+
+import numpy as np
+from scipy.io import wavfile
+
+SINE_FREQUENCY = 440
+SINE_DURATION = 0.1
+
+# File which stores the audio signal output data (after transport).
+# Used for running comparisons with the generated audio signal.
+OUTPUT_WAV_FILE = '/tmp/audiodata'
+
+WAV_RIFF_SIZE_OFFSET = 4
+WAV_DATA_SIZE_OFFSET = 40
+
+
+def _fixup_wav_header(path):
+ with open(path, 'r+b') as f:
+ f.seek(0, os.SEEK_END)
+ file_size = f.tell()
+ for offset in [WAV_RIFF_SIZE_OFFSET, WAV_DATA_SIZE_OFFSET]:
+ size = file_size - offset - 4
+ f.seek(offset)
+ f.write(size.to_bytes(4, byteorder='little'))
+
+
+class AudioSignal:
+ """Audio signal generator and verifier."""
+
+ def __init__(self, transport, amplitude, fs):
+ """Init AudioSignal class.
+
+ Args:
+ transport: function to send the generated audio data to.
+ amplitude: amplitude of the signal to generate.
+ fs: sampling rate of the signal to generate.
+ """
+ self.transport = transport
+ self.amplitude = amplitude
+ self.fs = fs
+ self.thread = None
+
+ def start(self):
+ """Generates the audio signal and send it to the transport."""
+ self.thread = Thread(target=self._run)
+ self.thread.start()
+
+ def _run(self):
+ sine = self._generate_sine(SINE_FREQUENCY, SINE_DURATION)
+
+ # Interleaved audio.
+ stereo = np.zeros(sine.size * 2, dtype=sine.dtype)
+ stereo[0::2] = sine
+
+ # Send 4 second of audio.
+ audio = itertools.repeat(stereo.tobytes(), int(4 / SINE_DURATION))
+
+ self.transport(audio)
+
+ def _generate_sine(self, f, duration):
+ sine = self.amplitude * \
+ np.sin(2 * np.pi * np.arange(self.fs * duration) * (f / self.fs))
+ s16le = (sine * 32767).astype('<i2')
+ return s16le
+
+ def verify(self):
+ """Verifies that the audio signal is correctly output."""
+ assert self.thread is not None
+ self.thread.join()
+ self.thread = None
+
+ _fixup_wav_header(OUTPUT_WAV_FILE)
+
+ samplerate, data = wavfile.read(OUTPUT_WAV_FILE)
+ # Take one second of audio after the first second.
+ audio = data[samplerate:samplerate*2, 0].astype(np.float) / 32767
+ assert len(audio) == samplerate
+
+ spectrum = np.abs(np.fft.fft(audio))
+ frequency = np.fft.fftfreq(samplerate, d=1/samplerate)
+ amplitudes = spectrum / (samplerate/2)
+ index = np.where(frequency == SINE_FREQUENCY)
+ amplitude = amplitudes[index][0]
+
+ match_amplitude = math.isclose(
+ amplitude, self.amplitude, rel_tol=1e-03)
+
+ return match_amplitude
diff --git a/mmi2grpc/_helpers.py b/mmi2grpc/_helpers.py
new file mode 100644
index 0000000..4e34d59
--- /dev/null
+++ b/mmi2grpc/_helpers.py
@@ -0,0 +1,94 @@
+# 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.
+
+"""Helper functions.
+
+Facilitates the implementation of a new profile proxy or a PTS MMI.
+"""
+
+import functools
+import textwrap
+import unittest
+
+DOCSTRING_WIDTH = 80 - 8 # 80 cols - 8 indentation spaces
+
+
+def assert_description(f):
+ """Decorator which verifies the description of a PTS MMI implementation.
+
+ Asserts that the docstring of a function implementing a PTS MMI is the same
+ as the corresponding official MMI description.
+
+ Args:
+ f: function implementing a PTS MMI.
+
+ Raises:
+ AssertionError: the docstring of the function does not match the MMI
+ description.
+ """
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+ description = textwrap.fill(
+ kwargs['description'], DOCSTRING_WIDTH, replace_whitespace=False)
+ docstring = textwrap.dedent(f.__doc__ or '')
+
+ if docstring.strip() != description.strip():
+ print(f'Expected description of {f.__name__}:')
+ print(description)
+
+ # Generate AssertionError.
+ test = unittest.TestCase()
+ test.maxDiff = None
+ test.assertMultiLineEqual(
+ docstring.strip(),
+ description.strip(),
+ f'description does not match with function docstring of'
+ f'{f.__name__}')
+
+ return f(*args, **kwargs)
+ return wrapper
+
+
+def format_function(mmi_name, mmi_description):
+ """Returns the base format of a function implementing a PTS MMI."""
+ wrapped_description = textwrap.fill(
+ mmi_description, DOCSTRING_WIDTH, replace_whitespace=False)
+ return (
+ f'@assert_description\n'
+ f'def {mmi_name}(self, **kwargs):\n'
+ f' """\n'
+ f'{textwrap.indent(wrapped_description, " ")}\n'
+ f' """\n'
+ f'\n'
+ f' return "OK"\n')
+
+
+def format_proxy(profile, mmi_name, mmi_description):
+ """Returns the base format of a profile proxy including a given MMI."""
+ wrapped_function = textwrap.indent(
+ format_function(mmi_name, mmi_description), ' ')
+ return (
+ f'from mmi2grpc._helpers import assert_description\n'
+ f'from mmi2grpc._proxy import ProfileProxy\n'
+ f'\n'
+ f'from pandora.{profile.lower()}_grpc import {profile}\n'
+ f'\n'
+ f'\n'
+ f'class {profile}Proxy(ProfileProxy):\n'
+ f'\n'
+ f' def __init__(self, channel):\n'
+ f' super().__init__()\n'
+ f' self.{profile.lower()} = {profile}(channel)\n'
+ f'\n'
+ f'{wrapped_function}')
diff --git a/mmi2grpc/_proxy.py b/mmi2grpc/_proxy.py
new file mode 100644
index 0000000..8eb4bd8
--- /dev/null
+++ b/mmi2grpc/_proxy.py
@@ -0,0 +1,42 @@
+# 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.
+
+"""Profile proxy base module."""
+
+from mmi2grpc._helpers import format_function
+
+
+class ProfileProxy:
+ """Profile proxy base class."""
+
+ def interact(
+ self, test: str, mmi_name: str, mmi_description: str,
+ pts_addr: bytes):
+ """Translate a MMI call to its corresponding implementation.
+
+ Args:
+ test: PTS test id.
+ mmi_name: MMI name.
+ mmi_description: MMI description.
+ pts_addr: Bluetooth MAC address of the PTS in bytes.
+
+ Raises:
+ AttributeError: the MMI is not implemented.
+ """
+ try:
+ return getattr(self, mmi_name)(
+ test=test, description=mmi_description, pts_addr=pts_addr)
+ except AttributeError:
+ code = format_function(mmi_name, mmi_description)
+ assert False, f'Unhandled mmi {id}\n{code}'
diff --git a/mmi2grpc/a2dp.py b/mmi2grpc/a2dp.py
new file mode 100644
index 0000000..856e45d
--- /dev/null
+++ b/mmi2grpc/a2dp.py
@@ -0,0 +1,590 @@
+# 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.
+
+"""A2DP proxy module."""
+
+import time
+from typing import Optional
+
+from grpc import RpcError
+
+from mmi2grpc._audio import AudioSignal
+from mmi2grpc._helpers import assert_description
+from mmi2grpc._proxy import ProfileProxy
+from pandora.a2dp_grpc import A2DP
+from pandora.a2dp_pb2 import Sink, Source, PlaybackAudioRequest
+from pandora.host_grpc import Host
+from pandora.host_pb2 import Connection
+
+AUDIO_SIGNAL_AMPLITUDE = 0.8
+AUDIO_SIGNAL_SAMPLING_RATE = 44100
+
+
+class A2DPProxy(ProfileProxy):
+ """A2DP proxy.
+
+ Implements A2DP and AVDTP PTS MMIs.
+ """
+
+ connection: Optional[Connection] = None
+ sink: Optional[Sink] = None
+ source: Optional[Source] = None
+
+ def __init__(self, channel):
+ super().__init__()
+
+ self.host = Host(channel)
+ self.a2dp = A2DP(channel)
+
+ def convert_frame(data):
+ return PlaybackAudioRequest(data=data, source=self.source)
+ self.audio = AudioSignal(
+ lambda frames: self.a2dp.PlaybackAudio(map(convert_frame, frames)),
+ AUDIO_SIGNAL_AMPLITUDE,
+ AUDIO_SIGNAL_SAMPLING_RATE
+ )
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_connect(
+ self, test: str, pts_addr: bytes, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Signaling Channel
+ Connection initiated by the tester.
+
+ Description: Make sure the IUT
+ (Implementation Under Test) is in a state to accept incoming Bluetooth
+ connections. Some devices may need to be on a specific screen, like a
+ Bluetooth settings screen, in order to pair with PTS. If the IUT is
+ still having problems pairing with PTS, try running a test case where
+ the IUT connects to PTS to establish pairing.
+ """
+
+ if "SRC" in test:
+ self.connection = self.host.WaitConnection(
+ address=pts_addr).connection
+ try:
+ if "INT" in test:
+ self.source = self.a2dp.OpenSource(
+ connection=self.connection).source
+ else:
+ self.source = self.a2dp.WaitSource(
+ connection=self.connection).source
+ except RpcError:
+ pass
+ else:
+ self.connection = self.host.WaitConnection(
+ address=pts_addr).connection
+ try:
+ self.sink = self.a2dp.WaitSink(
+ connection=self.connection).sink
+ except RpcError:
+ pass
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_discover(self, **kwargs):
+ """
+ Send a discover command to PTS.
+
+ Action: If the IUT (Implementation
+ Under Test) is already connected to PTS, attempting to send or receive
+ streaming media should trigger this action. If the IUT is not connected
+ to PTS, attempting to connect may trigger this action.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_start(self, test: str, **kwargs):
+ """
+ Send a start command to PTS.
+
+ Action: If the IUT (Implementation Under
+ Test) is already connected to PTS, attempting to send or receive
+ streaming media should trigger this action. If the IUT is not connected
+ to PTS, attempting to connect may trigger this action.
+ """
+
+ if "SRC" in test:
+ self.a2dp.Start(source=self.source)
+ else:
+ self.a2dp.Start(sink=self.sink)
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_suspend(self, test: str, **kwargs):
+ """
+ Suspend the streaming channel.
+ """
+
+ if "SRC" in test:
+ self.a2dp.Suspend(source=self.source)
+ else:
+ assert False
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_close_stream(self, test: str, **kwargs):
+ """
+ Close the streaming channel.
+
+ Action: Disconnect the streaming channel,
+ or close the Bluetooth connection to the PTS.
+ """
+
+ if "SRC" in test:
+ self.a2dp.Close(source=self.source)
+ self.source = None
+ else:
+ self.a2dp.Close(sink=self.sink)
+ self.sink = None
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_out_of_range(
+ self, pts_addr: bytes, **kwargs):
+ """
+ Move the IUT out of range to create a link loss scenario.
+
+ Action: This
+ can be also be done by placing the IUT or PTS in an RF shielded box.
+ """
+
+ if self.connection is None:
+ self.connection = self.host.GetConnection(
+ address=pts_addr).connection
+ self.host.Disconnect(connection=self.connection)
+ self.connection = None
+ self.sink = None
+ self.source = None
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_begin_streaming(self, test: str, **kwargs):
+ """
+ Begin streaming media ...
+
+ Note: If the IUT has suspended the stream
+ please restart the stream to begin streaming media.
+ """
+
+ if test == "AVDTP/SRC/ACP/SIG/SMG/BI-29-C":
+ time.sleep(2) # TODO: Remove, AVRCP SegFault
+ if test in ("A2DP/SRC/CC/BV-09-I",
+ "A2DP/SRC/SET/BV-04-I",
+ "AVDTP/SRC/ACP/SIG/SMG/BV-18-C",
+ "AVDTP/SRC/ACP/SIG/SMG/BV-20-C",
+ "AVDTP/SRC/ACP/SIG/SMG/BV-22-C"):
+ time.sleep(1) # TODO: Remove, AVRCP SegFault
+ if test == "A2DP/SRC/SUS/BV-01-I":
+ # Stream is not suspended when we receive the interaction
+ time.sleep(1)
+
+ self.a2dp.Start(source=self.source)
+ self.audio.start()
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_media(self, **kwargs):
+ """
+ Take action if necessary to start streaming media to the tester.
+ """
+
+ self.audio.start()
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_stream_media(self, **kwargs):
+ """
+ Stream media to PTS. If the IUT is a SNK, wait for PTS to start
+ streaming media.
+
+ Action: If the IUT (Implementation Under Test) is
+ already connected to PTS, attempting to send or receive streaming media
+ should trigger this action. If the IUT is not connected to PTS,
+ attempting to connect may trigger this action.
+ """
+
+ self.audio.start()
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_user_verify_media_playback(self, **kwargs):
+ """
+ Is the test system properly playing back the media being sent by the
+ IUT?
+ """
+
+ result = self.audio.verify()
+ assert result
+
+ return "Yes" if result else "No"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_get_capabilities(self, **kwargs):
+ """
+ Send a get capabilities command to PTS.
+
+ Action: If the IUT
+ (Implementation Under Test) is already connected to PTS, attempting to
+ send or receive streaming media should trigger this action. If the IUT
+ is not connected to PTS, attempting to connect may trigger this action.
+ """
+
+ # This will be done as part as the a2dp.OpenSource or a2dp.WaitSource
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_discover(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Discover operation
+ initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_set_configuration(self, **kwargs):
+ """
+ Send a set configuration command to PTS.
+
+ Action: If the IUT
+ (Implementation Under Test) is already connected to PTS, attempting to
+ send or receive streaming media should trigger this action. If the IUT
+ is not connected to PTS, attempting to connect may trigger this action.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_close_stream(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Close operation initiated
+ by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_abort(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Abort operation initiated
+ by the tester..
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_get_all_capabilities(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Get All Capabilities
+ operation initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_get_capabilities(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Get Capabilities operation
+ initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_set_configuration(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Set Configuration
+ operation initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_get_configuration(self, **kwargs):
+ """
+ Take action to accept the AVDTP Get Configuration command from the
+ tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_open_stream(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Open operation initiated
+ by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_start(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Start operation initiated
+ by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_suspend(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Suspend operation
+ initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_reconfigure(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Reconfigure operation
+ initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_media_transports(self, **kwargs):
+ """
+ Take action to accept transport channels for the recently configured
+ media stream.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_confirm_streaming(self, **kwargs):
+ """
+ Is the IUT (Implementation Under Test) receiving streaming media from
+ PTS?
+
+ Action: Press 'Yes' if the IUT is receiving streaming data from
+ the PTS (in some cases the sound may not be clear, this is normal).
+ """
+
+ # TODO: verify
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_open_stream(self, **kwargs):
+ """
+ Open a streaming media channel.
+
+ Action: If the IUT (Implementation
+ Under Test) is already connected to PTS, attempting to send or receive
+ streaming media should trigger this action. If the IUT is not connected
+ to PTS, attempting to connect may trigger this action.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_reconnect(self, pts_addr: bytes, **kwargs):
+ """
+ Press OK when the IUT (Implementation Under Test) is ready to allow the
+ PTS to reconnect the AVDTP signaling channel.
+
+ Action: Press OK when the
+ IUT is ready to accept Bluetooth connections again.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_get_all_capabilities(self, **kwargs):
+ """
+ Send a GET ALL CAPABILITIES command to PTS.
+
+ Action: If the IUT
+ (Implementation Under Test) is already connected to PTS, attempting to
+ send or receive streaming media should trigger this action. If the IUT
+ is not connected to PTS, attempting to connect may trigger this action.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_tester_verifying_suspend(self, **kwargs):
+ """
+ Please wait while the tester verifies the IUT does not send media during
+ suspend ...
+ """
+
+ return "Yes"
+
+ @assert_description
+ def TSC_A2DP_mmi_user_confirm_optional_data_attribute(self, **kwargs):
+ """
+ Tester found the optional SDP attribute named 'Supported Features'.
+ Press 'Yes' if the data displayed below is correct.
+
+ Value: 0x0001
+ """
+
+ # TODO: Extract and verify attribute name and value from description
+ return "OK"
+
+ @assert_description
+ def TSC_A2DP_mmi_user_confirm_optional_string_attribute(self, **kwargs):
+ """
+ Tester found the optional SDP attribute named 'Service Name'. Press
+ 'Yes' if the string displayed below is correct.
+
+ Value: Advanced Audio
+ Source
+ """
+
+ # TODO: Extract and verify attribute name and value from description
+ return "OK"
+
+ @assert_description
+ def TSC_A2DP_mmi_user_confirm_no_optional_attribute_support(self, **kwargs):
+ """
+ Tester could not find the optional SDP attribute named 'Provider Name'.
+ Is this correct?
+ """
+
+ # TODO: Extract and verify attribute name from description
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_accept_delayreport(self, **kwargs):
+ """
+ Take action if necessary to accept the Delay Reportl command from the
+ tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_initiate_media_transport_connect(self, **kwargs):
+ """
+ Take action to initiate an AVDTP media transport.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_user_confirm_SIG_SMG_BV_28_C(self, **kwargs):
+ """
+ Were all the service capabilities reported to the upper tester valid?
+ """
+
+ # TODO: verify
+ return "Yes"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_invalid_command(self, **kwargs):
+ """
+ Take action to reject the invalid command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_open(self, **kwargs):
+ """
+ Take action to reject the invalid OPEN command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_start(self, **kwargs):
+ """
+ Take action to reject the invalid START command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_suspend(self, **kwargs):
+ """
+ Take action to reject the invalid SUSPEND command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_reconfigure(self, **kwargs):
+ """
+ Take action to reject the invalid or incompatible RECONFIGURE command
+ sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_get_all_capabilities(self, **kwargs):
+ """
+ Take action to reject the invalid GET ALL CAPABILITIES command with the
+ error code BAD_LENGTH.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_get_capabilities(self, **kwargs):
+ """
+ Take action to reject the invalid GET CAPABILITIES command with the
+ error code BAD_LENGTH.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_set_configuration(self, **kwargs):
+ """
+ Take action to reject the SET CONFIGURATION sent by the tester. The IUT
+ is expected to respond with SEP_IN_USE because the SEP requested was
+ previously configured.
+ """
+
+ return "OK"
+
+ def TSC_AVDTPEX_mmi_iut_reject_get_configuration(self, **kwargs):
+ """
+ Take action to reject the GET CONFIGURATION sent by the tester. The IUT
+ is expected to respond with BAD_ACP_SEID because the SEID requested was
+ not previously configured.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_close(self, **kwargs):
+ """
+ Take action to reject the invalid CLOSE command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_user_confirm_SIG_SMG_BV_18_C(self, **kwargs):
+ """
+ Did the IUT receive media with the following information?
+
+ - V = RTP_Ver
+ - P = 0 (no padding bits)
+ - X = 0 (no extension)
+ - CC = 0 (no
+ contributing source)
+ - M = 0
+ """
+
+ # TODO: verify
+ return "OK"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..de54399
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,15 @@
+[project]
+name = "mmi2grpc"
+authors = [{name = "Pandora", email = "pandora-core@google.com"}]
+readme = "README.md"
+dynamic = ["version", "description"]
+dependencies = [
+ "bt-test-interfaces",
+ "grpcio>=1.41",
+ "numpy>=1.22",
+ "scipy>=1.8"
+]
+
+[build-system]
+requires = ["flit_core==3.7.1"]
+build-backend = "flit_core.buildapi"