hbldh / bleak

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

GATT connection disconnects when trying to read characteristic value #1691

Open rogerarchibald opened 2 days ago

rogerarchibald commented 2 days ago

Description

First off, I'm mainly an electronics guy and embedded developer so apologies in advance if I'm being ignorant here. I'm trying to develop a basic desktop app to get data from a BLE peripheral. I used the UART service example as a starting point for this. I have 4 characteristics in total: a read/notify status message, a write-only control value, a write-only enable characteristic and a read-only hour meter to count active time. I can write to the write characteristics without issue, but when I try to read or enable notifications on the status characteristic it immediately disconnects. If I watch the BLE packets with a sniffer, the read request doesn't happen: it just drops the connection. Likewise if I try to enable notifications, it just drops out right away.

I can read the hour meter characteristic without issue...I tried rearranging the characteristic declaration on my peripheral so that the status isn't the first characteristic in the GATT table to see if that might be the problem, but I get the same results.

I can read the characteristic with the NRF connect app as well as enable notifications and it seems to work normally. I'd appreciate if anybody might be able to point me towards the error of my ways here.

Here's the python in its entirety:

"""
UART Service
-------------

An example showing how to write a simple program using the Nordic Semiconductor
(nRF) UART service.

"""

import asyncio
import sys
from itertools import count, takewhile
from typing import Iterator

from bleak import BleakClient, BleakScanner
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData

MYDEVICE_SERVICE_UUID =   "00001701-1212-EFDE-1523-845FEDAED383"
MYDEVICE_STATUS_UUID =    "00001702-5212-EFDE-1523-845FEDAED383"
MYDEVICE_HOURMETER_UUID = "00001703-1212-EFDE-1523-845FEDAED383"
MYDEVICE_CONTROL_UUID =   "00001704-1212-EFDE-1523-845FEDAED383"
MYDEVICE_VARIABLESET_UUID =   "00001705-1212-EFDE-1523-845FEDAED383"

async def myDevice_Monitor():
    #attempt at modifying the NUS example to receive my data.
    def match_my_uuid(device: BLEDevice, adv: AdvertisementData):

        if MYDEVICE_SERVICE_UUID.lower() in adv.service_uuids:
            print("Found my UUID!")
            return True
        print("No Luck")
        return False

    device = await BleakScanner.find_device_by_filter(match_my_uuid)

    if device is None:
        print("no matching device found, bummer.")
        sys.exit(1)

    def handle_disconnect(_: BleakClient):
        print("Device was disconnected, goodbye.")
        # cancelling all tasks effectively ends the program
        for task in asyncio.all_tasks():
            task.cancel()

    def handle_connect(_: BleakClient):
        print("Successfully Connected!")

    def handle_status_notifications(_: BleakGATTCharacteristic, data):
        print("received:", data)

    async with BleakClient(device, disconnected_callback=handle_disconnect) as client:
        #await client.start_notify(MYDEVICE_STATUS_UUID, handle_status_notifications)

        myDevice = client.services.get_service(MYDEVICE_SERVICE_UUID)

        for chars in myDevice.characteristics:
            print(chars)
            print(chars.properties)

        await asyncio.sleep(10) #chill for a minute to allow connection parameters to be negotiated.

        print("attempting to set variable")

        await client.write_gatt_char(MYDEVICE_VARIABLESET_UUID, b"\x1e", True)
        await asyncio.sleep(5)
        print("attempting to enable things:")
        await client.write_gatt_char(MYDEVICE_CONTROL_UUID, b"\x01", True)
        await asyncio.sleep(5)
        print("trying to learn to read")
        statusData = bytearray(5)
        statusData = await client.read_gatt_char(MYDEVICE_STATUS_UUID)
        print("status = " + str(statusData))
        while True:
            await asyncio.sleep(10)

if __name__ == "__main__":
    try:
        asyncio.run(myDevice_Monitor())
    except asyncio.CancelledError:
        # task is cancelled on disconnect, so we ignore this error
        pass

Here's the terminal output from the print statements:

No Luck
No Luck
No Luck
Found my UUID!
00001702-1212-efde-1523-845fedaed383 (Handle: 17): Unknown
['read', 'notify']
00001703-1212-efde-1523-845fedaed383 (Handle: 20): Unknown
['read']
00001704-1212-efde-1523-845fedaed383 (Handle: 22): Unknown
['write']
00001705-1212-efde-1523-845fedaed383 (Handle: 24): Unknown
['write']
attempting to set variable
attempting to enable things:
trying to learn to read
Device was disconnected, goodbye.

Screenshot 2024-11-24 at 9 00 28 AM

dlech commented 2 days ago

In the Bluetooth logs, there is one write request that didn't get a write response from the peripheral. So this is likely the reason for the disconnect.

I don't think there is anything we can do about it in Bleak. The peripheral seems broken.

rogerarchibald commented 2 days ago

The peripheral works fine with the NRF Connect app.

The one write request that was sent twice is not common, looking at my logs it is normally written on the first attempt.
If I attempt to read prior to writing (i.e. immediately after the connection is made) the same disconnect problem occurs. In the BLE logs, there is not even an attempt to read: it just automatically disconnects.

Maybe there is something going on in my peripheral, but I can't see it from the sniffer.

rogerarchibald commented 2 days ago

It's also playing nice with simplePyBLE

dlech commented 2 days ago

With simplePyBLE are you doing write with response or write without response? Does the Bluetooth packet log look the same for both?

rogerarchibald commented 2 days ago

I'm not able to do Write Without Response using simplePyBLE...If I've got the peripheral set up as only write-without-response then the simplePyBLE crashes as soon as I try to send a write request: when this happens, I see nothing on the sniffer aside from the disconnect message. With my peripheral in this configuration, I can still write the characteristic OK using the NRF connect app.

When I change it back to write_with_response, everything looks solid.

I think the odd missed packet I was seeing previously was due to my debug setup: I had a Tag-Connect cable connected to my target board so I could print debug statements over SWO and the cable was routed right over my SOC/antenna. When I remove the cable I don't see any more repeat write requests.