hbldh / bleak

A cross platform Bluetooth Low Energy Client for Python using asyncio
MIT License
1.74k stars 292 forks source link

Can't get notifications from two heart rate sensors at the same time #630

Closed koenvervloesem closed 2 years ago

koenvervloesem commented 3 years ago

Description

I'm trying to adapt my heart rate monitor script from https://github.com/hbldh/bleak/issues/613#issuecomment-907613664 to read the Heart Rate Measurement characteristic from multiple BLE devices at the same time. I looked at the two_devices.py example for the approach with asyncio.gather.

I've tested this with two devices (the PineTime with InfiniTime firmware and a Xanes F15). The script is able to read the heart rate for each of the devices individually. But when I run my adapted script on both devices at the same time, only one of them sends its notification messages.

What I Did

The code:

import asyncio
import functools
import sys

import bleak

DEVICE_NAME_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0x2A00)
HEART_RATE_MEASUREMENT_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0x2A37)

def heart_rate_changed(device_name: str, handle: int, data: bytearray):
    print(f"{device_name}: {data[1]} bpm")

async def connect(address):
    try:
        async with bleak.BleakClient(address) as client:
            device_name = (await client.read_gatt_char(DEVICE_NAME_UUID)).decode()
            print(f"Connected to {device_name}")
            await client.start_notify(HEART_RATE_MEASUREMENT_UUID, functools.partial(heart_rate_changed, device_name))
            print(f"Start notifications...")
            while True:
                await asyncio.sleep(1)

    except asyncio.exceptions.TimeoutError:
        print(f"Can't connect to device {address}. Does it run a GATT server?")

async def main(addresses):
    await asyncio.gather(*(connect(address) for address in addresses))

if __name__ == "__main__":

    if len(sys.argv) >= 2:
        addresses = sys.argv[1:]
        asyncio.run(main(addresses))
    else:
        print("Please specify at least one BLE MAC address on the command line.")

This uses asyncio.run as suggested in https://github.com/hbldh/bleak/issues/613#issuecomment-907640439 to disconnect correctly after an unhandled exception. The two_devices.py example uses loop.run_until_complete, but even if I use that approach the script only connects to one device.

Running the script for only the PineTime works:

$ python3 heart-rate.py F3:BE:3E:97:17:A4
Connected to InfiniTime                             
Start notifications...                              
InfiniTime: 53 bpm                                                                                       
InfiniTime: 30 bpm                                  
InfiniTime: 52 bpm                                                                                       
InfiniTime: 45 bpm

Running the script for only the Xanes F15 works:

$ python3 heart-rate.py EB:76:55:B9:56:18
Connected to F15
F15: 70 bpm
Start notifications...
F15: 70 bpm
F15: 70 bpm
F15: 70 bpm

Running the script for both devices only shows the notification messages from one (I made sure to wait long enough):

$ python3 heart-rate.py EB:76:55:B9:56:18 F3:BE:3E:97:17:A4
Connected to InfiniTime
Start notifications...
InfiniTime: 54 bpm
InfiniTime: 45 bpm
InfiniTime: 72 bpm
InfiniTime: 57 bpm

I've tried this a couple of times and I switched the arguments, but it always seems to pick the device listed as the second argument.

dlech commented 3 years ago

There are a number of duplicate issues about this already. The recommendation is to use BleakScanner to find the devices first, then connect when the scanning is done.

koenvervloesem commented 2 years ago

Ok, I solved this as you suggested:

import asyncio
import functools
import sys

from bleak import BleakClient, BleakScanner

DEVICE_NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb"
HEART_RATE_MEASUREMENT_UUID = "00002a37-0000-1000-8000-00805f9b34fb"

addresses = []
heart_rate_sensors = []

def device_found(device, _):
    if device.address in addresses:
        heart_rate_sensors.append(device)
        print(f"Found device {device.name}")

def heart_rate_changed(
    device_name: str, handle: int, data: bytearray
):
    print(f"{device_name}: {data[1]} bpm")

async def connect(device):
    try:
        async with BleakClient(device) as client:
            device_name = (
                await client.read_gatt_char(DEVICE_NAME_UUID)
            ).decode()
            print(f"Connected to {device_name}")
            await client.start_notify(
                HEART_RATE_MEASUREMENT_UUID,
                functools.partial(heart_rate_changed, device_name),
            )
            print(f"Start notifications for {device_name}...")
            while True:
                await asyncio.sleep(1)

    except asyncio.exceptions.TimeoutError:
        print(
            f"Can't connect to device {device.address}. Does it run a GATT server?"
        )

async def main():
    scanner = BleakScanner()
    scanner.register_detection_callback(device_found)
    await scanner.start()
    await asyncio.sleep(5.0)
    await scanner.stop()
    await asyncio.gather(
        *(connect(device) for device in heart_rate_sensors)
    )

if __name__ == "__main__":

    if len(sys.argv) >= 2:
        addresses = sys.argv[1:]
        asyncio.run(main())
    else:
        print(
            "Please specify at least one Bluetooth address on the command line."
        )

And this works:

$ python3 heart-rate.py EB:76:55:B9:56:18 F3:BE:3E:97:17:A4
Found device F15
Found device InfiniTime
Connected to F15
Start notifications for F15...
Connected to InfiniTime
Start notifications for InfiniTime...
F15: 71 bpm
F15: 71 bpm
InfiniTime: 46 bpm
F15: 71 bpm
F15: 70 bpm
F15: 70 bpm
F15: 70 bpm
F15: 70 bpm
F15: 69 bpm
F15: 69 bpm
F15: 68 bpm
InfiniTime: 49 bpm
F15: 68 bpm
F15: 67 bpm