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.
Update: There is a current pull request to add a new Android backend to Bleak for using BeeWare.
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:
- Deprecated parts of Bleak have not been replicated in bleekWare
- bleekWare primarily serves me in my own small projects. Functionality that I don't require (e.g. bonding) is likely not implemented. However, pull requests to extend the functionality of bleekWare are welcome
- bleekWare is work in progress
- bleekWare is made for Android apps made with BeeWare and requires Chaquopy to access the Android API
- bleekWare requires
128-bitUUID strings to address services and characteristics Callback functions (likedetection_callbackornotify_callback)in bleekWare can't be asynchronous
The current set-up procedure requires some manual intervention:
-
Set up a virtual environment to start a new BeeWare project as described in the BeeWare tutorial
-
Install bleekWare from GitHub via pip:
python -m pip install git+https://github.com/MarkusPiotrowski/bleekWare -
Write and test some code for a desktop computer (Linux, Mac or Window) using Bleak to access Bluetooth LE
-
Before setting up an Android project, copy the following lines into the
tool.briefcase.app.YOURPROJECT.androidsection of yourpyproject.tomlfile, e.g. below thebuild_gradle_dependencies:build_gradle_extra_content = "android.defaultConfig.python.staticProxy('bleekWare.Scanner', '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" /> """ -
Also add bleekWare as an installation requirement for Android in the
pyproject.tomlfile:[tool.briefcase.app.YOURPROJECT.android] requires = [ "toga-android......", "git+https://github.com/MarkusPiotrowski/bleekWare", ] -
If your code runs fine on your desktop platform, set up an Android project as described in the BeeWare tutorial
-
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()