diff options
author | David Duarte <licorne@google.com> | 2022-08-12 19:05:01 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-08-12 19:05:01 +0000 |
commit | 294d14ffd4225bc4dae351357aabb223ec4354ee (patch) | |
tree | bd3595732ee9d3b5f7e37d5ddaa4ac9d9d8974a2 | |
parent | 710542227511d9ab03a2ea542b000e42f98f21aa (diff) | |
parent | 5dba88d8fe032cdaa05f22ead9cbb417bf98ece8 (diff) | |
download | avatar-294d14ffd4225bc4dae351357aabb223ec4354ee.tar.gz |
Merge upstream commit 'ee786f0f351281875c48a8e77810d7b35e0d4671' into master am: 65c5c618d0 am: 06792eb8ce am: 9835084430 am: 5dba88d8fe
Original change: https://android-review.googlesource.com/c/platform/external/pandora/avatar/+/2183016
Change-Id: If5a2df7441d8c7d5fcd4c22a7d874580e276a1c0
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .gitmodules | 6 | ||||
-rw-r--r-- | CONTRIBUTING.md | 30 | ||||
-rw-r--r-- | LICENSE | 202 | ||||
-rw-r--r-- | README.md | 29 | ||||
-rw-r--r-- | avatar/__init__.py | 20 | ||||
-rw-r--r-- | avatar/android_service.py | 63 | ||||
-rw-r--r-- | avatar/bumble_server/__init__.py | 81 | ||||
-rw-r--r-- | avatar/bumble_server/device_config.json | 5 | ||||
-rw-r--r-- | avatar/bumble_server/host.py | 90 | ||||
-rw-r--r-- | avatar/controllers/__init__.py | 0 | ||||
-rw-r--r-- | avatar/controllers/pandora_device.py | 132 | ||||
-rw-r--r-- | avatar/utils.py | 32 | ||||
m--------- | bt-test-interfaces | 0 | ||||
m--------- | bumble | 0 | ||||
-rw-r--r-- | examples/example.py | 47 | ||||
-rw-r--r-- | examples/example_classic_connect.py | 77 | ||||
-rw-r--r-- | examples/example_config.yml | 11 | ||||
-rw-r--r-- | pyproject.toml | 15 |
19 files changed, 842 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ea05a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv/ +__pycache__/
\ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..155d21e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "bt-test-interfaces"] + path = bt-test-interfaces + url = sso://pandora/bt-test-interfaces +[submodule "bumble"] + path = bumble + url = sso://pandora/third_party/bumble 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb0297b --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Avatar + +Avatar aims to provide a scalable multi-platform Bluetooth testing tool capable +of running any Bluetooth test cases virtually and physically. It aims to +complete PTS-bot in the Pandora testing suite. + +## Install + +```bash +git submodule update --init +python -m venv venv +source venv/bin/activate.fish # or any other shell +pip install [-e] bt-test-interfaces/python +pip install [-e] bumble +pip install [-e] . +``` + +## Rebuild gRPC Bluetooth test interfaces + +```bash +pip install grpcio-tools==1.46.3 +./bt-test-interfaces/python/_build/grpc.py +``` + +## Usage + +```bash +python examples/example.py -c examples/example_config.yml +``` diff --git a/avatar/__init__.py b/avatar/__init__.py new file mode 100644 index 0000000..0a1dc4d --- /dev/null +++ b/avatar/__init__.py @@ -0,0 +1,20 @@ +# 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. + +""" +Avatar is a scalable multi-platform Bluetooth testing tool capable of running +any Bluetooth test cases virtually and physically. +""" + +__version__ = "0.0.1" diff --git a/avatar/android_service.py b/avatar/android_service.py new file mode 100644 index 0000000..9dcf9e8 --- /dev/null +++ b/avatar/android_service.py @@ -0,0 +1,63 @@ +# 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 time + +from mobly.controllers.android_device_lib.services.base_service \ + import BaseService + +ANDROID_SERVER_PACKAGE = 'com.android.pandora' +ANDROID_SERVER_GRPC_PORT = 8999 + + +class AndroidService(BaseService): + + def __init__(self, device, configs=None): + super().__init__(device, configs) + self.port = configs['port'] + self._is_alive = False + + @property + def is_alive(self): + return self._is_alive + + def start(self): + # Start Pandora Android gRPC server. + self._device.adb._exec_adb_cmd( + 'shell', + f'am instrument -r -e Debug false {ANDROID_SERVER_PACKAGE}/.Main', + shell=False, + timeout=None, + stderr=None) + + self._device.adb.forward( + [f'tcp:{self.port}', f'tcp:{ANDROID_SERVER_GRPC_PORT}']) + + # Wait a few seconds for the Android gRPC server to be started. + time.sleep(3) + + self._is_alive = True + + def stop(self): + # Stop Pandora Android gRPC server. + self._device.adb._exec_adb_cmd( + 'shell', + f'am force-stop {ANDROID_SERVER_PACKAGE}', + shell=False, + timeout=None, + stderr=None) + + self._device.adb.forward(['--remove', f'tcp:{self.port}']) + + self._is_alive = False diff --git a/avatar/bumble_server/__init__.py b/avatar/bumble_server/__init__.py new file mode 100644 index 0000000..c3a307e --- /dev/null +++ b/avatar/bumble_server/__init__.py @@ -0,0 +1,81 @@ +# 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. + +"""Pandora Bumble Server.""" + +__version__ = "0.0.1" + +import asyncio +import logging +import os +import grpc + +from bumble.host import Host +from bumble.device import Device, DeviceConfiguration +from bumble.transport import open_transport + +from bumble.a2dp import make_audio_sink_service_sdp_records + +from pandora.host_grpc import add_HostServicer_to_server +from .host import HostService + +BUMBLE_SERVER_PORT = 7999 +ROOTCANAL_PORT_CUTTLEFISH = 7300 + +current_dir = os.path.dirname(os.path.realpath(__file__)) + + +class BumblePandoraServer: + def __init__(self, grpc_port, hci, config): + self.hci = hci + device_config = DeviceConfiguration() + device_config.load_from_dict(config) + host = Host(controller_source=hci.source, controller_sink=hci.sink) + self.device = Device(config=device_config, host=host) + self.device.classic_enabled = config.get('classic_enabled', False) + + self.server = grpc.aio.server() + add_HostServicer_to_server(HostService(self.device), self.server) + self.grpc_port = self.server.add_insecure_port( + f'localhost:{grpc_port}') + + @classmethod + async def open(cls, grpc_port, transport_name, config): + hci = await open_transport(transport_name) + return cls(grpc_port=grpc_port, hci=hci, config=config) + + async def start(self): + await self.device.power_on() + await self.server.start() + + async def wait_for_termination(self): + await self.server.wait_for_termination() + + async def close(self): + await self.server.stop(None) + await self.hci.close() + + +async def serve(): + transport = f'tcp-client:127.0.0.1:{ROOTCANAL_PORT_CUTTLEFISH}' + server = await BumblePandoraServer.open(BUMBLE_SERVER_PORT, transport, {'classic_enabled': True}) + + await server.start() + await server.wait_for_termination() + await server.close() + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) + asyncio.run(serve()) diff --git a/avatar/bumble_server/device_config.json b/avatar/bumble_server/device_config.json new file mode 100644 index 0000000..26b7f05 --- /dev/null +++ b/avatar/bumble_server/device_config.json @@ -0,0 +1,5 @@ +{ + "name": "Bumble", + "class_of_device": 2360324, + "keystore": "JsonKeyStore" +}
\ No newline at end of file diff --git a/avatar/bumble_server/host.py b/avatar/bumble_server/host.py new file mode 100644 index 0000000..96b73a2 --- /dev/null +++ b/avatar/bumble_server/host.py @@ -0,0 +1,90 @@ +# 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 logging + +from bumble.core import BT_BR_EDR_TRANSPORT +from bumble.hci import Address, HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR +from bumble.smp import PairingConfig + +from pandora.host_pb2 import ReadLocalAddressResponse, ConnectResponse, \ + Connection, DisconnectResponse, GetConnectionResponse +from pandora.host_grpc import HostServicer + + +class HostService(HostServicer): + + def __init__(self, device): + self.device = device + self.device.pairing_config_factory = lambda connection: PairingConfig( + bonding=False) + + async def ReadLocalAddress(self, request, context): + logging.info('ReadLocalAddress') + return ReadLocalAddressResponse( + address=bytes(reversed(bytes(self.device.public_address)))) + + async def Connect(self, request, context): + # Need to reverse bytes order since Bumble Address is using MSB. + address = Address(bytes(reversed(request.address))) + logging.info(f"Connect: {address}") + + try: + logging.info("Connecting...") + connection = await self.device.connect( + address, transport=BT_BR_EDR_TRANSPORT) + logging.info("Connected") + + logging.info("Authenticating...") + await self.device.authenticate(connection) + logging.info("Authenticated") + + logging.info("Enabling encryption...") + await self.device.encrypt(connection) + logging.info("Encryption on") + + logging.info(f"Connect: connection handle: {connection.handle}") + connection_handle = connection.handle.to_bytes(4, 'big') + return ConnectResponse(connection=Connection(cookie=connection_handle)) + + except Exception as error: + logging.error(error) + return ConnectResponse() + + async def Disconnect(self, request, context): + # Need to reverse bytes order since Bumble Address is using MSB. + connection_handle = int.from_bytes(request.connection.cookie,'big') + logging.info(f"Disconnect: {connection_handle}") + + try: + logging.info("Disconnecting...") + connection = self.device.lookup_connection(connection_handle) + await connection.disconnect(HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR) + except Exception as error: + logging.error(error) + + return DisconnectResponse() + + async def GetConnection(self, request, context): + address = Address(bytes(reversed(request.address))) + logging.info(f"GetConnection: {address}") + + try: + connection_handle = self.device.find_connection_by_bd_addr( + address).handle.to_bytes(4, 'big') + return GetConnectionResponse(connection=Connection(cookie=connection_handle)) + + except Exception as error: + logging.error(error) + return GetConnectionResponse() diff --git a/avatar/controllers/__init__.py b/avatar/controllers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/avatar/controllers/__init__.py diff --git a/avatar/controllers/pandora_device.py b/avatar/controllers/pandora_device.py new file mode 100644 index 0000000..a8922e2 --- /dev/null +++ b/avatar/controllers/pandora_device.py @@ -0,0 +1,132 @@ +# 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 logging +import grpc +import importlib +import asyncio +import threading + +import mobly.controllers.android_device +import mobly.signals + +from ..android_service import AndroidService +from ..bumble_server import BumblePandoraServer +from ..utils import Address + + +from pandora.host_grpc import Host + +MOBLY_CONTROLLER_CONFIG_NAME = 'PandoraDevice' + + +def create(configs): + def create_device(config): + module_name = config.pop('module', PandoraDevice.__module__) + class_name = config.pop('class', PandoraDevice.__name__) + + module = importlib.import_module(module_name) + return getattr(module, class_name).create(**config) + + return list(map(create_device, configs)) + + +def destroy(devices): + for device in devices: + device.close() + + +class PandoraDevice: + + def __init__(self, target): + self.channel = grpc.insecure_channel(target) + self.log = PandoraDeviceLoggerAdapter(logging.getLogger(), { + 'class': self.__class__.__name__, + 'address': self.address + }) + + @classmethod + def create(cls, **kwargs): + return cls(**kwargs) + + @property + def host(self): + return Host(self.channel) + + @property + def address(self): + return Address(self.host.ReadLocalAddress().address) + + def close(self): + self.channel.close() + + +class PandoraDeviceLoggerAdapter(logging.LoggerAdapter): + def process(self, msg, kwargs): + msg = f'[{self.extra["class"]}|{self.extra["address"]}] {msg}' + return (msg, kwargs) + + +class AndroidPandoraDevice(PandoraDevice): + + def __init__(self, android_device): + self.android_device = android_device + # TODO: Use a dynamic port + port = 8999 + self.android_device.services.register('pandora', AndroidService, configs={ + 'port': port + }) + super().__init__(f'localhost:{port}') + + def close(self): + super().close() + mobly.controllers.android_device.destroy([self.android_device]) + + @classmethod + def create(cls, config): + android_devices = mobly.controllers.android_device.create(config) + if not android_devices: + raise mobly.signals.ControllerError( + 'Expected to get at least 1 android controller objects, got 0.') + head, *tail = android_devices + mobly.controllers.android_device.destroy(tail) + return cls(head) + + +class BumblePandoraDevice(PandoraDevice): + + def __init__(self, server): + self.server = server + self.loop = asyncio.get_event_loop() + self.loop.run_until_complete(self.server.start()) + self.thread = threading.Thread(target=lambda: self.loop.run_forever()) + self.thread.start() + + super().__init__(f'localhost:{self.server.grpc_port}') + + def close(self): + super().close() + self.loop.call_soon_threadsafe(lambda: self.loop.stop()) + self.thread.join() + + @property + def device(self): + return self.server.device + + @classmethod + def create(cls, transport, **kwargs): + loop = asyncio.get_event_loop() + server = loop.run_until_complete( + BumblePandoraServer.open(0, transport, kwargs)) + return cls(server) diff --git a/avatar/utils.py b/avatar/utils.py new file mode 100644 index 0000000..fdf67da --- /dev/null +++ b/avatar/utils.py @@ -0,0 +1,32 @@ +# 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. + + +class Address(bytes): + + def __new__(cls, address): + if type(address) is bytes: + address_bytes = address + elif type(address) is str: + address_bytes = bytes.fromhex(address.replace(':', '')) + else: + raise ValueError('Invalid address format') + + if len(address_bytes) != 6: + raise ValueError('Invalid address length') + + return bytes.__new__(cls, address_bytes) + + def __str__(self): + return ':'.join([f'{x:02X}' for x in self]) diff --git a/bt-test-interfaces b/bt-test-interfaces new file mode 160000 +Subproject 469be68bd8e816707adf43d71af9b3cd22226b9 diff --git a/bumble b/bumble new file mode 160000 +Subproject 448c8e94a70b01ebb0a24cddc8b0bcad4cc80d6 diff --git a/examples/example.py b/examples/example.py new file mode 100644 index 0000000..9bbec15 --- /dev/null +++ b/examples/example.py @@ -0,0 +1,47 @@ +# 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 logging + +from mobly import test_runner, base_test, asserts +from grpc import RpcError + +from avatar.controllers import pandora_device + + +class ExampleTest(base_test.BaseTestClass): + def setup_class(self): + self.pandora_devices = self.register_controller(pandora_device) + self.dut = self.pandora_devices[0] + self.ref = self.pandora_devices[1] + + def test_print_addresses(self): + dut_address = self.dut.address + self.dut.log.info(f'Address: {dut_address}') + ref_address = self.ref.address + self.ref.log.info(f'Address: {ref_address}') + + def test_classic_connect(self): + dut_address = self.dut.address + self.dut.log.info(f'Address: {dut_address}') + try: + self.ref.host.Connect(address=dut_address) + except RpcError as error: + self.dut.log.error(error) + asserts.assert_true(False, 'gRPC Error') + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) + test_runner.main() diff --git a/examples/example_classic_connect.py b/examples/example_classic_connect.py new file mode 100644 index 0000000..760754e --- /dev/null +++ b/examples/example_classic_connect.py @@ -0,0 +1,77 @@ +# 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 logging + +from mobly import suite_runner, asserts, base_test + +from avatar.controllers import pandora_device + +from bumble.smp import PairingDelegate, PairingConfig + + +class ClassicConnect(base_test.BaseTestClass): + def setup_class(self): + self.pandora_devices = self.register_controller(pandora_device) + self.dut = self.pandora_devices[0] + self.bumble = self.pandora_devices[1] + + def setup_test(self): + self.dut.host.Reset() + + def test_io_cap_keyboard_only(self): + self._connect_to_dut(PairingDelegate.KEYBOARD_INPUT_ONLY) + + def test_display_yes_no(self): + self._connect_to_dut(PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT) + + def test_io_cap_display_only(self): + self._connect_to_dut(PairingDelegate.DISPLAY_OUTPUT_ONLY) + + def test_io_cap_no_input_no_output(self): + self._connect_to_dut(PairingDelegate.NO_OUTPUT_NO_INPUT) + + def _connect_to_dut(self, io_cap): + bumble_address = self.bumble.address + self.bumble.device.pairing_config_factory = lambda _: PairingConfig( + delegate=Delegate(io_cap, self.dut, bumble_address) + ) + connect_resp = self.bumble.host.Connect( + address=self.dut.address, wait_for_ready=True) + asserts.assert_true(connect_resp.WhichOneof( + "result") == "connection", "Failed to connect") + + +class Delegate(PairingDelegate): + + def __init__(self, io_capability, dut, address): + super().__init__(io_capability) + logging.info("Delegate init") + self._dut = dut + self._address = address + + async def get_number(self): + logging.info("get_number") + passkey = self._dut.host.ReadPasskey(address=self._address).passkey + return passkey + + async def compare_numbers(self, number, digits=6): + logging.info("compare_number") + dut_passkey = self._dut.host.ReadPasskey(address=self._address).passkey + return dut_passkey == number + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) + suite_runner.run_suite([ClassicConnect]) diff --git a/examples/example_config.yml b/examples/example_config.yml new file mode 100644 index 0000000..0a5c4e6 --- /dev/null +++ b/examples/example_config.yml @@ -0,0 +1,11 @@ +--- + +TestBeds: +- Name: ExampleTest + Controllers: + PandoraDevice: + - class: AndroidPandoraDevice + config: '*' + - class: BumblePandoraDevice + transport: 'tcp-client:127.0.0.1:7300' + classic_enabled: true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..660268d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "avatar" +authors = [{name = "Pandora", email = "pandora-core@google.com"}] +readme = "README.md" +dynamic = ["version", "description"] +dependencies = [ + "bt-test-interfaces", + "bumble", + "grpcio>=1.41", + "mobly>=1.11.1" +] + +[build-system] +requires = ["flit_core==3.7.1"] +build-backend = "flit_core.buildapi" |