zacharyedwardbull / pycycling

A Python package for interacting with Bluetooth Low Energy (BLE) compatible bike trainers, power meters, radars and heart rate monitors
https://pypi.org/project/pycycling/
MIT License
131 stars 25 forks source link

Get data from multiple devices simultaneously #9

Closed shaunmulligan closed 2 years ago

shaunmulligan commented 2 years ago

Thanks for this amazing library! I have been playing around with it to get data from my Garmin HRM and 4iiii Powermeter. Both of them work correctly when alone, but I can't seem to get them to connect and read data simultaneously.

As a test case I tried to get data from two heart rate monitors at the same time, you can see the code below. Is there something I am doing wrong or is this a limitation of pycycling? I see its seems possible with the raw Bleak lib: https://github.com/hbldh/bleak/blob/develop/examples/two_devices.py

import asyncio
from bleak import BleakClient

from pycycling.heart_rate_service import HeartRateService

async def start(address):
    async with BleakClient(address, timeout=10.0) as client:
        print("Starting loop for ", address)
        def my_measurement_handler(data):
            print(data)

        await client.is_connected()
        print("Sensor ", address, " is connected")
        hr_service = HeartRateService(client)
        hr_service.set_hr_measurement_handler(my_measurement_handler)

        await hr_service.enable_hr_measurement_notifications()
        await asyncio.sleep(30.0)
        await hr_service.disable_hr_measurement_notifications()

async def multiple_tasks(addresses):
    hrm1 = asyncio.create_task(start(addresses[0]))
    hrm2 = asyncio.create_task(start(addresses[1]))

    await hrm1
    await hrm2

if __name__ == "__main__":
    import os

    os.environ["PYTHONASYNCIODEBUG"] = str(1)

    device_addresses = ["D9:38:0B:2E:22:DD", "F0:99:19:59:B4:00"]
    asyncio.run(multiple_tasks(device_addresses))

Thanks again!

zacharyedwardbull commented 2 years ago

Hi, it's definitely possible to use multiple Bluetooth devices at once with the library. I wrote the module for a University project: a virtual training environment which used 4 Bluetooth devices simultaneously (2 turbo trainers and 2 Sterzo plates). I've never tested with heart rate monitors, but I expect it would be the same. I've pulled out a bit of the code from my project which shows how I handled the multiple connections:

from bleak import BleakClient
import asyncio

from pycycling.sterzo import Sterzo
from pycycling.tacx_trainer_control import TacxTrainerControl, RoadSurface
...

class VirtualTrainingEnvironment:

   ...

    async def run_simulation(self):
        loop = asyncio.get_running_loop()
        loop.create_task(self.connect_to_turbo(rider_id=0, address=TURBO_1_ADDRESS))
        loop.create_task(self.connect_to_steering(rider_id=0, address=STERZO_1_ADDRESS))
        loop.create_task(self.connect_to_turbo(rider_id=1, address=TURBO_2_ADDRESS))
        loop.create_task(self.connect_to_steering(rider_id=1, address=STERZO_2_ADDRESS))
        ...
        while True:
            await asyncio.gather(
                self.game_tick(),
                asyncio.sleep(self.interval),
            )
    ...

    async def connect_to_turbo(self, rider_id, address):
        async with BleakClient(address, timeout=30.0) as client:
            def my_general_handler(data):
                self.turbo_speed_measurements[rider_id] = data.speed

            def my_specific_handler(data):
                self.turbo_power_measurements[rider_id] = data.instantaneous_power

            self.turbos[rider_id] = TacxTrainerControl(client)
            self.turbos[rider_id].set_specific_trainer_data_page_handler(my_specific_handler)
            self.turbos[rider_id].set_general_fe_data_page_handler(my_general_handler)
            ...
            await asyncio.sleep(300)

    async def connect_to_steering(self, rider_id, address):
        async with BleakClient(address, timeout=30.0) as client:
            def steering_handler(angle):
                self.steering_measurements[rider_id].pop(0)
                self.steering_measurements[rider_id].append(angle / 30)

            await client.is_connected()
            sterzo = Sterzo(client)
            sterzo.set_steering_measurement_callback(steering_handler)
            await sterzo.enable_steering_measurement_notifications()
            ...
            await asyncio.sleep(300)

if __name__ == '__main__':
    import os

    os.environ["PYTHONASYNCIODEBUG"] = str(1)

    vte = VirtualTrainingEnvironment()
    asyncio.run(vte.run_simulation())

I'm afraid I'm not entirely sure why your code snippet isn't working. You could try increasing the timeout a bit and see if that helps.

If you figure out what's going wrong please post the solution here :)

shaunmulligan commented 2 years ago

Hi @zacharyedwardbull thanks for the quick response. I have been looking at some of the Bleak issues and it seems its a limitation of the Bluez linux implementation. Apparently it doesn't work because bleak runs a scan for each client if you pass BleakClient an address rather than a BleDevice object. So the work around seems to be to run the scanner yourself and then pass the actual BleDevices from that scan to the BleakClient constructor. There is an example here https://github.com/hbldh/bleak/issues/630 that works for me. So I will follow that approach and restructure my code. Thanks for the help!

zacharyedwardbull commented 2 years ago

No worries, good luck with getting it to work. I should have mentioned that I only ever tested multiple devices on macOS Catalina and Windows 10.

shaunmulligan commented 2 years ago

Thanks! I got it working now on a linux, the trick was to do the scan separately and pass BLEDevice objects to the BleakClient. Thanks for the help, I will close this for now.