hbldh / bleak

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

Can't shut down notification properly. #254

Closed kswann-imb closed 3 years ago

kswann-imb commented 4 years ago

Description

I'm trying to write a bleak script that will do the following:

  1. Connect to a BLE heart rate monitor, and turn on notification, and then stay alive until the program is terminated.
  2. Attempt to reconnect if the connection is lost continuously until the program is terminated.
  3. Terminate on shutdown signal from Ctrl+C

What I Did

I can connect to the device without issue and turn on notification, but I'm getting stuck at the shutdown on Ctrl+C and the reconnection. In both cases, the program seems to hang when attempting to shutdown notification.

HEART_RATE_CHARACTERISTIC_UUID = (
    "00002a37-0000-1000-8000-00805f9b34fb" # UUID of heart rate characteristic
)

class DisconnectionException(Exception):
    """Raised when the device has disconnected."""

class ShutdownException(Exception):
    """Raised when the program should shutdown."""

def ask_exit():
    for task in asyncio.Task.all_tasks():
        task.cancel()

async def stay_connected(device_address: str, loop: asyncio.AbstractEventLoop):
    while True:
        print("Starting Loop")
        try:
            print("Connecting to device.")
            await connect_and_record(device_address=device_address, loop=loop)
        except DisconnectionException as e:
            print(e)
        except ShutdownException as e:
            break
        except Exception as e:
            print(e)
            pass
        print("End of Loop Iteration")

    loop.stop()

async def connect_and_record(device_address: str, loop: asyncio.AbstractEventLoop):
    async with BleakClient(device_address, loop=loop) as client:
        def disconnect_callback(client, future):
            raise DisconnectionException("Client with address {} got disconnected!".format(client.address))

        client.set_disconnected_callback(disconnect_callback)

        print("Connected: {0}".format(await client.is_connected()))
        print("Starting notification.")
        await client.start_notify(HEART_RATE_CHARACTERISTIC_UUID, heart_rate_data_handler)
        while True:
            try:
                await asyncio.sleep(1)
            except asyncio.CancelledError:
                print("Shutdown Request Received")
                break

        print("Shutting down notification.")
        await client.stop_notify(HEART_RATE_CHARACTERISTIC_UUID)
        print("Done shutting down notification.")
        print("Shutting Down.")
        raise ShutdownException

async def run(loop: asyncio.AbstractEventLoop):

    print("Attempting to connect to device and start recording.")
    await stay_connected(device_address=client_device_config.heart_rate_sensor_address, loop=loop)
    loop.stop()

def main():

    loop = asyncio.get_event_loop()

    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.add_signal_handler(sig, ask_exit)

    asyncio.ensure_future(run(loop))
    loop.run_forever()

    # I had to manually remove the handlers to
    # avoid an exception on BaseEventLoop.__del__
    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.remove_signal_handler(sig)

    loop.close()
    print("Done.")

if __name__ == '__main__':
    main()

I suspect this isn't as much an issue as operator error, but any help would be appreciated. I think this would be a really useful example to add to the examples directory, as many use cases would involve turning on notification and leaving it running and needing reconnects.

Thanks!

hbldh commented 4 years ago

Hmm. I will look into this after the 0.8.0 release. I agree that an example regarding this would be a good idea.

GilShoshan94 commented 4 years ago

@kswann-imb Hi, I took a look at your code, I am relatively new to asyncio code so I might be wrong.

I rewrote your code but did not try it since I don't have your device. Try it. I hope it will help you.

import asyncio
import signal

from bleak import BleakClient

HEART_RATE_CHARACTERISTIC_UUID = "00002a37-0000-1000-8000-00805f9b34fb"  # UUID of heart rate characteristic
ADDRESS = "YOUR BLE ADDRESS"
exit_flag = False

def heart_rate_data_handler(sender, data):
    print(str(data))  # Do stuff

class DisconnectionException(Exception):
    """Raised when the device has disconnected."""

class ShutdownException(Exception):
    """Raised when the program should shutdown."""

def ask_exit():
    global exit_flag
    print("Shutdown Request Received")
    exit_flag = True

async def stay_connected(device_address: str, timeout: float = 4.0):
    global exit_flag
    exit_flag = False
    print("Starting Loop")
    client = BleakClient(address=device_address, timeout=timeout)
    try:
        print("Connecting to device.")
        await client.connect()
        await notify_and_record(client)
    except DisconnectionException as e:
        print(e)
    except ShutdownException as e:
        print(e)
    except Exception as e:
        print(e)
        pass

    print("Shutting down notification.")
    await client.stop_notify(HEART_RATE_CHARACTERISTIC_UUID)
    print("Done shutting down notification.")

    print("Disconnecting to device.")
    await client.disconnect()
    print("End of Loop Iteration")

async def notify_and_record(client):
    global exit_flag

    def disconnect_callback(client, future):
        raise DisconnectionException("Client with address {} got disconnected!".format(client.address))

    client.set_disconnected_callback(disconnect_callback)

    print("Connected: {0}".format(await client.is_connected()))
    print("Starting notification.")
    await client.start_notify(HEART_RATE_CHARACTERISTIC_UUID, heart_rate_data_handler)
    while not exit_flag:
        await asyncio.sleep(1)

    print("Shutting Down.")
    raise ShutdownException

async def run():

    print("Attempting to connect to device and start recording.")
    await stay_connected(device_address=ADDRESS)

def main():

    loop = asyncio.get_event_loop()

    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.add_signal_handler(sig, ask_exit)

    loop.run_until_complete(run())

    # I had to manually remove the handlers to
    # avoid an exception on BaseEventLoop.__del__
    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.remove_signal_handler(sig)

    loop.stop()
    loop.close()
    print("Done.")

if __name__ == "__main__":
    main()
kswann-imb commented 4 years ago

@GilShoshan94 thanks for taking the time to edit and make those suggestions. I'll give it a try and let you know how it goes!

kswann-imb commented 4 years ago

Have a quick update on this one. I didn't use this solution exactly, but I did use a few of the suggestions including global variables, and not stopping the loop in the routine as described.

There was a couple complications that I thought would be useful to share.

For BLE heart rate monitors, they turn on automatically when they touch the skin, and also turn off automatically after a period of no contact with the skin.

What was happening to me was, the sensor would lose contact with the skin, and after a period of time it would shut off. At that point, it was not possible to shut down notification, because the connection was already closed. This is why I was getting a hang. Similarly, when shutting down with Ctrl+C if the device was already shut off, you also can't disconnect from the client because it's already disconnected. So I had to check these conditions before making those calls in conjunction with the advice above in order to get the desired behaviour.

In hindsight both of these things seem incredibly obvious 🤷

Thanks again for the help!

hbldh commented 3 years ago

Will try to incorporate more complex examples when working on #266. Will close this issue in the meantime.

kswann-imb commented 3 years ago

Thanks @hbldh. Overall though great package. I searched for a long time before finding something that worked well for BLE.

skylark1991 commented 3 years ago

@kswann-imb I am having similar issues with my heart rate sensor (Polar H10), would you kindly share your solution with us please?