IanHarvey / bluepy

Python interface to Bluetooth LE on Linux
Other
1.58k stars 490 forks source link

Very low sampling rate when constantly reading device characteristics #468

Closed AydinGokce closed 2 years ago

AydinGokce commented 2 years ago

Hi Everyone,

I have a simple BLE device (ESP32) which has three characteristics, and updates them at a rate of 96 hz. Each characteristic contains a single float value. Just to test this repo, I've made a very simple application which connects to the device, and constantly reads the characteristics and prints them in an infinite loop.

The issue I'm running into is that I'm getting a very low sampling rate, the code prints them at just below 3 hz, which is too low for my application. Ideally I'd be printing at a rate of >10hz

My (relevant) code is as follows below. Assume all imports, uuids and addresses are correctly included

from bluepy import btle

ADDRESS = "SOME-ADDRESS"

GX_SENSORTAG_UUID = "SOME-UUID-1"
GY_SENSORTAG_UUID = "SOME-UUID-2"
GZ_SENSORTAG_UUID = "SOME-UUID-3"

def get_float_from_characteristic(characteristic):
    return characteristic.read()

def main():

    print("Attempting to connect...")
    esp32 = btle.Peripheral(ADDRESS)

    service = esp32.getServiceByUUID("SOME-UUID")

    gx = service.getCharacteristics(GX_SENSORTAG_UUID)[0]
    gy = service.getCharacteristics(GY_SENSORTAG_UUID)[0]
    gz = service.getCharacteristics(GZ_SENSORTAG_UUID)[0]

    while True:
        print(f"X: {get_float_from_characteristic(gx)}, Y: {get_float_from_characteristic(gy)}, Z: {get_float_from_characteristic(gz)}")

if __name__ == "__main__":
    main()

Can anyone spot a bottleneck in my code which would cause such a low sampling rate? Is such a sampling rate even unexpected?

Thank you

ukBaz commented 2 years ago

Having three separate characteristics and reading them is probably the slowest way to do this.

Typically three values that are to be read at the same time would be combined into one characteristic. Each value taking a known number of bytes so the values can be extracted at the other end of the link. Take a look at the GATT Specification Supplement as to how this is done for some of the Bluetooth adopted characteristics. 3.138 Magnetic Flux Density - 3D might be a good example.

The next thing you could do to speed things up is use Indications or Notifications. These are a way for a GATT Client to subscribe to data provided by a GATT Server. A Notification is an unacknowledged message or update while an Indication is an acknowledged message or update. As you are going to be updating so frequently it would seem sensible to go with notifications. This is covered in this libraries documentation at: https://ianharvey.github.io/bluepy-doc/notifications.html

BlueZ, which is the underlying stack used, goes through various OS layers when doing notifications which is known to slow the rate at which data can be updated. To overcome this, in later versions BlueZ introduced AcquireNotify which opens a socket to bypass some of the layers to speed things up. I am not aware that bluepy supports this functionality.

doug-holtsinger commented 2 years ago

You might try running 'sudo btmon' and look at the timestamps to get a rough idea of the bottleneck. On my system (Raspberry Pi + Nordic BLE) the turnaround time from receiving a read response to sending the next request is very short, but the time from sending the read request to receiving the response is in the millisecond range. I get about 5hz throughput. Notifications, as ukBaz mentioned, are about 4x faster on my system compared to reading a characteristic.

AydinGokce commented 2 years ago

@ukBaz I appreciate your response. Your insights were very helpful as this is the first time I am working with BLE in any capacity. I heeded your advice and modified the GATT server to combine the three characteristics into a series of bytes tied to one characteristic, and implemented notifications for it.

I attemped to make a very simple program to check that notifications are being updated, but am now running into an issue where the waitForNotifications() function is not triggering. I cannot receive notifications for whatever reason while closely following the documentation you had linked. I confirmed that the server notifications are functioning properly by using a mobile app designed for the purpose. My code is as follows:

from bluepy import btle

class MyDelegate(btle.DefaultDelegate):
    def __init__(self):
        btle.DefaultDelegate.__init__(self)

    def handleNotification(self, cHandle, data):
        print(data)

# Initialisation  -------
ADDRESS = "SOME-ADDRESS"
CHARACTERISTIC_UUID = "SOME-UUID"
SERVICE_UUID = "SOME-UUID"

p = btle.Peripheral(ADDRESS)
p.setDelegate(MyDelegate())

notif_on = b"\0x1\x00"

svc = p.getServiceByUUID(SERVICE_UUID)
ch = svc.getCharacteristics(CHARACTERISTIC_UUID)[0]
ch.write(notif_on)

while True:
    if p.waitForNotifications(1.0):
        print("notification received")
        # handleNotification() was called
        continue

    print("Waiting...")

The code prints "Waiting..." in an endless loop and does not pick up on any notifications. I'm certain that the address and uuids are correct.

Would you mind checking this over to see if you can spot any implementation issues?

Thanks again!

ukBaz commented 2 years ago

Looking at this quickly, it looks like you are not enabling the notifications. Looks like you are sending notif_on to the characteristic, not the descriptor.

ch = svc.getCharacteristics(CHARACTERISTIC_UUID)[0]
ch.write(notif_on)

The notify descriptor to the characteristic has a specific UUID of 0x2902. I'm not in a position to test this right now but it should be something like:

CCCD_UUID = 0x2902
ch = svc.getCharacteristics(CHARACTERISTIC_UUID)[0]
ch_cccd = ch.getDescriptors(forUUID=CCCD_UUID)[0]
ch_cccd.write(notif_on)
AydinGokce commented 2 years ago

@ukBaz That worked, I can now sample data at 170 hz from my GATT server! Thanks a lot!