A limited Bleak complement for accessing Bluetooth LE on Android in Python apps made with BeeWare.
bleekWare (a portmanteau of "Bleak" and "BeeWare") is a limited complement for Bleak to access Bluetooth LE on Android devices from Python apps made with BeeWare.
Bleak, the 'Bluetooth Low Energy platform Agnostic Klient', allows using Python to access Bluetooth LE cross-platform, but it's existing platform backend for Android requires python-for-android (P4A). This can be used e.g. in Kivy but not in BeeWare, because BeeWare uses Chaquopy as bridging tool between Python and Android.
For discussion if the existing Android backend of Bleak can be modified for using it in BeeWare or to add another Android backend to Bleak, read here, here and here.
bleekWare makes use of Chaquopy to access the native Android's Bluetooth LE APIs. bleekWare is 'usage compatible' to Bleak, meaning that it's methods have the same names and return (mostly) the same data as Bleak. Thus, using platform-dependent import switches, the same code can run on Linux, Mac and Windows using Bleak or on Android using bleekWare. However, bleekWare is not dependent on Bleak; if your Python app should only run on Android you don't need to install or import Bleak in addition to bleekWare.
bleekWare is a limited complement for Bleak. Not all functions are covered:
detection_callback
or notify_callback
)in bleekWare can't be asynchronousThe current set-up procedure requires some manual intervention and puts the bleekWare code next to your app code. This may change in the future.
Before setting up an Android project, copy the following lines into the tool.briefcase.app.bluetooth.android
section of your pyproject.toml
file, e.g. below the
build_gradle_dependencies
:
build_gradle_extra_content = "android.defaultConfig.python.staticProxy('your_project.bleekWare.Scanner', 'your_project.bleekWare.Client')"
android_manifest_extra_content = """
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
"""
Replace your_project
with the actual folder name of your project.
Now, download the bleekWare module as zip file and place the unzipped bleekWare subfolder in your apps's folder:
beeware_venv/
|- your_project/
|- build/
|- logs/
|- src/
| |- your_project/
| | |- bleekWare/ <---
| | | |- __init__.py
| | | |- Client.py
| | | |- Scanner.py
| | |- resources/
| | |- __init__.py
| | |- __main__.py
| | |- app.py
| |- your_project.dist-info/
|- tests/
...
Note: If you use the 'Download ZIP' option from the <> Code
button above, the download will contain the whole repository. Copy only the bleekWare subfolder to your project.
If your application should run cross-platform and you require both Bleak and bleekWare, you may want to use conditional imports like this:
import toga
if toga.platform.current_platform == 'android':
from .bleekWare import bleekWareError as BLEError
from .bleekWare.Client import Client as Client
from .bleekWare.Scanner import Scanner as Scanner
else:
from bleak import BleakError as BLEError
from bleak import BleakClient as Client
from bleak import BleakScanner as Scanner
...
See android_ble_scanner.py
in the Example folder for different BLE scanning examples. The code is tested to run on Windows (using Bleak) and Android devices (using bleekWare). It should be running on Mac and Linux as well (again using Bleak), but this hasn't been tested.
Connecting to a BLE device and reading from or writing to it's characteristics is dependent on the device's capabilities; thus providing a general working example app isn't possible. But here is an outline for connecting to a BLE device and reading from a notifying service:
"""
Connect to and read notifications from a BLE device
"""
import asyncio
import toga
from toga.style import Pack
from toga.style.pack import COLUMN
from toga import Button, MultilineTextInput
if toga.platform.current_platform == 'android':
from .bleekWare import bleekWareError as BLEError
from .bleekWare.Client import Client as Client
from .bleekWare.Scanner import Scanner as Scanner
else:
from bleak import BleakError as BLEError
from bleak import BleakScanner as Scanner
from bleak import BleakClient as Client
# Put here a notifying characteristic of your device:
NOTIFY_UUID = '0000fff1-0000-1000-8000-00805f9b34fb'
# Possibly not available or different UUID:
BATTERY_UUID = '00002a19-0000-1000-8000-00805f9b34fb'
class bleekWareExample(toga.App):
def startup(self):
"""Build a little GUI."""
self.scan_button = Button(
'Scan for BLE devices', on_press=self.search_device
)
self.message_box = MultilineTextInput(
readonly=True,
style=Pack(padding=(10, 5), height=200),
)
self.data_box = MultilineTextInput(
readonly=True,
style=Pack(padding=(10, 5), height=200),
)
box = toga.Box(
children=[
self.scan_button,
self.message_box,
self.data_box
],
style=Pack(direction=COLUMN)
)
self.main_window = toga.MainWindow(title='bleekWare Example')
self.main_window.content = box
self.main_window.show()
async def search_device(self, widget):
"""Search for BLE device by name."""
device = None
self.message_box.value = 'Start BLE scan...\n'
try:
# Replace the name of your device here:
device = await Scanner.find_device_by_name('your_device_by_name')
# Alternatively, you may want to find your device by it's
# MAC address or UUID (on Mac):
# device = await Scanner.find_device_by_address('AA:BB:CC:DD:EE:FF'):
except (OSError, BLEError) as e:
self.message_box.value += (
f'Bluetooth not available. Error: {str(e)}\n'
)
if device:
self.message_box.value += 'Found it!\n'
await self.connect_to_device(device)
else:
self.message_box.value += "Sorry, couldn't find it...\n"
async def connect_to_device(self, device):
"""Connect to BLE device."""
self.message_box.value += 'Connecting to device...\n'
async with Client(device, self.disconnect_callback) as client:
self.message_box.value += (
f'Client is connected: {client.is_connected}\n'
)
# You device probably hasn't a battery level characteristic
battery_level = await client.read_gatt_char(BATTERY_UUID) # if available
self.message_box.value += (
'Battery level: '
f'{int.from_bytes(battery_level, "big")}%\n'
)
await client.start_notify(
NOTIFY_UUID, self.show_data
)
await asyncio.Future()
def disconnect_callback(self, client):
"""Handle disconnection."""
self.message_box.value += f'DEVICE WAS DISCONNECTED from {client}\n'
def show_data(self, char, data):
"""Show notifications."""
self.data_box.value += str(data.hex()) + '\n'
def main():
return bleekWareExample()