From a8ec1b09493616dde52b658317e3c87551016176 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Fri, 3 Feb 2023 18:45:45 -0800 Subject: [PATCH 1/7] minor cleanup of the internals of the usb transport implementation --- bumble/transport/usb.py | 111 +++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/bumble/transport/usb.py b/bumble/transport/usb.py index ccc82c1..baeb27f 100644 --- a/bumble/transport/usb.py +++ b/bumble/transport/usb.py @@ -113,7 +113,7 @@ async def open_usb_transport(spec: str) -> Transport: def __init__(self, device, acl_out): self.device = device self.acl_out = acl_out - self.transfer = device.getTransfer() + self.acl_out_transfer = device.getTransfer() self.packets = collections.deque() # Queue of packets waiting to be sent self.loop = asyncio.get_running_loop() self.cancel_done = self.loop.create_future() @@ -137,21 +137,20 @@ async def open_usb_transport(spec: str) -> Transport: # The queue was previously empty, re-prime the pump self.process_queue() - def on_packet_sent(self, transfer): + def transfer_callback(self, transfer): status = transfer.getStatus() - # logger.debug(f'<<< USB out transfer callback: status={status}') # pylint: disable=no-member if status == usb1.TRANSFER_COMPLETED: - self.loop.call_soon_threadsafe(self.on_packet_sent_) + self.loop.call_soon_threadsafe(self.on_packet_sent) elif status == usb1.TRANSFER_CANCELLED: self.loop.call_soon_threadsafe(self.cancel_done.set_result, None) else: logger.warning( - color(f'!!! out transfer not completed: status={status}', 'red') + color(f'!!! OUT transfer not completed: status={status}', 'red') ) - def on_packet_sent_(self): + def on_packet_sent(self): if self.packets: self.packets.popleft() self.process_queue() @@ -163,22 +162,20 @@ async def open_usb_transport(spec: str) -> Transport: packet = self.packets[0] packet_type = packet[0] if packet_type == hci.HCI_ACL_DATA_PACKET: - self.transfer.setBulk( - self.acl_out, packet[1:], callback=self.on_packet_sent + self.acl_out_transfer.setBulk( + self.acl_out, packet[1:], callback=self.transfer_callback ) - logger.debug('submit ACL') - self.transfer.submit() + self.acl_out_transfer.submit() elif packet_type == hci.HCI_COMMAND_PACKET: - self.transfer.setControl( + self.acl_out_transfer.setControl( USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS, 0, 0, 0, packet[1:], - callback=self.on_packet_sent, + callback=self.transfer_callback, ) - logger.debug('submit COMMAND') - self.transfer.submit() + self.acl_out_transfer.submit() else: logger.warning(color(f'unsupported packet type {packet_type}', 'red')) @@ -193,11 +190,11 @@ async def open_usb_transport(spec: str) -> Transport: self.packets.clear() # If we have a transfer in flight, cancel it - if self.transfer.isSubmitted(): + if self.acl_out_transfer.isSubmitted(): # Try to cancel the transfer, but that may fail because it may have # already completed try: - self.transfer.cancel() + self.acl_out_transfer.cancel() logger.debug('waiting for OUT transfer cancellation to be done...') await self.cancel_done @@ -206,27 +203,22 @@ async def open_usb_transport(spec: str) -> Transport: logger.debug('OUT transfer likely already completed') class UsbPacketSource(asyncio.Protocol, ParserSource): - def __init__(self, context, device, metadata, acl_in, events_in): + def __init__(self, device, metadata, acl_in, events_in): super().__init__() - self.context = context self.device = device self.metadata = metadata self.acl_in = acl_in + self.acl_in_transfer = None self.events_in = events_in + self.events_in_transfer = None self.loop = asyncio.get_running_loop() self.queue = asyncio.Queue() self.dequeue_task = None - self.closed = False - self.event_loop_done = self.loop.create_future() self.cancel_done = { hci.HCI_EVENT_PACKET: self.loop.create_future(), hci.HCI_ACL_DATA_PACKET: self.loop.create_future(), } - self.events_in_transfer = None - self.acl_in_transfer = None - - # Create a thread to process events - self.event_thread = threading.Thread(target=self.run) + self.closed = False def start(self): # Set up transfer objects for input @@ -234,7 +226,7 @@ async def open_usb_transport(spec: str) -> Transport: self.events_in_transfer.setInterrupt( self.events_in, READ_SIZE, - callback=self.on_packet_received, + callback=self.transfer_callback, user_data=hci.HCI_EVENT_PACKET, ) self.events_in_transfer.submit() @@ -243,22 +235,23 @@ async def open_usb_transport(spec: str) -> Transport: self.acl_in_transfer.setBulk( self.acl_in, READ_SIZE, - callback=self.on_packet_received, + callback=self.transfer_callback, user_data=hci.HCI_ACL_DATA_PACKET, ) self.acl_in_transfer.submit() self.dequeue_task = self.loop.create_task(self.dequeue()) - self.event_thread.start() - def on_packet_received(self, transfer): + @property + def usb_transfer_submitted(self): + return ( + self.events_in_transfer.isSubmitted() + or self.acl_in_transfer.isSubmitted() + ) + + def transfer_callback(self, transfer): packet_type = transfer.getUserData() status = transfer.getStatus() - # logger.debug( - # f'<<< USB IN transfer callback: status={status} ' - # f'packet_type={packet_type} ' - # f'length={transfer.getActualLength()}' - # ) # pylint: disable=no-member if status == usb1.TRANSFER_COMPLETED: @@ -267,19 +260,18 @@ async def open_usb_transport(spec: str) -> Transport: + transfer.getBuffer()[: transfer.getActualLength()] ) self.loop.call_soon_threadsafe(self.queue.put_nowait, packet) + + # Re-submit the transfer so we can receive more data + transfer.submit() elif status == usb1.TRANSFER_CANCELLED: self.loop.call_soon_threadsafe( self.cancel_done[packet_type].set_result, None ) - return else: logger.warning( - color(f'!!! transfer not completed: status={status}', 'red') + color(f'!!! IN transfer not completed: status={status}', 'red') ) - # Re-submit the transfer so we can receive more data - transfer.submit() - async def dequeue(self): while not self.closed: try: @@ -288,21 +280,6 @@ async def open_usb_transport(spec: str) -> Transport: return self.parser.feed_data(packet) - def run(self): - logger.debug('starting USB event loop') - while ( - self.events_in_transfer.isSubmitted() - or self.acl_in_transfer.isSubmitted() - ): - # pylint: disable=no-member - try: - self.context.handleEvents() - except usb1.USBErrorInterrupted: - pass - - logger.debug('USB event loop done') - self.loop.call_soon_threadsafe(self.event_loop_done.set_result, None) - def close(self): self.closed = True @@ -331,15 +308,14 @@ async def open_usb_transport(spec: str) -> Transport: f'IN[{packet_type}] transfer likely already completed' ) - # Wait for the thread to terminate - await self.event_loop_done - class UsbTransport(Transport): def __init__(self, context, device, interface, setting, source, sink): super().__init__(source, sink) self.context = context self.device = device self.interface = interface + self.loop = asyncio.get_running_loop() + self.event_loop_done = self.loop.create_future() # Get exclusive access device.claimInterface(interface) @@ -352,6 +328,22 @@ async def open_usb_transport(spec: str) -> Transport: source.start() sink.start() + # Create a thread to process events + self.event_thread = threading.Thread(target=self.run) + self.event_thread.start() + + def run(self): + logger.debug('starting USB event loop') + while self.source.usb_transfer_submitted: + # pylint: disable=no-member + try: + self.context.handleEvents() + except usb1.USBErrorInterrupted: + pass + + logger.debug('USB event loop done') + self.loop.call_soon_threadsafe(self.event_loop_done.set_result, None) + async def close(self): self.source.close() self.sink.close() @@ -361,6 +353,9 @@ async def open_usb_transport(spec: str) -> Transport: self.device.close() self.context.close() + # Wait for the thread to terminate + await self.event_loop_done + # Find the device according to the spec moniker load_libusb() context = usb1.USBContext() @@ -540,7 +535,7 @@ async def open_usb_transport(spec: str) -> Transport: except usb1.USBError: logger.warning('failed to set configuration') - source = UsbPacketSource(context, device, device_metadata, acl_in, events_in) + source = UsbPacketSource(device, device_metadata, acl_in, events_in) sink = UsbPacketSink(device, acl_out) return UsbTransport(context, device, interface, setting, source, sink) except usb1.USBError as error: From 9c7089c8fff039ba41ed14ee53ac3bf4cfb595bd Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Sun, 19 Nov 2023 11:36:38 -0800 Subject: [PATCH 2/7] terminate when unplugged --- bumble/transport/usb.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bumble/transport/usb.py b/bumble/transport/usb.py index baeb27f..d48b239 100644 --- a/bumble/transport/usb.py +++ b/bumble/transport/usb.py @@ -24,9 +24,10 @@ import platform import usb1 -from .common import Transport, ParserSource -from .. import hci -from ..colors import color +from bumble.transport.common import Transport, ParserSource +from bumble import hci +from bumble.colors import color +from bumble.utils import AsyncRunner # ----------------------------------------------------------------------------- @@ -271,6 +272,7 @@ async def open_usb_transport(spec: str) -> Transport: logger.warning( color(f'!!! IN transfer not completed: status={status}', 'red') ) + self.loop.call_soon_threadsafe(self.on_transport_lost) async def dequeue(self): while not self.closed: From f9f5d7ccbde01043dfe641bb01117a5274d65e36 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Sun, 29 Oct 2023 08:43:39 -0700 Subject: [PATCH 3/7] first implementation (+1 squashed commit) Squashed commits: [ee00d67] wip --- apps/bench.py | 139 ++++++-- apps/controller_info.py | 14 + apps/pair.py | 9 + bumble/device.py | 20 ++ docs/mkdocs/mkdocs.yml | 1 + docs/mkdocs/src/extras/android_bt_bench.md | 64 ++++ docs/mkdocs/src/extras/index.md | 10 +- extras/android/BtBench/.gitignore | 15 + extras/android/BtBench/app/.gitignore | 1 + extras/android/BtBench/app/build.gradle.kts | 70 ++++ extras/android/BtBench/app/proguard-rules.pro | 21 ++ .../BtBench/app/src/main/AndroidManifest.xml | 40 +++ .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 43530 bytes .../google/bumble/btbench/L2capClient.kt | 35 ++ .../google/bumble/btbench/L2capServer.kt | 62 ++++ .../google/bumble/btbench/MainActivity.kt | 307 ++++++++++++++++++ .../com/github/google/bumble/btbench/Model.kt | 163 ++++++++++ .../github/google/bumble/btbench/Packet.kt | 178 ++++++++++ .../github/google/bumble/btbench/Receiver.kt | 58 ++++ .../google/bumble/btbench/RfcommClient.kt | 36 ++ .../google/bumble/btbench/RfcommServer.kt | 35 ++ .../github/google/bumble/btbench/Sender.kt | 84 +++++ .../google/bumble/btbench/SocketClient.kt | 63 ++++ .../google/bumble/btbench/SocketServer.kt | 66 ++++ .../google/bumble/btbench/ui/theme/Color.kt | 11 + .../google/bumble/btbench/ui/theme/Theme.kt | 63 ++++ .../google/bumble/btbench/ui/theme/Type.kt | 33 ++ .../res/drawable/ic_launcher_background.xml | 74 +++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 3562 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 4268 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 2378 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2748 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 4722 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 6248 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 7090 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 9588 bytes .../ic_launcher_foreground.webp | Bin 0 -> 9766 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 13350 bytes .../app/src/main/res/values/colors.xml | 10 + .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 5 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ extras/android/BtBench/build.gradle.kts | 7 + extras/android/BtBench/gradle.properties | 23 ++ .../android/BtBench/gradle/libs.versions.toml | 31 ++ .../BtBench/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + extras/android/BtBench/gradlew | 185 +++++++++++ extras/android/BtBench/gradlew.bat | 89 +++++ extras/android/BtBench/settings.gradle.kts | 24 ++ .../RemoteHCI/gradle/libs.versions.toml | 4 + 56 files changed, 2109 insertions(+), 26 deletions(-) create mode 100644 docs/mkdocs/src/extras/android_bt_bench.md create mode 100644 extras/android/BtBench/.gitignore create mode 100644 extras/android/BtBench/app/.gitignore create mode 100644 extras/android/BtBench/app/build.gradle.kts create mode 100644 extras/android/BtBench/app/proguard-rules.pro create mode 100644 extras/android/BtBench/app/src/main/AndroidManifest.xml create mode 100644 extras/android/BtBench/app/src/main/ic_launcher-playstore.png create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capServer.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Packet.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Receiver.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/RfcommClient.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/RfcommServer.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Sender.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketClient.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketServer.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Color.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Theme.kt create mode 100644 extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Type.kt create mode 100644 extras/android/BtBench/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 extras/android/BtBench/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 extras/android/BtBench/app/src/main/res/values/colors.xml create mode 100644 extras/android/BtBench/app/src/main/res/values/ic_launcher_background.xml create mode 100644 extras/android/BtBench/app/src/main/res/values/strings.xml create mode 100644 extras/android/BtBench/app/src/main/res/values/themes.xml create mode 100644 extras/android/BtBench/app/src/main/res/xml/backup_rules.xml create mode 100644 extras/android/BtBench/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 extras/android/BtBench/build.gradle.kts create mode 100644 extras/android/BtBench/gradle.properties create mode 100644 extras/android/BtBench/gradle/libs.versions.toml create mode 100644 extras/android/BtBench/gradle/wrapper/gradle-wrapper.jar create mode 100644 extras/android/BtBench/gradle/wrapper/gradle-wrapper.properties create mode 100755 extras/android/BtBench/gradlew create mode 100644 extras/android/BtBench/gradlew.bat create mode 100644 extras/android/BtBench/settings.gradle.kts diff --git a/apps/bench.py b/apps/bench.py index 8b37883..de14eee 100644 --- a/apps/bench.py +++ b/apps/bench.py @@ -77,6 +77,7 @@ SPEED_SERVICE_UUID = '50DB505C-8AC4-4738-8448-3B1D9CC09CC5' SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53' SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D' +DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE' DEFAULT_L2CAP_PSM = 1234 DEFAULT_L2CAP_MAX_CREDITS = 128 DEFAULT_L2CAP_MTU = 1022 @@ -128,11 +129,16 @@ def print_connection(connection): if connection.transport == BT_LE_TRANSPORT: phy_state = ( 'PHY=' - f'RX:{le_phy_name(connection.phy.rx_phy)}/' - f'TX:{le_phy_name(connection.phy.tx_phy)}' + f'TX:{le_phy_name(connection.phy.tx_phy)}/' + f'RX:{le_phy_name(connection.phy.rx_phy)}' ) - data_length = f'DL={connection.data_length}' + data_length = ( + 'DL=(' + f'TX:{connection.data_length[0]}/{connection.data_length[1]},' + f'RX:{connection.data_length[2]}/{connection.data_length[3]}' + ')' + ) connection_parameters = ( 'Parameters=' f'{connection.parameters.connection_interval * 1.25:.2f}/' @@ -169,9 +175,7 @@ def make_sdp_records(channel): ), ServiceAttribute( SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, - DataElement.sequence( - [DataElement.uuid(UUID('E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'))] - ), + DataElement.sequence([DataElement.uuid(UUID(DEFAULT_RFCOMM_UUID))]), ), ServiceAttribute( SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, @@ -224,7 +228,7 @@ class Sender: if self.tx_start_delay: print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue')) - await asyncio.sleep(self.tx_start_delay) # FIXME + await asyncio.sleep(self.tx_start_delay) print(color('=== Sending RESET', 'magenta')) await self.packet_io.send_packet(bytes([PacketType.RESET])) @@ -364,7 +368,7 @@ class Ping: if self.tx_start_delay: print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue')) - await asyncio.sleep(self.tx_start_delay) # FIXME + await asyncio.sleep(self.tx_start_delay) print(color('=== Sending RESET', 'magenta')) await self.packet_io.send_packet(bytes([PacketType.RESET])) @@ -710,14 +714,14 @@ class L2capServer(StreamedPacketIO): self.l2cap_channel = None self.ready = asyncio.Event() - # Listen for incoming L2CAP CoC connections + # Listen for incoming L2CAP connections device.create_l2cap_server( spec=l2cap.LeCreditBasedChannelSpec( psm=psm, mtu=mtu, mps=mps, max_credits=max_credits ), handler=self.on_l2cap_channel, ) - print(color(f'### Listening for CoC connection on PSM {psm}', 'yellow')) + print(color(f'### Listening for L2CAP connection on PSM {psm}', 'yellow')) async def on_connection(self, connection): connection.on('disconnection', self.on_disconnection) @@ -743,9 +747,10 @@ class L2capServer(StreamedPacketIO): # RfcommClient # ----------------------------------------------------------------------------- class RfcommClient(StreamedPacketIO): - def __init__(self, device): + def __init__(self, device, channel): super().__init__() self.device = device + self.channel = channel self.ready = asyncio.Event() async def on_connection(self, connection): @@ -757,10 +762,9 @@ class RfcommClient(StreamedPacketIO): rfcomm_mux = await rfcomm_client.start() print(color('*** Started', 'blue')) - channel = DEFAULT_RFCOMM_CHANNEL - print(color(f'### Opening session for channel {channel}...', 'yellow')) + print(color(f'### Opening session for channel {self.channel}...', 'yellow')) try: - rfcomm_session = await rfcomm_mux.open_dlc(channel) + rfcomm_session = await rfcomm_mux.open_dlc(self.channel) print(color('### Session open', 'yellow'), rfcomm_session) except bumble.core.ConnectionError as error: print(color(f'!!! Session open failed: {error}', 'red')) @@ -780,7 +784,7 @@ class RfcommClient(StreamedPacketIO): # RfcommServer # ----------------------------------------------------------------------------- class RfcommServer(StreamedPacketIO): - def __init__(self, device): + def __init__(self, device, channel): super().__init__() self.ready = asyncio.Event() @@ -788,7 +792,7 @@ class RfcommServer(StreamedPacketIO): rfcomm_server = bumble.rfcomm.Server(device) # Listen for incoming DLC connections - channel_number = rfcomm_server.listen(self.on_dlc, DEFAULT_RFCOMM_CHANNEL) + channel_number = rfcomm_server.listen(self.on_dlc, channel) # Setup the SDP to advertise this channel device.sdp_service_records = make_sdp_records(channel_number) @@ -825,6 +829,9 @@ class Central(Connection.Listener): mode_factory, connection_interval, phy, + authenticate, + encrypt, + extended_data_length, ): super().__init__() self.transport = transport @@ -832,6 +839,9 @@ class Central(Connection.Listener): self.classic = classic self.role_factory = role_factory self.mode_factory = mode_factory + self.authenticate = authenticate + self.encrypt = encrypt or authenticate + self.extended_data_length = extended_data_length self.device = None self.connection = None @@ -904,7 +914,26 @@ class Central(Connection.Listener): self.connection.listener = self print_connection(self.connection) - await mode.on_connection(self.connection) + # Request a new data length if requested + if self.extended_data_length: + print(color('+++ Requesting extended data length', 'cyan')) + await self.connection.set_data_length( + self.extended_data_length[0], self.extended_data_length[1] + ) + + # Authenticate if requested + if self.authenticate: + # Request authentication + print(color('*** Authenticating...', 'cyan')) + await self.connection.authenticate() + print(color('*** Authenticated', 'cyan')) + + # Encrypt if requested + if self.encrypt: + # Enable encryption + print(color('*** Enabling encryption...', 'cyan')) + await self.connection.encrypt() + print(color('*** Encryption on', 'cyan')) # Set the PHY if requested if self.phy is not None: @@ -919,6 +948,8 @@ class Central(Connection.Listener): ) ) + await mode.on_connection(self.connection) + await role.run() await asyncio.sleep(DEFAULT_LINGER_TIME) @@ -943,9 +974,12 @@ class Central(Connection.Listener): # Peripheral # ----------------------------------------------------------------------------- class Peripheral(Device.Listener, Connection.Listener): - def __init__(self, transport, classic, role_factory, mode_factory): + def __init__( + self, transport, classic, extended_data_length, role_factory, mode_factory + ): self.transport = transport self.classic = classic + self.extended_data_length = extended_data_length self.role_factory = role_factory self.role = None self.mode_factory = mode_factory @@ -1006,6 +1040,15 @@ class Peripheral(Device.Listener, Connection.Listener): self.connection = connection self.connected.set() + # Request a new data length if needed + if self.extended_data_length: + print("+++ Requesting extended data length") + AsyncRunner.spawn( + connection.set_data_length( + self.extended_data_length[0], self.extended_data_length[1] + ) + ) + def on_disconnection(self, reason): print(color(f'!!! Disconnection: reason={reason}', 'red')) self.connection = None @@ -1038,16 +1081,16 @@ def create_mode_factory(ctx, default_mode): return GattServer(device) if mode == 'l2cap-client': - return L2capClient(device) + return L2capClient(device, psm=ctx.obj['l2cap_psm']) if mode == 'l2cap-server': - return L2capServer(device) + return L2capServer(device, psm=ctx.obj['l2cap_psm']) if mode == 'rfcomm-client': - return RfcommClient(device) + return RfcommClient(device, channel=ctx.obj['rfcomm_channel']) if mode == 'rfcomm-server': - return RfcommServer(device) + return RfcommServer(device, channel=ctx.obj['rfcomm_channel']) raise ValueError('invalid mode') @@ -1113,6 +1156,22 @@ def create_role_factory(ctx, default_role): type=click.IntRange(23, 517), help='GATT MTU (gatt-client mode)', ) +@click.option( + '--extended-data-length', + help='Request a data length upon connection, specified as tx_octets/tx_time', +) +@click.option( + '--rfcomm-channel', + type=int, + default=DEFAULT_RFCOMM_CHANNEL, + help='RFComm channel to use', +) +@click.option( + '--l2cap-psm', + type=int, + default=DEFAULT_L2CAP_PSM, + help='L2CAP PSM to use', +) @click.option( '--packet-size', '-s', @@ -1139,17 +1198,34 @@ def create_role_factory(ctx, default_role): ) @click.pass_context def bench( - ctx, device_config, role, mode, att_mtu, packet_size, packet_count, start_delay + ctx, + device_config, + role, + mode, + att_mtu, + extended_data_length, + packet_size, + packet_count, + start_delay, + rfcomm_channel, + l2cap_psm, ): ctx.ensure_object(dict) ctx.obj['device_config'] = device_config ctx.obj['role'] = role ctx.obj['mode'] = mode ctx.obj['att_mtu'] = att_mtu + ctx.obj['rfcomm_channel'] = rfcomm_channel + ctx.obj['l2cap_psm'] = l2cap_psm ctx.obj['packet_size'] = packet_size ctx.obj['packet_count'] = packet_count ctx.obj['start_delay'] = start_delay + ctx.obj['extended_data_length'] = ( + [int(x) for x in extended_data_length.split('/')] + if extended_data_length + else None + ) ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server') @@ -1170,8 +1246,12 @@ def bench( help='Connection interval (in ms)', ) @click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use') +@click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)') +@click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)') @click.pass_context -def central(ctx, transport, peripheral_address, connection_interval, phy): +def central( + ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt +): """Run as a central (initiates the connection)""" role_factory = create_role_factory(ctx, 'sender') mode_factory = create_mode_factory(ctx, 'gatt-client') @@ -1186,6 +1266,9 @@ def central(ctx, transport, peripheral_address, connection_interval, phy): mode_factory, connection_interval, phy, + authenticate, + encrypt or authenticate, + ctx.obj['extended_data_length'], ).run() ) @@ -1199,7 +1282,13 @@ def peripheral(ctx, transport): mode_factory = create_mode_factory(ctx, 'gatt-server') asyncio.run( - Peripheral(transport, ctx.obj['classic'], role_factory, mode_factory).run() + Peripheral( + transport, + ctx.obj['classic'], + ctx.obj['extended_data_length'], + role_factory, + mode_factory, + ).run() ) diff --git a/apps/controller_info.py b/apps/controller_info.py index 5be4f3d..1e02a32 100644 --- a/apps/controller_info.py +++ b/apps/controller_info.py @@ -42,6 +42,8 @@ from bumble.hci import ( HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command, HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND, HCI_LE_Read_Maximum_Advertising_Data_Length_Command, + HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND, + HCI_LE_Read_Suggested_Default_Data_Length_Command, ) from bumble.host import Host from bumble.transport import open_transport_or_link @@ -117,6 +119,18 @@ async def get_le_info(host): '\n', ) + if host.supports_command(HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND): + response = await host.send_command( + HCI_LE_Read_Suggested_Default_Data_Length_Command() + ) + if command_succeeded(response): + print( + color('Suggested Default Data Length:', 'yellow'), + f'{response.return_parameters.suggested_max_tx_octets}/' + f'{response.return_parameters.suggested_max_tx_time}', + '\n', + ) + print(color('LE Features:', 'yellow')) for feature in host.supported_le_features: print(' ', name_or_number(HCI_LE_SUPPORTED_FEATURES_NAMES, feature)) diff --git a/apps/pair.py b/apps/pair.py index 996b01c..884882a 100644 --- a/apps/pair.py +++ b/apps/pair.py @@ -28,12 +28,16 @@ from bumble.pairing import OobData, PairingDelegate, PairingConfig from bumble.smp import OobContext, OobLegacyContext from bumble.smp import error_name as smp_error_name from bumble.keys import JsonKeyStore +<<<<<<< HEAD from bumble.core import ( AdvertisingData, ProtocolError, BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT, ) +======= +from bumble.core import ProtocolError, BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT +>>>>>>> e3de14f (first implementation (+1 squashed commit)) from bumble.gatt import ( GATT_DEVICE_NAME_CHARACTERISTIC, GATT_GENERIC_ACCESS_SERVICE, @@ -394,9 +398,14 @@ async def pair( print(color(f'=== Connecting to {address_or_name}...', 'green')) connection = await device.connect( address_or_name, +<<<<<<< HEAD transport=BT_LE_TRANSPORT if mode == 'le' else BT_BR_EDR_TRANSPORT, ) pairing_failure = False +======= + transport=(BT_LE_TRANSPORT if mode == 'le' else BT_BR_EDR_TRANSPORT), + ) +>>>>>>> e3de14f (first implementation (+1 squashed commit)) if not request: try: diff --git a/bumble/device.py b/bumble/device.py index 45e919d..f343a4b 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -103,6 +103,7 @@ from .hci import ( HCI_LE_Set_Advertising_Data_Command, HCI_LE_Set_Advertising_Enable_Command, HCI_LE_Set_Advertising_Parameters_Command, + HCI_LE_Set_Data_Length_Command, HCI_LE_Set_Default_PHY_Command, HCI_LE_Set_Extended_Scan_Enable_Command, HCI_LE_Set_Extended_Scan_Parameters_Command, @@ -736,6 +737,9 @@ class Connection(CompositeEventEmitter): self.remove_listener('disconnection', abort.set_result) self.remove_listener('disconnection_failure', abort.set_exception) + async def set_data_length(self, tx_octets, tx_time) -> None: + return await self.device.set_data_length(self, tx_octets, tx_time) + async def update_parameters( self, connection_interval_min, @@ -2193,6 +2197,22 @@ class Device(CompositeEventEmitter): ) self.disconnecting = False + async def set_data_length(self, connection, tx_octets, tx_time) -> None: + if tx_octets < 0x001B or tx_octets > 0x00FB: + raise ValueError('tx_octets must be between 0x001B and 0x00FB') + + if tx_time < 0x0148 or tx_time > 0x4290: + raise ValueError('tx_time must be between 0x0148 and 0x4290') + + return await self.send_command( + HCI_LE_Set_Data_Length_Command( + connection_handle=connection.handle, + tx_octets=tx_octets, + tx_time=tx_time, + ), # type: ignore[call-arg] + check_result=True, + ) + async def update_connection_parameters( self, connection, diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 84e2515..6590d12 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -70,6 +70,7 @@ nav: - Extras: - extras/index.md - Android Remote HCI: extras/android_remote_hci.md + - Android BT Bench: extras/android_bt_bench.md - Hive: - hive/index.md - Speaker: hive/web/speaker/speaker.html diff --git a/docs/mkdocs/src/extras/android_bt_bench.md b/docs/mkdocs/src/extras/android_bt_bench.md new file mode 100644 index 0000000..2417e00 --- /dev/null +++ b/docs/mkdocs/src/extras/android_bt_bench.md @@ -0,0 +1,64 @@ +ANDROID BENCH APP +================= + +This Android app that is compatible with the Bumble `bench` command line app. +This app can be used to test the throughput and latency between two Android +devices, or between an Android device and another device running the Bumble +`bench` app. +Only the RFComm Client, RFComm Server, L2CAP Client and L2CAP Server modes are +supported. + +Building +-------- + +You can build the app by running `./gradlew build` (use `gradlew.bat` on Windows) from the `BtBench` top level directory. +You can also build with Android Studio: open the `BtBench` project. You can build and/or debug from there. + +If the build succeeds, you can find the app APKs (debug and release) at: + + * [Release] ``app/build/outputs/apk/release/app-release-unsigned.apk`` + * [Debug] ``app/build/outputs/apk/debug/app-debug.apk`` + + +Running +------- + +### Starting the app +You can start the app from the Android launcher, from Android Studio, or with `adb` + +#### Launching from the launcher +Just tap the app icon on the launcher, check the parameters, and tap +one of the benchmark action buttons. + +#### Launching with `adb` +Using the `am` command, you can start the activity, and pass it arguments so that you can +automatically start the benchmark test, and/or set the parameters. + +| Parameter Name | Parameter Type | Description +|------------------------|----------------|------------ +| autostart | String | Benchmark to start. (rfcomm-client, rfcomm-server, l2cap-client or l2cap-server) +| packet-count | Integer | Number of packets to send (rfcomm-client and l2cap-client only) +| packet-size | Integer | Number of bytes per packet (rfcomm-client and l2cap-client only) +| peer-bluetooth-address | Integer | Peer Bluetooth address to connect to (rfcomm-client and l2cap-client | only) + + +!!! tip "Launching from adb with auto-start" + In this example, we auto-start the Rfcomm Server bench action. + ```bash + $ adb shell am start -n com.github.google.bumble.btbench/.MainActivity --es autostart rfcomm-server + ``` + +!!! tip "Launching from adb with auto-start and some parameters" + In this example, we auto-start the Rfcomm Client bench action, set the packet count to 100, + and the packet size to 1024, and connect to DA:4C:10:DE:17:02 + ```bash + $ adb shell am start -n com.github.google.bumble.btbench/.MainActivity --es autostart rfcomm-client --ei packet-count 100 --ei packet-size 1024 --es peer-bluetooth-address DA:4C:10:DE:17:02 + ``` + +#### Selecting a Peer Bluetooth Address +The app's main activity has a "Peer Bluetooth Address" setting where you can change the address. + +!!! note "Bluetooth Address for L2CAP vs RFComm" + For BLE (L2CAP mode), the address of a device typically changes regularly (it is randomized for privacy), whereas the Bluetooth Classic addresses will remain the same (RFComm mode). + If two devices are paired and bonded, then they will each "see" a non-changing address for each other even with BLE (Resolvable Private Address) + diff --git a/docs/mkdocs/src/extras/index.md b/docs/mkdocs/src/extras/index.md index ae906c1..59af838 100644 --- a/docs/mkdocs/src/extras/index.md +++ b/docs/mkdocs/src/extras/index.md @@ -8,4 +8,12 @@ Android Remote HCI Allows using an Android phone's built-in Bluetooth controller with a Bumble stack running on a development machine. -See [Android Remote HCI](android_remote_hci.md) for details. \ No newline at end of file +See [Android Remote HCI](android_remote_hci.md) for details. + +Android BT Bench +---------------- + +An Android app that is compatible with the Bumble `bench` command line app. +This app can be used to test the throughput and latency between two Android +devices, or between an Android device and another device running the Bumble +`bench` app. \ No newline at end of file diff --git a/extras/android/BtBench/.gitignore b/extras/android/BtBench/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/extras/android/BtBench/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/extras/android/BtBench/app/.gitignore b/extras/android/BtBench/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/extras/android/BtBench/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/extras/android/BtBench/app/build.gradle.kts b/extras/android/BtBench/app/build.gradle.kts new file mode 100644 index 0000000..ffde197 --- /dev/null +++ b/extras/android/BtBench/app/build.gradle.kts @@ -0,0 +1,70 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.kotlinAndroid) +} + +android { + namespace = "com.github.google.bumble.btbench" + compileSdk = 34 + + defaultConfig { + applicationId = "com.github.google.bumble.btbench" + minSdk = 30 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation(libs.core.ktx) + implementation(libs.lifecycle.runtime.ktx) + implementation(libs.activity.compose) + implementation(platform(libs.compose.bom)) + implementation(libs.ui) + implementation(libs.ui.graphics) + implementation(libs.ui.tooling.preview) + implementation(libs.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.espresso.core) + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation(libs.ui.test.junit4) + debugImplementation(libs.ui.tooling) + debugImplementation(libs.ui.test.manifest) +} \ No newline at end of file diff --git a/extras/android/BtBench/app/proguard-rules.pro b/extras/android/BtBench/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/extras/android/BtBench/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/AndroidManifest.xml b/extras/android/BtBench/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a6b5d77 --- /dev/null +++ b/extras/android/BtBench/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/ic_launcher-playstore.png b/extras/android/BtBench/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..d27fdd27bd47aebe86395c6c2af6e7b06079ea71 GIT binary patch literal 43530 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 A*8l(j literal 0 HcmV?d00001 diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt new file mode 100644 index 0000000..7722bb8 --- /dev/null +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt @@ -0,0 +1,35 @@ +// Copyright 2023 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. + +package com.github.google.bumble.btbench + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import java.io.IOException +import java.util.logging.Logger +import kotlin.concurrent.thread + +private val Log = Logger.getLogger("btbench.l2cap-client") + +class L2capClient(private val viewModel: AppViewModel, val bluetoothAdapter: BluetoothAdapter) { + @SuppressLint("MissingPermission") + fun run() { + viewModel.running = true + val remoteDevice = bluetoothAdapter.getRemoteDevice(viewModel.peerBluetoothAddress) + val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm) + + val client = SocketClient(viewModel, socket) + client.run() + } +} \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capServer.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capServer.kt new file mode 100644 index 0000000..79c7004 --- /dev/null +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capServer.kt @@ -0,0 +1,62 @@ +// Copyright 2023 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. + +package com.github.google.bumble.btbench + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.le.AdvertiseCallback +import android.bluetooth.le.AdvertiseData +import android.bluetooth.le.AdvertiseSettings +import android.bluetooth.le.AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY +import android.os.Build +import java.io.IOException +import java.util.logging.Logger +import kotlin.concurrent.thread + +private val Log = Logger.getLogger("btbench.l2cap-server") + +class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdapter: BluetoothAdapter) { + @SuppressLint("MissingPermission") + fun run() { + // Advertise to that the peer can find us and connect. + val callback = object: AdvertiseCallback() { + override fun onStartFailure(errorCode: Int) { + Log.warning("failed to start advertising: $errorCode") + } + + override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { + Log.info("advertising started: $settingsInEffect") + } + } + val advertiseSettingsBuilder = AdvertiseSettings.Builder() + .setAdvertiseMode(ADVERTISE_MODE_LOW_LATENCY) + .setConnectable(true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + advertiseSettingsBuilder.setDiscoverable(true) + } + val advertiseSettings = advertiseSettingsBuilder.build() + val advertiseData = AdvertiseData.Builder().build() + val scanData = AdvertiseData.Builder().setIncludeDeviceName(true).build() + val advertiser = bluetoothAdapter.bluetoothLeAdvertiser + advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) + + val serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel() + viewModel.l2capPsm = serverSocket.psm + Log.info("psm = $serverSocket.psm") + + val server = SocketServer(viewModel, serverSocket) + server.run({ advertiser.stopAdvertising(callback) }) + } +} \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt new file mode 100644 index 0000000..314f746 --- /dev/null +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt @@ -0,0 +1,307 @@ +// Copyright 2023 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. + +package com.github.google.bumble.btbench + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import com.github.google.bumble.btbench.ui.theme.BTBenchTheme +import java.util.logging.Logger + +private val Log = Logger.getLogger("bumble.main-activity") + +const val PEER_BLUETOOTH_ADDRESS_PREF_KEY = "peer_bluetooth_address" +const val SENDER_PACKET_COUNT_PREF_KEY = "sender_packet_count" +const val SENDER_PACKET_SIZE_PREF_KEY = "sender_packet_size" + +class MainActivity : ComponentActivity() { + private val appViewModel = AppViewModel() + private var bluetoothAdapter: BluetoothAdapter? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + appViewModel.loadPreferences(getPreferences(Context.MODE_PRIVATE)) + checkPermissions() + } + + private fun checkPermissions() { + val neededPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + arrayOf( + Manifest.permission.BLUETOOTH_ADVERTISE, + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT + ) + } else { + arrayOf(Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN) + } + val missingPermissions = neededPermissions.filter { + ContextCompat.checkSelfPermission(baseContext, it) != PackageManager.PERMISSION_GRANTED + } + + if (missingPermissions.isEmpty()) { + start() + return + } + + val requestPermissionsLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + permissions.entries.forEach { + Log.info("permission: ${it.key} = ${it.value}") + } + val grantCount = permissions.count { it.value } + if (grantCount == neededPermissions.size) { + // We have all the permissions we need. + start() + } else { + Log.warning("not all permissions granted") + } + } + + requestPermissionsLauncher.launch(missingPermissions.toTypedArray()) + return + } + + @SuppressLint("MissingPermission") + private fun initBluetooth() { + val bluetoothManager = ContextCompat.getSystemService(this, BluetoothManager::class.java) + bluetoothAdapter = bluetoothManager?.adapter + + if (bluetoothAdapter == null) { + Log.warning("no bluetooth adapter") + return + } + + if (!bluetoothAdapter!!.isEnabled) { + Log.warning("bluetooth not enabled") + return + } + } + + private fun start() { + initBluetooth() + setContent { + MainView( + appViewModel, + ::becomeDiscoverable, + ::runRfcommClient, + ::runRfcommServer, + ::runL2capClient, + ::runL2capServer + ) + } + + // Process intent parameters, if any. + intent.getStringExtra("peer-bluetooth-address")?.let { + appViewModel.peerBluetoothAddress = it + } + val packetCount = intent.getIntExtra("packet-count", 0) + if (packetCount > 0) { + appViewModel.senderPacketCount = packetCount + } + appViewModel.updateSenderPacketCountSlider() + val packetSize = intent.getIntExtra("packet-size", 0) + if (packetSize > 0) { + appViewModel.senderPacketSize = packetSize + } + appViewModel.updateSenderPacketSizeSlider() + intent.getStringExtra("autostart")?.let { + when (it) { + "rfcomm-client" -> runRfcommClient() + "rfcomm-server" -> runRfcommServer() + "l2cap-client" -> runL2capClient() + "l2cap-server" -> runL2capServer() + } + } + } + + private fun runRfcommClient() { + val rfcommClient = bluetoothAdapter?.let { RfcommClient(appViewModel, it) } + rfcommClient?.run() + } + + private fun runRfcommServer() { + val rfcommServer = bluetoothAdapter?.let { RfcommServer(appViewModel, it) } + rfcommServer?.run() + } + + private fun runL2capClient() { + val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it) } + l2capClient?.run() + } + + private fun runL2capServer() { + val l2capServer = bluetoothAdapter?.let { L2capServer(appViewModel, it) } + l2capServer?.run() + } + + @SuppressLint("MissingPermission") + fun becomeDiscoverable() { + val discoverableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE) + discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300) + startActivity(discoverableIntent) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun MainView( + appViewModel: AppViewModel, + becomeDiscoverable: () -> Unit, + runRfcommClient: () -> Unit, + runRfcommServer: () -> Unit, + runL2capClient: () -> Unit, + runL2capServer: () -> Unit +) { + BTBenchTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + text = "Bumble Bench", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + Divider() + val keyboardController = LocalSoftwareKeyboardController.current + TextField(label = { + Text(text = "Peer Bluetooth Address") + }, + value = appViewModel.peerBluetoothAddress, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done + ), + onValueChange = { + appViewModel.updatePeerBluetoothAddress(it) + }, + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) + ) + Divider() + TextField(label = { + Text(text = "L2CAP PSM") + }, + value = appViewModel.l2capPsm.toString(), + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + onValueChange = { + if (it.isNotEmpty()) { + val psm = it.toIntOrNull() + if (psm != null) { + appViewModel.l2capPsm = psm + } + } + }, + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })) + Divider() + Slider( + value = appViewModel.senderPacketCountSlider, onValueChange = { + appViewModel.senderPacketCountSlider = it + appViewModel.updateSenderPacketCount() + }, steps = 4 + ) + Text(text = "Packet Count: " + appViewModel.senderPacketCount.toString()) + Divider() + Slider( + value = appViewModel.senderPacketSizeSlider, onValueChange = { + appViewModel.senderPacketSizeSlider = it + appViewModel.updateSenderPacketSize() + }, steps = 4 + ) + Text(text = "Packet Size: " + appViewModel.senderPacketSize.toString()) + Divider() + ActionButton( + text = "Become Discoverable", onClick = becomeDiscoverable, true + ) + Row() { + ActionButton( + text = "RFCOMM Client", onClick = runRfcommClient, !appViewModel.running + ) + ActionButton( + text = "RFCOMM Server", onClick = runRfcommServer, !appViewModel.running + ) + } + Row() { + ActionButton( + text = "L2CAP Client", onClick = runL2capClient, !appViewModel.running + ) + ActionButton( + text = "L2CAP Server", onClick = runL2capServer, !appViewModel.running + ) + } + Divider() + Text( + text = "Packets Sent: ${appViewModel.packetsSent}" + ) + Text( + text = "Packets Received: ${appViewModel.packetsReceived}" + ) + Text( + text = "Throughput: ${appViewModel.throughput}" + ) + Divider() + ActionButton( + text = "Abort", onClick = appViewModel::abort, appViewModel.running + ) + } + } + } +} + +@Composable +fun ActionButton(text: String, onClick: () -> Unit, enabled: Boolean) { + Button(onClick = onClick, enabled = enabled) { + Text(text = text) + } +} \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt new file mode 100644 index 0000000..3a62519 --- /dev/null +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt @@ -0,0 +1,163 @@ +// Copyright 2023 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. + +package com.github.google.bumble.btbench + +import android.content.SharedPreferences +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import java.util.UUID + +val DEFAULT_RFCOMM_UUID = UUID.fromString("0AF17D61-5DAE-4530-BE0D-6A8D54C3608B") +const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF" +const val DEFAULT_SENDER_PACKET_COUNT = 100 +const val DEFAULT_SENDER_PACKET_SIZE = 1024 + +class AppViewModel : ViewModel() { + private var preferences: SharedPreferences? = null + var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS) + var l2capPsm by mutableStateOf(0) + var senderPacketCountSlider by mutableFloatStateOf(0.0F) + var senderPacketSizeSlider by mutableFloatStateOf(0.0F) + var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT) + var senderPacketSize by mutableIntStateOf(DEFAULT_SENDER_PACKET_SIZE) + var packetsSent by mutableIntStateOf(0) + var packetsReceived by mutableIntStateOf(0) + var throughput by mutableIntStateOf(0) + var running by mutableStateOf(false) + var aborter: (() -> Unit)? = null + + fun loadPreferences(preferences: SharedPreferences) { + this.preferences = preferences + + val savedPeerBluetoothAddress = preferences.getString(PEER_BLUETOOTH_ADDRESS_PREF_KEY, null) + if (savedPeerBluetoothAddress != null) { + peerBluetoothAddress = savedPeerBluetoothAddress + } + + val savedSenderPacketCount = preferences.getInt(SENDER_PACKET_COUNT_PREF_KEY, 0) + if (savedSenderPacketCount != 0) { + senderPacketCount = savedSenderPacketCount + } + updateSenderPacketCountSlider() + + val savedSenderPacketSize = preferences.getInt(SENDER_PACKET_SIZE_PREF_KEY, 0) + if (savedSenderPacketSize != 0) { + senderPacketSize = savedSenderPacketSize + } + updateSenderPacketSizeSlider() + } + + fun updatePeerBluetoothAddress(peerBluetoothAddress: String) { + this.peerBluetoothAddress = peerBluetoothAddress + + // Save the address to the preferences + with(preferences!!.edit()) { + putString(PEER_BLUETOOTH_ADDRESS_PREF_KEY, peerBluetoothAddress) + apply() + } + } + + fun updateSenderPacketCountSlider() { + if (senderPacketCount <= 10) { + senderPacketCountSlider = 0.0F + } else if (senderPacketCount <= 50) { + senderPacketCountSlider = 0.2F + } else if (senderPacketCount <= 100) { + senderPacketCountSlider = 0.4F + } else if (senderPacketCount <= 500) { + senderPacketCountSlider = 0.6F + } else if (senderPacketCount <= 1000) { + senderPacketCountSlider = 0.8F + } else { + senderPacketCountSlider = 1.0F + } + + with(preferences!!.edit()) { + putInt(SENDER_PACKET_COUNT_PREF_KEY, senderPacketCount) + apply() + } + } + + fun updateSenderPacketCount() { + if (senderPacketCountSlider < 0.2) { + senderPacketCount = 10 + } else if (senderPacketCountSlider < 0.4) { + senderPacketCount = 50 + } else if (senderPacketCountSlider < 0.6) { + senderPacketCount = 100 + } else if (senderPacketCountSlider < 0.8) { + senderPacketCount = 500 + } else if (senderPacketCountSlider < 1.0) { + senderPacketCount = 1000 + } else { + senderPacketCount = 10000 + } + + with(preferences!!.edit()) { + putInt(SENDER_PACKET_COUNT_PREF_KEY, senderPacketCount) + apply() + } + } + + fun updateSenderPacketSizeSlider() { + if (senderPacketSize <= 1) { + senderPacketSizeSlider = 0.0F + } else if (senderPacketSize <= 256) { + senderPacketSizeSlider = 0.02F + } else if (senderPacketSize <= 512) { + senderPacketSizeSlider = 0.4F + } else if (senderPacketSize <= 1024) { + senderPacketSizeSlider = 0.6F + } else if (senderPacketSize <= 2048) { + senderPacketSizeSlider = 0.8F + } else { + senderPacketSizeSlider = 1.0F + } + + with(preferences!!.edit()) { + putInt(SENDER_PACKET_SIZE_PREF_KEY, senderPacketSize) + apply() + } + } + + fun updateSenderPacketSize() { + if (senderPacketSizeSlider < 0.2) { + senderPacketSize = 1 + } else if (senderPacketSizeSlider < 0.4) { + senderPacketSize = 256 + } else if (senderPacketSizeSlider < 0.6) { + senderPacketSize = 512 + } else if (senderPacketSizeSlider < 0.8) { + senderPacketSize = 1024 + } else if (senderPacketSizeSlider < 1.0) { + senderPacketSize = 2048 + } else { + senderPacketSize = 4096 + } + + with(preferences!!.edit()) { + putInt(SENDER_PACKET_SIZE_PREF_KEY, senderPacketSize) + apply() + } + } + + fun abort() { + aborter?.let { it() } + } +} diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Packet.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Packet.kt new file mode 100644 index 0000000..0fa8500 --- /dev/null +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Packet.kt @@ -0,0 +1,178 @@ +// Copyright 2023 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. + +package com.github.google.bumble.btbench + +import android.bluetooth.BluetoothSocket +import java.io.IOException +import java.nio.ByteBuffer +import java.util.logging.Logger +import kotlin.math.min + +private val Log = Logger.getLogger("btbench.packet") + +fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } + +abstract class Packet(val type: Int, val payload: ByteArray = ByteArray(0)) { + companion object { + const val RESET = 0 + const val SEQUENCE = 1 + const val ACK = 2 + + const val LAST_FLAG = 1 + + fun from(data: ByteArray): Packet { + return when (data[0].toInt()) { + RESET -> ResetPacket() + SEQUENCE -> SequencePacket( + data[1].toInt(), + ByteBuffer.wrap(data, 2, 4).getInt(), + data.sliceArray(6.. AckPacket(data[1].toInt(), ByteBuffer.wrap(data, 2, 4).getInt()) + else -> GenericPacket(data[0].toInt(), data.sliceArray(1.. onResetPacket() + is AckPacket -> onAckPacket() + is SequencePacket -> onSequencePacket(packet) + } + } + + abstract fun onResetPacket() + abstract fun onAckPacket() + abstract fun onSequencePacket(packet: SequencePacket) +} + +interface DataSink { + fun onData(data: ByteArray) +} + +interface PacketIO { + var packetSink: PacketSink? + fun sendPacket(packet: Packet) +} + +class StreamedPacketIO(private val dataSink: DataSink) : PacketIO { + private var bytesNeeded: Int = 0 + private var rxPacket: ByteBuffer? = null + private var rxHeader = ByteBuffer.allocate(2) + + override var packetSink: PacketSink? = null + + fun onData(data: ByteArray) { + var current = data + while (current.isNotEmpty()) { + if (bytesNeeded > 0) { + val chunk = current.sliceArray(0.. Unit +) { + fun receive() { + val buffer = ByteArray(4096) + do { + try { + val bytesRead = socket.inputStream.read(buffer) + if (bytesRead <= 0) { + break + } + onData(buffer.sliceArray(0.. Unit) { + var aborted = false + viewModel.running = true + + fun cleanup() { + serverSocket.close() + viewModel.running = false + onTerminate() + } + + thread(name = "SocketServer") { + while (!aborted) { + viewModel.aborter = { + serverSocket.close() + } + Log.info("waiting for connection...") + val socket = try { + serverSocket.accept() + } catch (error: IOException) { + Log.warning("server socket closed") + cleanup() + return@thread + } + Log.info("got connection") + + viewModel.aborter = { + aborted = true + socket.close() + } + viewModel.peerBluetoothAddress = socket.remoteDevice.address + + val socketDataSink = SocketDataSink(socket) + val streamIO = StreamedPacketIO(socketDataSink) + val socketDataSource = SocketDataSource(socket, streamIO::onData) + val receiver = Receiver(viewModel, streamIO) + socketDataSource.receive() + socket.close() + } + cleanup() + } + } +} \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Color.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Color.kt new file mode 100644 index 0000000..2b538c8 --- /dev/null +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.github.google.bumble.btbench.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Theme.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Theme.kt new file mode 100644 index 0000000..1751579 --- /dev/null +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Theme.kt @@ -0,0 +1,63 @@ +package com.github.google.bumble.btbench.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun BTBenchTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, typography = Typography, content = content + ) +} \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Type.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Type.kt new file mode 100644 index 0000000..029f898 --- /dev/null +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Type.kt @@ -0,0 +1,33 @@ +package com.github.google.bumble.btbench.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + )/* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_background.xml b/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_foreground.xml b/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..7dc413535afb92696346e3ff92f35848ca662eb5 GIT binary patch literal 3562 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 Ozvp>J9-6_`KPdnZJwpco literal 0 HcmV?d00001 diff --git a/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..e8f5332be1768e8c2b301045af8623ea4b909f1b GIT binary patch literal 2378 zcmV-Q3AOf8Nk&FO2><|BMM6+kP&iCA2><{uYrq;1Cc4q~FOr;ZU~y=f@pyV>sqRk9 z%*@Qp%*@Qp%*@P;A*pA&d#?JwCGV=moPlJ?Rsix;iUM0Kke|Z zH=mcDJ>C^!?xVa=RVV53x=zj4O=`Y?>^wb9^+J6@lwZ9uuPkc-VTCA9wwP~Wq~g!U zQD*$Af2QqmQSqepUNfd@e@k)kgyEa1J6$(kd4UPCzf9KXIb+J!_slT(nDHv|O_={- zvc}ICQ?z(!{M27#7UgR%JD3o2-+6MC;xUG|nk*nhX^^t345Rr5PlGJBe1P>Ig_^wC zW9vN(v*fy-7GKrZlx^?W__e$F=S(xhz!R3*(%J0OH(K*Xnk82EvBu?WGYvmz*{y9D ztDLE7>W()UqZWPV4HbA2*@-dSiM45+u@n(m|4Fb#7Z0}X?HH5PdM~)dn%)*)(cgsO zo8YP!E0}HS7PqW=zLH|&keNmvu-=1kOK$9D!iIgH^+WHUe3xRhh+ z&eyDTthQMu>=c~7?-|o{yX?2R)Ac^5$t|zEX3Rc)lmAybTE~=a?)sBOge0#}ok7d4 z5!v{ahZPPs5NsY?`BZf)9j|GoQTx?3mfzdNEi;eVYu!81nk~1xnVE(k;&(D?(GNMH zA|a8TS$o7C<(Zc9Esb3Kl`(Efv5BjHQvRDUQT(dB{F8%>@hb97konD+xW-3~$0;hc z&CiT0#w{%}W>JCmGeZeceyEa_2{guZmASO9Tu7H6s_Qg8Nw=xR0<+81EHw*_R0}!m zWscv0kwA2&0MY~70FGj}JFVv1Z!HKl@8pIm&jK?)RC~~JzI9M{70Y@hiQi$O`~jW- zjlN1Sn{intE_6R*p1~KoxIKxa11EO20%EVIq=*>`JF0h%Mk9@ z=bTqjadEux;E{c=84yEzR@cSrOuDpVHNgLHwvSrX zXDFAL&AOai{EjPUcrWWXxLtbPtL{whZ@*4ANe~eT;vaAqsIDqN8sMvi_Hz|g7L4jw z4bE#Jw>r3Ad|gm?7MHP8v%1^4ULjzFnjA=N8ajY;9zeRawtaPN$K`82sXY^WkKtDh zkrBM9?c{#pb+=lxxvb+Vsv5!WKCn_<+B2aKS4=d&36g?3S^@E>C=H0tka!!?P%x~ z7;+hW+4Kim9}QCxdYu8PqS8XvsjkCVn&$>|F&lnH8)I<5i0g6w@$tfgS>q!c=NmRo zw<0**eAveMx{b>NEI*f3F0tl$Hx zpR{LuZ!^ueFNgJtq2!nfR8teRJ_aPHYfOaP z78+(1^dXl*OaO(2N6O4pKlUEMd$Q~$sEvkR30?P#VqxR8HVhbSqWYO2!iYYSD__%4 z%c1)mVkyKZ6jnaeir&LalwUWGO7DdbTPU99Uektud=mrB^F*YE6_EY8Tj@nbAS zJnEyGgD~ZBNR6&0h>xw3sYUte}I6 zjOKgIFgA}>1xNk>!V3~JM$f%0@~Wd|3pGX$If3TrA0WCV87{p}go zhs$|0jk6v8Ed2J zmHv}Qy;;9-2|e4R zvEEGe6L<}@2f{PAnh4YY?(N^8f9INTf4j4$qoDQz?$5V*noSO2cmw!MeByJzv%?2VZ=r|^n`8n_xSOg^2n70`) w8u)Id`{-6{PG{zJb@I4Cz+oVtRw%p<*hB<80_uIG6&8WwKo20AR=Za<084nKWdHyG literal 0 HcmV?d00001 diff --git a/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..ac1ae9b098c16ff320122e4c0d8ff4964680befb GIT binary patch literal 2748 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 literal 0 HcmV?d00001 diff --git a/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..a2b1c8be4f492079029fe2fb8fb01a38cd25a7d3 GIT binary patch literal 9766 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 literal 0 HcmV?d00001 diff --git a/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..2bbc83fef1c95ed5edccf0b2c2c6096dd8b31257 GIT binary patch literal 13350 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 + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/res/values/ic_launcher_background.xml b/extras/android/BtBench/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/extras/android/BtBench/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/res/values/strings.xml b/extras/android/BtBench/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..018c3f9 --- /dev/null +++ b/extras/android/BtBench/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + BT Bench + \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/res/values/themes.xml b/extras/android/BtBench/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..f0d08db --- /dev/null +++ b/extras/android/BtBench/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +