bleak: Multiple clients crashes bluetooth stack: Bluetooth: hci0: Opcode 0x200e failed: -16

  • raspberry pi 4B
  • debian bookworm
  • bleak 0.20.2-1 from python3-bleak package
  • bluetoothctl: 5.66

Description

I have a simple python script that has the task to poll a large number of devices (50 to 100). I want to optimize the process by not serializing all connects + polls + disconnects, see below.

The result is that the bluetooth stack on my rpi crashes. Clients are no longer disconnecting and scanning never terminates.

kernel logs show:

[  193.752537] Bluetooth: hci0: Opcode 0x200e failed: -16
[  205.529410] Bluetooth: hci0: Opcode 0x200e failed: -16
[  210.648622] Bluetooth: hci0: Opcode 0x200e failed: -16

Not able to power off/on as well:

Also tested with an external USB bluetooth dongle, same results.

[Biro 07]# power off
[Biro 07]# power on
Failed to set power on: org.bluez.Error.Busy

What I Did

, the procedure I have now is:

while true:
  start scanning 
  scan for 30 seconds, collect devices in callback
  stop scanning
  for 1 minute:
    pick the oldest polled device
    await connect
    await read data
    await disconnect

This works pretty much ok but is rather slow as there is only one active client connection at a time.

I understand that doing multiple connects in parallel is no recommended (yes I have looked at the two-devices example), but having multiple connections established should be fine. This is what I changed my code to now, effectively this serializes the connect()s, but allows the polling and disconnect to run async:

while true:
  start scanning 
  scan for 30 seconds, collect devices in callback
  stop scanning
  for 1 minute:
    pick the oldest polled device
    await connect
    create async process that does:
       await read_data
       await disconnect

As far as I know, this should abide to all the rules:

  • do not connect while scanning
  • do not perform connects in parallel

I also tried to postpone the scanning until all the active clients have disconnecetd, but to no avail.

Minimal workable example is not easy since there is a large number of specific devices involved.

Logs

BLEAK_LOGGING=1 log attached (sorry for the newlines, this was copy/pasted from a scrollback buffer)

debug-log.txt

Code

Below is the code as I run it now, albeit stripped a bit from error handling and the protocol details that are not relevant:

#!/usr/bin/python

from bleak import BleakScanner, BleakClient
import logging
import contextlib
import traceback
import os
import asyncio
import signal
import sys
import json
import logging
import time
import struct
from time import sleep

SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb"
CHAR_HANDLE = "0000ffe1-0000-1000-8000-00805f9b34fb"

class JK:

    def __init__(self, scanner, dev, address, name):
        self.scanner = scanner
        self.dev = dev
        self.client = BleakClient(self.dev)
        self.address = address
        self.name = name
        self.t_last_poll = time.time() - 60 * 10

    async def reg_wr(self, add, vals: bytearray, length: int):
        # write something to device
        pass

    def ncallback(self, sender: int, data: bytearray):
        # handle data
        pass

    async def read(self):
        await self.client.start_notify(CHAR_HANDLE, self.ncallback)
        await self.reg_wr(COMMAND_DEVICE_INFO, b"\0\0\0\0", 0x00)
        self.poll_ready = False
        while not self.poll_ready:
            await asyncio.sleep(0.1)
        await self.client.disconnect()
        self.scanner.decref()

    async def poll(self):
        self.scanner.incref()
        client = self.client
        await client.connect()
    
        # Awaiting works fine:
        # await self.read()
    
        # But running in an async tast not:
        asyncio.create_task(self.read())


class JKviewer:

    def __init__(self):
        self.devices = {}
        self.npolling = 0

    def incref(self):
        self.npolling += 1

    def decref(self):
        self.npolling -= 1


    async def poll(self):
            
        # Find the jk with the oldest t_last_poll
        jk = None
        t_oldest = t_now
        for address in self.devices.keys():
            jk2 = self.devices[address]
            if jk2.t_last_poll < t_oldest:
                t_oldest = jk2.t_last_poll
                jk = jk2

        if jk == None:
            print("nothing to do")
            return
        self.busy = jk.name

        # Poll oldest JK
        async def aux():
            jk.t_last_poll = time.time()
            await jk.poll()
            self.busy = None
        asyncio.create_task(aux())


    async def start(self):

        def on_scan(dev, data):
            if SERVICE_UUID in data.service_uuids:
                if dev.address not in self.devices:
                    jk = JK(self, dev, dev.address, dev.name)
                    self.devices[dev.address] = jk
                if dev.address in self.devices:
                    self.devices[dev.address].t_last_seen = time.time()

        while True:
            # scan for a few seconds
            print("scanning")
            async with BleakScanner(on_scan) as scanner:
                await asyncio.sleep(3)
            # work the devices
            print("working")
            for i in range(20):
                for i in range(10):
                    await self.poll()
                    await asyncio.sleep(0.1)
            # wait for all the connections to be gone
            print("waiting")
            while self.npolling > 0:
                await asyncio.sleep(0.5)


jkviewer = JKviewer()
asyncio.run(jkviewer.start())

# vi: ft=python ts=4 sw=4 et

About this issue

  • Original URL
  • State: open
  • Created 5 months ago
  • Comments: 18 (8 by maintainers)

Most upvoted comments

I’m not sure what causes the limitation, but the “two clients” examples jumps through hoops to make sure only one connect() is running at a time:

https://github.com/hbldh/bleak/blob/develop/examples/two_devices.py#L40-L41