Skip to content

Conversation

@TristanOpbroek
Copy link
Contributor

@TristanOpbroek TristanOpbroek commented Nov 6, 2025

I ran into a problem using the BlueZ backend whereas when notifying on a characteristic and triggering a read, the data from the response comes back twice. Once as a read, and once as a notification. This occurs in instances where a peripheral sends a notification which requires a reread.

I wrote this test script to show the error:

DEVICE_NAME = "DEVICE_NAME_HERE"
CHAR_UUID = "CHAR_UUID_HERE"

def is_notification(data):
    return len(data) == 1 and data[0] == 0xAA

async def run_ble_test():
    print(f"Scanning for {DEVICE_NAME}")
    devices = await BleakScanner.discover(timeout=5.0)
    target = None
    for d in devices:
        if d.name == DEVICE_NAME:
            target = d
            break

    if target is None:
        print(f"Device '{DEVICE_NAME}' not found.")
        return

    async with BleakClient(target.address) as client:
        print(f"Connected to {target.name} ({target.address})")

        async def notification_handler(sender: str, data: bytearray):
            print(f"Notification from {sender}: {data.hex()}")
            # If notification, trigger re-read
            if is_notification(data):
                new_data = await client.read_gatt_char(CHAR_UUID)
                print(f"Notification re-read triggered: {new_data.hex()}")

        await client.start_notify(CHAR_UUID, notification_handler)

        print("Listening for notifications (10 seconds)...")
        await asyncio.sleep(10.0)

        print("Stopping notifications...")
        await client.stop_notify(CHAR_UUID)


def test_bleak():
    asyncio.run(run_ble_test())


if __name__ == "__main__":
    test_bleak()

On MacOS with a peripheral I can trigger notifications on, I get a single notification, and then single re-read (which is expected):

Listening for notifications (10 seconds)...
Notification from <CHAR_UUID_HERE> (Handle: 56): Unknown: aa
Notification re-read triggered: <DATA>
Stopping notifications...

On my Linux machine however, I receive the data from the characteristics in both the re-read, and as an additional notification:

Listening for notifications (10 seconds)...
Notification from <CHAR_UUID_HERE> (Handle: 56): Unknown: aa
Notification from <CHAR_UUID_HERE> (Handle: 56): Unknown: <DATA>
Notification re-read triggered: <DATA>
Stopping notifications...

This change adds a 'notification_discriminator' argument to Bleak's start_notify method to suppress notifications which don't pass the discriminator.

@dlech
Copy link
Collaborator

dlech commented Nov 6, 2025

Interesting... I'm surprised this hasn't come up before since we have much more Linux users that Mac and this is basically the same issue we recently solved on Mac.

For the BlueZ case though, I think we can solve it a better way by using AcquireNotify. We do know that not all devices correctly report "notify" and "indicate" flags though. So the logic would have to go like:

If d-bus characteristic object has property "NotifyAcquired" then use "AcquireNotify" else use "Value" property change.

This way, it would just work as expected without a need to write a discriminator.

On the other hand, since users would have to write a discriminator for Mac anyway, maybe it isn't worth the extra complexity? On the other, other hand, since Linux is much more popular, it might be worth it still since many users only care about running on Linux.

@dlech
Copy link
Collaborator

dlech commented Nov 6, 2025

if is_notification(data):

Ah, I see you built the discriminator into the notification callback instead of using the one in CBStartNotifyArgs. So if this works on Mac but doesn't work on Linux, I'm not sure how adding a notification discriminator on Linux would work. It should be doing the exact same thing.

@dlech
Copy link
Collaborator

dlech commented Nov 6, 2025

It should be doing the exact same thing.

Oh right, in the CoreBluetooth backend, we assume that if there is a pending read request then the changed value must be the response to the read request and don't send the notification in that case. However, that isn't 100% foolproof without a discriminator since a notification could be received after the read request but before the read response.

@TristanOpbroek
Copy link
Contributor Author

I've updated it to optionally use ActiveNotify if the characteristic presents it and removed the discriminator. This is definitely a better solution. Let me know what feedback you have on the changes!

Thanks!

Copy link
Collaborator

@dlech dlech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for updating!

I made a few quick comments, then I would like to try it out when I have a chance.

@TristanOpbroek
Copy link
Contributor Author

Hi @dlech, do you have any additional thoughts on the changes above?

Thanks!

@dlech
Copy link
Collaborator

dlech commented Nov 22, 2025

I added one more tweak to add some logging the the stop_notify() function and did some more testing. I like the way this turned out! Let's hope it don't break in unexpected ways! 🤞

@dlech dlech merged commit 520bed7 into hbldh:develop Nov 22, 2025
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants