diff options
author | Thomas Girardier <girardier@google.com> | 2022-04-26 16:23:35 -0700 |
---|---|---|
committer | Thomas Girardier <girardier@google.com> | 2022-04-27 08:58:21 -0700 |
commit | 4641dc0df37d0edc9a30fadeaa51c29764212362 (patch) | |
tree | 8601c98235b237cb92a3b31ce1b9a915dd1524c2 | |
parent | 62dec0f0e53869a011b1064df92b29965f04782b (diff) | |
download | mmi2grpc-4641dc0df37d0edc9a30fadeaa51c29764212362.tar.gz |
mmi2grpc: prepares for open-source
- Adds license file and headers.
- Adds contributing guidelines.
- Corrects linter errors.
- Updates bt-test-interfaces submodule.
Change-Id: I4ec64b0572197046bc966fe239ec5b91b643bf39
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | CONTRIBUTING.md | 30 | ||||
-rw-r--r-- | LICENSE | 202 | ||||
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | mmi2grpc/__init__.py | 79 | ||||
-rw-r--r-- | mmi2grpc/_audio.py | 60 | ||||
-rw-r--r-- | mmi2grpc/_description.py | 60 | ||||
-rw-r--r-- | mmi2grpc/_helpers.py | 94 | ||||
-rw-r--r-- | mmi2grpc/_proxy.py | 40 | ||||
-rw-r--r-- | mmi2grpc/a2dp.py | 48 | ||||
m--------- | proto | 0 | ||||
-rwxr-xr-x | protoc-gen-custom_grpc | 27 | ||||
-rwxr-xr-x | setup.py | 22 |
13 files changed, 553 insertions, 117 deletions
@@ -1,4 +1,3 @@ -out/ build __pycache__ pandora/* 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/). @@ -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. @@ -1,8 +1,13 @@ # mmi2grpc +## Install + +```bash +pip3 install -r requirements.txt +``` + ## Build grpc interfaces ```bash ./setup.py build_grpc ``` - diff --git a/mmi2grpc/__init__.py b/mmi2grpc/__init__.py index e127348..a2b6530 100644 --- a/mmi2grpc/__init__.py +++ b/mmi2grpc/__init__.py @@ -1,39 +1,73 @@ +# 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. + +"""Entry point to mmi2grpc.""" + from typing import List -import grpc import time import sys -from pandora.host_grpc import Host +import grpc -from .a2dp import A2DPProxy -from ._description import format_proxy +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): - self.a2dp_ = None - self.address_ = None + """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): - self.a2dp_ = None + 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 + return Host(channel).ReadLocalAddress( + wait_for_ready=True).address except grpc.RpcError: if tries >= MAX_RETRIES: raise @@ -49,20 +83,33 @@ class IUT: description: str, style: str, **kwargs) -> str: - print(f'{profile} mmi: {interaction}', file=sys.stderr) + """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: {description}', file=sys.stderr) + + # Handles A2DP and AVDTP MMIs. if profile in ('A2DP', 'AVDTP'): - if not self.a2dp_: - self.a2dp_ = A2DPProxy( + if not self._a2dp: + self._a2dp = A2DPProxy( grpc.insecure_channel(f'localhost:{self.port}')) - return self.a2dp_.interact( - interaction, test, description, pts_address) + 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.' - ) + f'and generate the corresponding interface.') + assert False, error_msg diff --git a/mmi2grpc/_audio.py b/mmi2grpc/_audio.py index 92e06df..8e83c67 100644 --- a/mmi2grpc/_audio.py +++ b/mmi2grpc/_audio.py @@ -1,3 +1,19 @@ +# 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 @@ -6,11 +22,18 @@ from threading import Thread import numpy as np from scipy.io import wavfile +SINE_FREQUENCY = 440 +SINE_DURATION = 0.1 -def _fixup_wav_header(path): - WAV_RIFF_SIZE_OFFSET = 4 - WAV_DATA_SIZE_OFFSET = 40 +# 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() @@ -20,31 +43,35 @@ def _fixup_wav_header(path): f.write(size.to_bytes(4, byteorder='little')) -SINE_FREQUENCY = 440 -SINE_DURATION = 0.1 - -WAV_FILE = "/tmp/audiodata" - - 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 + # Interleaved audio. stereo = np.zeros(sine.size * 2, dtype=sine.dtype) stereo[0::2] = sine - # Send 4 second of audio + # Send 4 second of audio. audio = itertools.repeat(stereo.tobytes(), int(4 / SINE_DURATION)) self.transport(audio) @@ -52,20 +79,21 @@ class AudioSignal: 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") + 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(WAV_FILE) + _fixup_wav_header(OUTPUT_WAV_FILE) - samplerate, data = wavfile.read(WAV_FILE) - # Take one second of audio after the first second + 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) + assert len(audio) == samplerate spectrum = np.abs(np.fft.fft(audio)) frequency = np.fft.fftfreq(samplerate, d=1/samplerate) diff --git a/mmi2grpc/_description.py b/mmi2grpc/_description.py deleted file mode 100644 index c292720..0000000 --- a/mmi2grpc/_description.py +++ /dev/null @@ -1,60 +0,0 @@ -import functools -import unittest -import textwrap - -COMMENT_WIDTH = 80 - 8 # 80 cols - 8 indentation space - - -def assert_description(f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - description = textwrap.fill( - kwargs["description"], COMMENT_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.__name__}') - - return f(*args, **kwargs) - return wrapper - - -def format_function(id, description): - wrapped = textwrap.fill( - description, COMMENT_WIDTH, replace_whitespace=False) - return ( - f'@assert_description\n' - f'def {id}(self, **kwargs):\n' - f' """\n' - f'{textwrap.indent(wrapped, " ")}\n' - f' """\n' - f'\n' - f' return "OK"\n' - ) - - -def format_proxy(profile, id, description): - return ( - f'from ._description import assert_description\n' - f'from ._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'{textwrap.indent(format_function(id, description), " ")}' - ) 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 index f5065a3..8eb4bd8 100644 --- a/mmi2grpc/_proxy.py +++ b/mmi2grpc/_proxy.py @@ -1,12 +1,42 @@ -from mmi2grpc._description import format_function +# 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. - def interact(self, id: str, test: str, description: str, pts_addr: bytes): + Raises: + AttributeError: the MMI is not implemented. + """ try: - return getattr(self, id)( - test=test, description=description, pts_addr=pts_addr) + return getattr(self, mmi_name)( + test=test, description=mmi_description, pts_addr=pts_addr) except AttributeError: - code = format_function(id, description) + 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 index 7e51c68..856e45d 100644 --- a/mmi2grpc/a2dp.py +++ b/mmi2grpc/a2dp.py @@ -1,20 +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. + +"""A2DP proxy module.""" + import time from typing import Optional -from pandora.a2dp_grpc import A2DP -from pandora.host_grpc import Host +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 -from ._audio import AudioSignal -from ._description import assert_description -from ._proxy import ProfileProxy - -AUDIO_AMPLITUDE = 0.8 +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 @@ -25,12 +47,12 @@ class A2DPProxy(ProfileProxy): self.host = Host(channel) self.a2dp = A2DP(channel) - def convert_frame(data): return PlaybackAudioRequest( - data=data, source=self.source) + def convert_frame(data): + return PlaybackAudioRequest(data=data, source=self.source) self.audio = AudioSignal( lambda frames: self.a2dp.PlaybackAudio(map(convert_frame, frames)), - AUDIO_AMPLITUDE, - 44100 + AUDIO_SIGNAL_AMPLITUDE, + AUDIO_SIGNAL_SAMPLING_RATE ) @assert_description @@ -58,7 +80,7 @@ class A2DPProxy(ProfileProxy): else: self.source = self.a2dp.WaitSource( connection=self.connection).source - except: + except RpcError: pass else: self.connection = self.host.WaitConnection( @@ -66,7 +88,7 @@ class A2DPProxy(ProfileProxy): try: self.sink = self.a2dp.WaitSink( connection=self.connection).sink - except: + except RpcError: pass return "OK" diff --git a/proto b/proto -Subproject 05cd39cf98b3fdc14699316c69cdaf8e496ee06 +Subproject 413cc97ae077373e45b6d3f057f6113e16d03c2 diff --git a/protoc-gen-custom_grpc b/protoc-gen-custom_grpc index eb5ec11..00e47a5 100755 --- a/protoc-gen-custom_grpc +++ b/protoc-gen-custom_grpc @@ -1,7 +1,25 @@ #!/usr/bin/env python3 + +# 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. + +"""Custom mmi2grpc gRPC compiler.""" + import sys -from google.protobuf.compiler.plugin_pb2 import CodeGeneratorRequest, CodeGeneratorResponse +from google.protobuf.compiler.plugin_pb2 import CodeGeneratorRequest, \ + CodeGeneratorResponse def eprint(*args, **kwargs): @@ -10,14 +28,17 @@ def eprint(*args, **kwargs): request = CodeGeneratorRequest.FromString(sys.stdin.buffer.read()) + def has_type(proto_file, type_name): return any(filter(lambda x: x.name == type_name, proto_file.message_type)) + def import_type(imports, type): - # eprint(f'type: {type} request: {request.proto_file}') package = type[1:type.rindex('.')] type_name = type[type.rindex('.')+1:] - file = next(filter(lambda x: x.package == package and has_type(x, type_name), request.proto_file)) + file = next(filter( + lambda x: x.package == package and has_type(x, type_name), + request.proto_file)) python_path = file.name.replace('.proto', '').replace('/', '.') as_name = python_path.replace('.', '_dot_') + '__pb2' module_path = python_path[:python_path.rindex('.')] @@ -1,4 +1,21 @@ #!/usr/bin/env python3 + +# 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. + +"""Custom mmi2grpc setuptools commands.""" + from setuptools import setup, Command from setuptools.command.build_py import build_py import pkg_resources @@ -10,7 +27,7 @@ os.environ["PATH"] = package_directory + ':' + os.environ["PATH"] class BuildGrpc(Command): - """GRPC build command.""" + """gRPC build command.""" description = 'build grpc files' user_options = [] @@ -25,7 +42,8 @@ class BuildGrpc(Command): proto_include = pkg_resources.resource_filename('grpc_tools', '_proto') - files = [f'pandora/{f}' for f in os.listdir('proto/pandora') if f.endswith('.proto')] + files = [f'pandora/{f}' + for f in os.listdir('proto/pandora') if f.endswith('.proto')] protoc.main([ 'grpc_tools.protoc', '-Iproto', |