If you want to get started with Bluetooth BLE automation, start by trying to read your phone's battery level. It's a simple project that doesn't take long. In this post, we'll use this idea to test out the Python Bluetooth framework Bleak. Bleak appears to be the most active Bluetooth library for Python. I've never used it before writing this post. Let's see how it goes!
This post is the first installment of a series of posts I'm putting together about Python and Bleak. If you're want to try out something a little more advanced, you might want to check out the second post here.
Edit: This post has been revised to reflect changes made in the most recent Bleak release.
Installation
Starting with the ever useful pip, all we have to do is execute the following commands.
pip3 install bleak
pip3 install asyncio
Notice, I snuck the asyncio library in there. Maybe you are familiar with asyncio. Bleak seems to rely on it heavily. Better make sure you have it up to date.
BLE Hello World
The Bleak documentation recommends running the following code as a first step.
import asyncio
from bleak import BleakScanner
async def main():
devices = await BleakScanner.discover(5.0, return_adv=True)
for d in devices:
print(d)
asyncio.run(main())
This will look for devices for 5 seconds and then return the advertisement information as data in the result. After running, I could see the following output.
08C44024-5542-6B4B-B7A3-1C520C31A6FD
056E316C-35DD-3F6D-05DE-354EB1B94594
6112371A-E5EE-FF29-DBE5-F0EA2AC5228F
2C52EE24-D7ED-8236-FF9E-24DE465FD743
3F2197D1-9B45-6106-2875-B4ED744CB816
511F0382-C863-2F42-851D-8356EB908DC8
8081A33E-0981-FCC0-8D48-39918D0571C9
CFD01867-3CC4-739C-58A9-848092FF27BB
1811E039-DF31-8390-33C2-53CE79900BE7
8A191A11-2971-0A14-5DB7-0EA02990FA4E
CC02E119-FA8C-AF00-6BF6-9547CC7E0084
BCC17B40-C826-FF50-1DAB-4429EE3AF60C
0685B765-D92D-E8F7-DF15-BDABDA72D7B5
F2ECFBCE-C7BA-F411-E1E6-59E8D11C43EB
D1F7A962-F3C8-D47F-FE26-4129E3DE3640
549BF970-023E-7E2D-83AF-FB81D97B1A27
C9E749B9-D36B-00DC-42E6-430FACE904A3
B506D8A3-D11A-F99C-E5B6-6D2B0BC5842F
27F9CCE6-FBB7-8A02-76BC-41F47259357B
E1340ADE-F856-DF57-5676-C5984EC346E4
2984CD75-A91B-77C5-E83F-7161D12B9F18
17BD67AF-6488-411A-7F36-F50094F2D415
70DE4DE1-55D2-44D5-43D4-230DE4A5F3CC
19AA3728-87BE-F3D3-98AE-A864AA6D10E1
8AD30606-D1EB-AC34-D8D8-850D207FA461
5EF63A60-B293-37C4-B84F-E626B1060088
B2CB0069-15C8-F622-105B-FC10028D4CA1
7633554A-55DF-3B4D-0974-ADAAA6BD1883
32913F6D-1855-AE9E-DFF6-63577E7B6252
FB2314A7-1B5D-740C-0664-1923139F2C6C
810D553D-079F-05E8-0619-8BA464D1F89E
D649067E-5870-3683-F2FB-36E0D5B630A0
34887F21-B297-C6C3-E76D-E3D8E7AFE992
1C2E5934-1C8D-0D34-D84E-CA5F862140C3
Looks good! But it doesn't tell us a lot.
Wavecake makes this easier
If you're here to automate a Bluetooth process using Python, now's the time to point out that you could be using Wavecake instead. I built Wavecake to make it easier to share Bluetooth automations amongst teams. you can think of it as an IDE with some nice extras. It's in a free Beta release. I'm actively seeking feedback. Please feel free to give it a shot and pass along your two cents!
Wavecake Demo (no sign-in requried)
Preparing the phone
Now we need to prepare the phone. The preparation will be different for different phones. I'll be setting this up on an iPhone. I recommend using the nRF Connect app. You can find it for Android and iPhone in the app stores.
First, navigate to the Peripheral tab. Then, add an advertiser.
This is what it should look like. Since the iPhone already advertises battery level by default, we don't need to enable the service specifically. We are just using the app to make the iPhone advertisements public.
Return to the peripheral screen. The peripheral tab should look like this:
Last, select the switch to enable advertising the battery service.
Finding the phone
At this point, your phone is advertising a battery service. Using Bleak, you can find the phone and connect.
The Bleak library returns a device object from the discover routine. I want to explore this object so that I can implement a filter. This will help me find the exact device I'm looking for. Bluetooth devices are everywhere. Returning all the devices would be overwhelming.
If you print(devices) you can see the gory details of the larger object. I'll keep this tutorial a little lighter. The devices object returns each device as a dictionary. The key of the dictionary is the Bluetooth device's UID on your system. The value of the device key is an array. The second item of the array contains the adverisement data.
It's confusing to type out, but the code is straightforward. Print the advertisement data by changing your code.
import asyncio
from bleak import BleakScanner
async def main():
devices = await BleakScanner.discover(5.0, return_adv=True)
for d in devices:
print(devices[d][1])
asyncio.run(main())
Here's the output.
AdvertisementData(local_name='Wa', manufacturer_data={89: b'U\x01'}, service_data={'0000feaa-0000-1000-8000-00805f9b34fb': b'\x10\x00\x00zephyrproject\x08'}, service_uuids=['0000180d-0000-1000-8000-00805f9b34fb', '0000180f-0000-1000-8000-00805f9b34fb', '00001805-0000-1000-8000-00805f9b34fb', '0000feaa-0000-1000-8000-00805f9b34fb', '8d53dc1d-1db7-4cd3-868b-8a527460aa84'], rssi=-40)
AdvertisementData(manufacturer_data={76: b'\x12\x02\x00\x00'}, rssi=-68)
AdvertisementData(manufacturer_data={76: b'\x0f\x05\x90\x00X\xf4\x0b\x10\x02!\x04'}, tx_power=6, rssi=-70)
AdvertisementData(manufacturer_data={76: b'\x02\x15\xe2\xc5m\xb5\xdf\xfbH\xd2\xb0`\xd0\xf5\xa7\x10\x96\xe0opi\x0c\xa1'}, rssi=-80)
AdvertisementData(local_name='Frost depth sensor', rssi=-75)
AdvertisementData(manufacturer_data={76: b'\x0c\x0e\x00}2\xf7\x16\xc7V\xb1\xe0\xfc{\xca\xda<\x10\x050\x1c\xba\xfdy'}, rssi=-78)
AdvertisementData(manufacturer_data={6: b'\x01\t &\xb2Mgq\xba\xe6\xc8?D\xc9\n\xfb+6\x18Dq\x99\x14_\xfe\x87\xe1'}, rssi=-77)
AdvertisementData(manufacturer_data={76: b'\x10\x078\x1f\x96(9\xeeh'}, tx_power=12, rssi=-77)
AdvertisementData(manufacturer_data={76: b'\x07\x19\x01$ \x0bU\x8f\x11\x00\t![j\xfa\x87\xb4\x834}\x0c\xf3\x0e\xf9\xc6\x11&'}, rssi=-73)
AdvertisementData(local_name='[TV] Samsung 8 Series (82)', manufacturer_data={117: b'B\x04\x01\x01op*\xd5X\xf1\xdfr*\xd5X\xf1\xde\x01\xbb\x0bB\x00\x00\x00'}, rssi=-66)
AdvertisementData(manufacturer_data={6: b'\x01\t "\'\n?R\xc8\\\xff\xf0\xa5L4\xa0\xaf\x01\xe4_6\xd5k`\xa8\xaa\xe6'}, rssi=-54)
AdvertisementData(manufacturer_data={6: b'\x01\t \x02_^\xa1+\xef]\x13\x18\xb0\x87\xf9L\xb4\x15\xe2\x93\xa7\x11\xed8BE\xa9'}, rssi=-76)
AdvertisementData(manufacturer_data={76: b'\t\x08\x13\x82\n\n\x10\xb7\x1bX\x16\x08\x00LCT\x9e\xb9\xb8<'}, rssi=-73)
AdvertisementData(manufacturer_data={76: b'\x12\x029\x01'}, rssi=-71)
AdvertisementData(manufacturer_data={76: b'\x10\x07{\x1f\\b\\D8'}, tx_power=12, rssi=-50)
AdvertisementData(manufacturer_data={76: b'\t\x08\x13\x97\xc0\xa8\x08\xb8\x1bX\x13\x08\nF\xf0~\xfc\xfc\xf1\x00'}, rssi=-71)
AdvertisementData(manufacturer_data={76: b'\x12\x02\x00\x01'}, rssi=-69)
AdvertisementData(manufacturer_data={76: b'\x10\x07u\x1fK\x14\x12\x9f\xe8'}, tx_power=12, rssi=-75)
AdvertisementData(manufacturer_data={6: b'\x01\t \x02j\xdf\xc96)R\xd0\xd1\xc4M\x84\xe4\xe1\xbb\xb8\xee\xa0\xfbh+p\xe4V'}, rssi=-79)
AdvertisementData(manufacturer_data={76: b'\x10\x06\x0b\x19\xb77ax'}, tx_power=12, rssi=-76)
AdvertisementData(local_name='IOTWFB5C', manufacturer_data={23040: b'R#a%@\x0f\x00\x00\x00cd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, service_data={'00005a00-0000-1000-8000-00805f9b34fb': b'R\x06\xb4\xe8B];\\\x00b\xc2\x04\x01\x00'}, rssi=-72)
AdvertisementData(manufacturer_data={76: b'\x10\x06B\x1d\xba\x9e\xcf8'}, tx_power=12, rssi=-79)
AdvertisementData(manufacturer_data={76: b'\x02\x15Pv\\\xb7\xd9\xeaN!\x99\xa4\xfa\x87\x96\x13\xa4\x92\xe7\xc0\xc9\x16\xce'}, rssi=-72)
AdvertisementData(manufacturer_data={76: b'\x10\x07q\x1f>\xa9\xea\x12\x18'}, tx_power=12, rssi=-69)
AdvertisementData(manufacturer_data={76: b'\x10\x06B\x1d\xba\x9e\xcf8'}, tx_power=12, rssi=-73)
AdvertisementData(manufacturer_data={76: b'\x10\x06z\x1e\xa1mvd'}, tx_power=12, rssi=-75)
AdvertisementData(manufacturer_data={76: b'\x12\x02\x00\x01'}, rssi=-75)
AdvertisementData(manufacturer_data={76: b'\x10\x05\x13\x18\x0b\xd6\xec'}, tx_power=12, rssi=-71)
AdvertisementData(manufacturer_data={6: b'\x01\t &\xc2\x05t\xd7\x9a\x1c&\xc3|\xae\x13i\xa0\xea\xef\x85\xa9\xa2\x88s\xa2\xd0_'}, rssi=-78)
AdvertisementData(manufacturer_data={301: b'\x02\x00\x01\x10\x191w\x93PRK|\xb6"\xf2\x80\xd3\xf5i\xb5L\xa158\xc55'}, rssi=-79)
AdvertisementData(local_name='JBL Flip 5', manufacturer_data={87: b'1\x1f\x01<\x00\x00'}, service_data={'0000fddf-0000-1000-8000-00805f9b34fb': b''}, rssi=-77)
AdvertisementData(manufacturer_data={6: b'\x01\x0f "h\x8e\xacU{\xeeb\x0b\x0fk\x91M#\xc6E\xcc\x03\xe0]\xbbm\xd6\n'}, rssi=-79)
AdvertisementData(manufacturer_data={76: b'\x10\x06:\x1a\x95\x98@\x14'}, tx_power=12, rssi=-81)
AdvertisementData(manufacturer_data={23042: b'R#a\x01P\x0f\x00\x00\x00dd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, rssi=-79)
AdvertisementData(manufacturer_data={76: b'\x07\x19\x01\x0f +f\x8f\x01\x00\t\xa2\x9d\xabo\xcb\xeb\x0b-\xc0\xac\xbegR\xb6}:'}, rssi=-77)
AdvertisementData(local_name='231M001641016271', service_uuids=['0000fef1-0000-1000-8000-00805f9b34fb'], tx_power=3, rssi=-79)
AdvertisementData(manufacturer_data={76: b'\x12\x02\x00\x00'}, rssi=-71)
AdvertisementData(manufacturer_data={76: b'\x10\x07;\x1fL\xc8\xc7\xb4\x18\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00'}, tx_power=8, rssi=-69)
AdvertisementData(manufacturer_data={76: b'\x16\x08\x00\xbfi\x9f"\xad\xab\x1a'}, rssi=-70)
AdvertisementData(manufacturer_data={76: b'\x10\x06H\x1d\xa8\xb2\x0e('}, tx_power=12, rssi=-78)
From the output I see that to complete this quickly, I can look for my phone by name.
The AdvertisementData object is another dictionary. We can compare against the local_name key to find the device we are looking for
import asyncio
from bleak import BleakScanner
async def main():
devices = await BleakScanner.discover(5.0, return_adv=True)
for d in devices:
if(devices[d][1].local_name == 'iPhone'):
print("Found it")
asyncio.run(main())
Here's the output.
Found it
Huzzah! It worked!
Connecting to the phone
Now we're ready to connect to the phone. I'll add code to show the available services as well. Add a simple async function to the code.
import asyncio
from bleak import BleakScanner, BleakClient
async def main():
myDevice = ''
devices = await BleakScanner.discover(5.0, return_adv=True)
for d in devices:
if(devices[d][1].local_name == 'iPhone'):
print("Found it")
myDevice = d
address = myDevice
async with BleakClient(address) as client:
svcs = client.services
print("Services:")
for service in svcs:
print(service)
asyncio.run(main())
Here's the output.
Found it
Services:
0000180a-0000-1000-8000-00805f9b34fb (Handle: 10): Device Information
0000180f-0000-1000-8000-00805f9b34fb (Handle: 15): Battery Service
00001805-0000-1000-8000-00805f9b34fb (Handle: 19): Current Time Service
d0611e78-bbb4-4591-a5f8-487910ae4366 (Handle: 35): Apple Continuity Service
9fa480e0-4967-4542-9390-d343dc5d04ae (Handle: 40): Apple Nearby Service
Just like that! Battery service UUID 180F displays as available.
The Bluetooth spec defines the battery level characteristic UUID to be 2A19. Use the read characteristic function to read the battery level.
import asyncio
from bleak import BleakScanner, BleakClient
from PyObjCTools import KeyValueCoding
uuid_battery_level_characteristic = '00002a19-0000-1000-8000-00805f9b34fb'
async def main():
devices = await BleakScanner.discover()
for d in devices:
if KeyValueCoding.getKey(d.details,'name') == 'awesomecoolphone':
myDevice = d
print('Found it')
address = str(KeyValueCoding.getKey(myDevice.details,'identifier'))
async with BleakClient(address) as client:
svcs = await client.get_services()
battery_level = await client.read_gatt_char(uuid_battery_level_characteristic)
print(int.from_bytes(battery_level))
asyncio.run(main())
The output.
Battery level: 42 %
Sweet! We did it.
Conclusion
Hopefully, you can use this as a starting point to build from. Scripting gives you unlimited options to improve your Bluetooth development process.
If you're want to keep going, check out the second installment of this Bleak tutorial here.
If you've made it to the end of this tutorial and find the prospect a little bit daunting, you should check out Wavecake. The tool includes a library and environment. It can help you share your work with your team and break down friction involved in automation for embedded devices.