bleak icon indicating copy to clipboard operation
bleak copied to clipboard

Intermittent connection failures / le-connection-abort-by-local when managing multiple BLE peripherals from multiple processes or high-notification load on Linux

Open cysko7927 opened this issue 1 month ago • 1 comments

Summary

I am experiencing intermittent and reproducible BLE connection failures on device with Linux when using Bleak with multiple BLE33 peripherals (IMU sensors). The behavior depends on:

  • the number of peripherals,
  • the sampling/notification rate,
  • whether multiple processes access the same adapter simultaneously.

Key symptoms:

  • On a Raspberry Pi 4 (Raspbian):
    A single script connecting and sampling at a frequency of 10 Hz to 2 IMUs works fine.
    Launching a second script connecting to 2 additional IMUs causes all connections in the second script to fail immediately with le-connection-abort-by-local. In general, the other two IMUs struggle to connect even if I make several attempts. Rarely, only one of these IMUs manages to connect, but only after several attempts.

    Launching a single script connecting and sampling always at a frequency of 10 Hz to 4 IMUs works fine.

  • On a desktop laptop (Linux Mint):
    One script connecting to 6 IMUs disconnects randomly depending on frequency of sampling:

    • 10 Hz → 2 IMUs disconnect after a few seconds
    • 2 Hz → 1 IMU disconnects at random
    • 3 IMUs @ 10 Hz → stable

I’m opening this issue to understand whether these behaviors are expected, whether Bleak has known limitations with many concurrent devices or multi-process scenarios, and whether Bleak could expose recommended practices or safer defaults.


Environment PC Desktop (system info)

  • Bleak version: 1.0.1
  • Python version: 3.10.12
  • BlueZ version: 5.64
  • Kernel: #88~22.04.1-Ubuntu
  • Adapter info:
hci0:	Type: Primary  Bus: USB
	BD Address: 00:45:E2:BD:1D:78  ACL MTU: 1021:6  SCO MTU: 255:12
	UP RUNNING 
	RX bytes:2502 acl:0 sco:0 events:234 errors:0
	TX bytes:36824 acl:0 sco:0 commands:234 errors:0
	Features: 0xff 0xff 0xff 0xfe 0xdb 0xfd 0x7b 0x87
	Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3 
	Link policy: RSWITCH HOLD SNIFF PARK 
	Link mode: PERIPHERAL ACCEPT 
	Name: 'host1'
	Class: 0x7c010c
	Service Classes: Rendering, Capturing, Object Transfer, Audio, Telephony
	Device Class: Computer, Laptop
	HCI Version: 5.1 (0xa)  Revision: 0x19b7
	LMP Version: 5.1 (0xa)  Subversion: 0x6d7d
	Manufacturer: Realtek Semiconductor Corporation (93)
  • Tested on:
    • Raspberry Pi 3 (Raspbian)
    • Desktop PC (Linux Mint)

Reproduction steps (high level)

Scenario A — Raspberry Pi (multi-process failure)

  1. Start Script A using Bleak, connecting to IMU1 + IMU2, then starting sampling at 10 Hz.
  2. Start Script B (same script duplicated) connecting to IMU3 + IMU4.
  3. Expected: both scripts connect successfully.
  4. Observed:
    • Script B fails to connect to any IMU.
    • BlueZ reports le-connection-abort-by-local.
    • bluetoothctl briefly shows Connected: yes followed by immediate disconnection.

Scenario B — Desktop Linux Mint (single process, multiple IMUs)

  1. Script connects to 6 IMUs using Bleak.
  2. Subscribes to notifications at different sampling rates.
  3. Results:
    • 10 Hz per IMU → after a few seconds, two IMUs disconnect randomly.
    • 2 Hz per IMUone IMU disconnects at random.
    • 3 IMUs @ 10 Hzstable.

This appears to correlate with the total aggregate notification rate and/or connection scheduling load in BlueZ but i'm not sure.


Observed errors

From bluetoothctl:

Attempting to connect to 0E:FC:50:D2:37:3B
[CHG] Device 0E:FC:50:D2:37:3B Connected: yes
Failed to connect: org.bluez.Error.Failed le-connection-abort-by-local
[CHG] Device 0E:FC:50:D2:37:3B Connected: no
Device 0E:FC:50:D2:37:3B not available

From journalctl -u bluetooth:

src/battery.c:btd_battery_update() error updating battery: percentage is not valid
No matching connection for device

Diagnostics performed

  • Confirmed devices are advertising normally.
  • Verified manual connect/pair; BlueZ still aborts the connection.
  • Restarted Bluetooth: sudo systemctl restart bluetooth.
  • Removed BlueZ cache: /var/lib/bluetooth/<adapter>/*.
  • Confirmed adapter UP with hciconfig.
  • Tested on two systems (Pi + desktop) with similar results.
  • Varied notification rate:
    • 6 IMUs @ 10 Hz → random disconnections (two IMU)
    • 6 IMUs @ 2 Hz → random disconnection (one IMU)
  • Multi-process competition consistently causes immediate connection aborts on the Raspberry Pi and on PC Desktop.

These results suggest an interaction between Bleak’s DBus usage and BlueZ’s connection scheduling / controller limits.


My thoughts:

Errors seem to occur when:

  • Multiple processes attempt to access the same BlueZ adapter simultaneously (two scripts on Pi).

  • High aggregate notification throughput + many simultaneous connections cause random disconnections.

  • Bleak opens DBus connections and performs Device1.Connect() / GATT operations. It may be important to know whether Bleak performs adapter-level operations that are risky when multiple processes are present, or whether I should use a particular concurrency model.

I am unsure whether these issues are:

  • purely BlueZ/controller/hardware limitations (predictable), or

  • caused in part by the way Bleak handles DBus connections, timeouts, or per-client handling.

Questions

  1. Are there known limits or recommended patterns for using Bleak with many concurrent BLE connections?

  2. Does Bleak support or recommend a single-process “connection manager” approach?
    Multiple processes seem to trigger BlueZ aborting connections.

  3. Are there recommended Bleak APIs or settings for stabilizing many-notification setups?

  4. Is it expected that BlueZ aborts connections when multiple processes simultaneously use the adapter?

  5. Does Bleak have a way to serialise or stagger connections in order to reduce the load on the controller?


I’d appreciate guidance whether this is expected (BlueZ/controller limit) or if I'm doing something wrong. I'll paste the code of the script I used for testing here as well.

import asyncio
from bleak import BleakClient, BleakScanner
import paho.mqtt.publish as publish
import logging
import time
import struct
import sys
from datetime import datetime

# Replace with your device name or address
MQTT_BROKER             = "*****************"
MQTT_PORT               = ****
MQTT_TOPIC              = "aaac/campaign/imu"

## BLE RELATED SETTINGS
# UUID of the characteristic sending notifications (from Arduino sketch)
ACK_UUID				=   "555A0002-0010-467A-9538-01F0652C74E8"
#
RAWSENSOR_UUID	  		=   "555A0002-0030-467A-9538-01F0652C74E8"
QUAT_UUID		   		=   "555A0002-0031-467A-9538-01F0652C74E8"
TIMESTAMP_CHAR_UUID  	=   "555A0002-0034-467A-9538-01F0652C74E8"
#
CAMPAIGN_UUID	   		=   "555A0002-0035-467A-9538-01F0652C74E8"
ACTIVITY_UUID	   		=   "555A0002-0040-467A-9538-01F0652C74E8"  # start and stop

BATTERY_SRV_UID			=	"0000180F-0000-1000-8000-00805F9B34FB" # "180F"
BATTERY_LEVEL_UID		=	"00002A19-0000-1000-8000-00805F9B34FB" # "2A19"

START_CAMPAIGN	  		=  	0x0
STOP_CAMPAIGN	   		=  	0xF
ACK_OK			  		=  	0x00
CAMPAIGN_RAW			=  	0x0
CAMPAIGN_QUAT	   		=  	0xF

MAX_WAIT                = 3
WAIT_ON_DISCONNECT		= 15

## SENSOR SPECIFIC SETTINGS

NUM_SENSORS             = 3
NUM_READINGS_PER_SENSOR = 3
NUM_READINGS            = NUM_SENSORS * NUM_READINGS_PER_SENSOR
SENSOR_PRECISION        = 2 # bytes
COUNTER_PRECISION       = 2 # bytes 0 - 65535
TIMESTAMP_SIZE          = 4 
SENSOR_PACKET_TEMPLATE  = ["ax", "ay", "az", "gx", "gy", "gz", "mx", "my", "mz", "nth", "ts"]
SENSOR_PACKET_LENGTH    = SENSOR_PRECISION * NUM_READINGS + COUNTER_PRECISION + TIMESTAMP_SIZE 
SENSOR_PACKET_DICT      = {"ax": 0x0000, "ay": 0x0000, "az": 0x0000, 
                           "gx": 0x0000, "gy": 0x0000, "gz": 0x0000, 
                           "mx": 0x0000, "my": 0x0000, "mz": 0x0000, 
                           "nth": 0x0000, "ts": 0x00000000}
BATTERY_LEVEL_DICT      = {"battery": 0x00, "ts": 0x00000000}

LOOP_DELAY              = 30       # every 30 secs

# expected input parameters
CAMPAIGN_TEMPLATE       = {"name": "", "init_counter": 0, "sampling_frequency": 10, "raw": True}

## GLOBAL VARIABLES

### collected data from BLE readings
sensor_reading_payload  = {}
sensor_battery_payload  = {}

### IMUs 
available_imus          = set()
connected_imus          = {}
connected_clients       = {}
ready_imus              = set()
missing_imus			= {}

### flag to start BLE data collection
startALL = True

## closing everything
stop_event = asyncio.Event()

def match_device(device, adv_data):
	#with lock:
	return adv_data.local_name.startswith('BLE') and device.address not in available_imus

def match_known_name(device, adv_data):
	return device.name in BSN_IMUS and device.address not in available_imus

# Notification callback
def notification_handler(client):
    def notification_payload_handler(sender, data):
        packlen = len(data)
        imu = connected_imus[client.address][0]
        rs_payload = sensor_reading_payload[imu] #SENSOR_PACKET_DICT.copy()
        if(packlen < SENSOR_PACKET_LENGTH):    # xiao nRF52840 - messages are split to have max 24 bytes 
            devtype = "xiao"
            # initial part of the transmission
            if (data[19] * 256 + data[18]) != rs_payload["nth"]:  # rs_payload["nth"]
                for i in range (0, NUM_READINGS):
                    rs_payload[SENSOR_PACKET_TEMPLATE[i]] = (data[i*2+1] * 256 + data[i*2])
                rs_payload["nth"] = (data[NUM_READINGS*2+1] * 256 + data[NUM_READINGS*2])
            # second part of the message, automatically created, overlapping the remaining 8 bytes that
            # did not fit in the first half of the message
            else:   # timestamp, only 8 bytes
                sensorts = 0
                for i in range(0, TIMESTAMP_SIZE):
                   sensorts = sensorts + data[i] * (256**i)
                rs_payload["ts"] = sensorts
#                    sensor_reading_payload[imu] = rs_payload
                csv_data_d = ",".join(f"{k}=0x{v:x}" for k, v in rs_payload.items())
                csv_data_d += "," + "imuid=" + imu
                csv_data_d += "," + "cname=" + CAMPAIGN_TEMPLATE["name"]
        else:    # nano 33 BLE - 28 byte messages
            devtype = "nano33BLE"
            for i in range (0, NUM_READINGS):
                rs_payload[SENSOR_PACKET_TEMPLATE[i]] = (data[i*2+1] * 256 + data[i*2])    # Little-Endian
            rs_payload["nth"] = (data[NUM_READINGS*2+1] * 256 + data[NUM_READINGS*2])
            # timestamp 
            sensorts = 0
            j = 0
            for i in range(NUM_READINGS*2+2, NUM_READINGS*2+2+TIMESTAMP_SIZE):
                sensorts = sensorts + data[i] * (256**j)
                j += 1
            rs_payload["ts"] = sensorts
#                sensor_reading_payload[imu] = rs_payload
            csv_data_d = ",".join(f"{k}=0x{v:04x}" for k, v in rs_payload.items())
            csv_data_d += "," + "imuid=" + imu
            csv_data_d += "," + "cname=" + CAMPAIGN_TEMPLATE["name"]
        try:
            #publish.single(topic=MQTT_TOPIC, payload=csv_data_d, hostname=MQTT_BROKER, port=MQTT_PORT)
            print(f"{imu}: {csv_data_d[:40]}")
        except Exception as e:
            print(f"notification_payload_handler {devtype}: {e} | {data}")
    return notification_payload_handler

#battery ... do I need a separate one?
def notification_handler_battery(client):
	def notification_battery_handler(sender, data):
		try:
			imus = connected_imus[client.address]
			imu = imus[0]
			batval = (data[1] * 256 + data[0])
            # global variable updated at every reading
			batteryLevel_hex = hex(batval)
			csv_data = "battery=" + batteryLevel_hex
			sensor_battery_payload[imu] = batval
			csv_data += "," + "imuid=" + imu
			csv_data += "," + "cname=" + CAMPAIGN_TEMPLATE["name"]	 
			publish.single(topic=MQTT_TOPIC, payload=csv_data, hostname=MQTT_BROKER, port=MQTT_PORT)
			print(f"{imu}: {csv_data}")
		except Exception as e:
			imu = connected_imus[client.address][0]
			print(f"notification_battery_handler ({imu}): {e} | {data}")
	return notification_battery_handler

# Handling the disconnection
def disconnected_callback(client):
    imu_addr = client.address
    if imu_addr in connected_imus:
        imu_name = connected_imus.get(imu_addr)
        missing_imus[imu_addr] = [imu_addr]
        print(f"{imu_name} unexpectedly disconnected")
        asyncio.create_task(handle_unexpected_disconnection(client))

## Already connected
async def send_timestamp(client):
	timestamp = int(time.time())
	value = struct.pack(">i", timestamp) 
	print("timestamp", TIMESTAMP_CHAR_UUID, value)
	await client.write_gatt_char(TIMESTAMP_CHAR_UUID, value)
	print(f"Sent timestamp: {timestamp}")

## Already connected
async def send_campaign_parameters(client):
	co = CAMPAIGN_TEMPLATE["init_counter"]
	fr = CAMPAIGN_TEMPLATE["sampling_frequency"]
	if CAMPAIGN_TEMPLATE["raw"]:
		ra = CAMPAIGN_RAW
	else:
		ra = CAMPAIGN_QUAT
	data = bytearray(co.to_bytes(2, byteorder='little') + bytes([fr, ra]))
	print("configuration", CAMPAIGN_UUID, data)
	await client.write_gatt_char(CAMPAIGN_UUID, data, len(data))
	print(f"Sent campaign parameters: {CAMPAIGN_TEMPLATE}")

async def send_start_new(client):
	data = bytearray(co.to_bytes(1, byteorder='little') + bytes([START_CAMPAIGN]))
	print("start ", ACTIVITY_UUID, data)
	await client.write_gatt_char(ACTIVITY_UUID, data, len(data))
	print(f"Sent START")

## Already connected
async def read_ack(client):
	response = await client.read_gatt_char(ACK_UUID)
	if (response[0] == ACK_OK):
		print("IMU is ready to start")
		state = True
	else:
		state = False
	return state

async def send_start(client):
	data = bytearray([START_CAMPAIGN])
	print("start", ACTIVITY_UUID, data)
	await client.write_gatt_char(ACTIVITY_UUID, data)
	print(f"Sent START")

# maybe connected, maybe not
async def send_stop(client, imu):
	data = bytearray([STOP_CAMPAIGN])
	if client:
		await client.write_gatt_char(ACTIVITY_UUID, data)
		print(f"Sent STOP")
	else:
		async with BleakClient(imu) as clientlocal:
			await clientlocal.write_gatt_char(ACTIVITY_UUID, data)
			print(f"Sent STOP")

async def setup_all_connections():
	print("Scanning for IMUs ", BSN_IMUS)
   ## waits for all imus to show up and be available
	num_imus = len(BSN_IMUS)
	num_imus_ok = 0
	tot_waiting = 0 
	while num_imus > num_imus_ok:
		devices = await BleakScanner.discover(timeout=5.0)
		for dev in devices:
			if dev.name and dev.name in BSN_IMUS and not dev.name in connected_imus:
				print(f"Found {dev.name}:", end=" ")
				available_imus.add(dev.address)
                ## CB 4 disconnect
				client = BleakClient(dev.address, disconnected_callback=disconnected_callback)
				try:
					print("trying to connect")
					await client.connect()
					if client.is_connected:
						print(f"Connected to {dev.name}")
						connected_clients[dev.address] = client
						num_imus_ok += 1
						connected_imus[dev.address] = [dev.name]
					else:
						print(f"Impossible to connect to {dev.name}")
				except Exception as e:
					print(f"connect_all: Failed to connect to {dev.name}: {e}")
		if tot_waiting < MAX_WAIT and num_imus_ok < num_imus: 
			tot_waiting += 1
			#wait a moment
			print(f"Waiting 2 sec before retrying for the {num_imus - num_imus_ok} IMU(s)")
			await asyncio.sleep(2)
		elif num_imus_ok < num_imus:
			print(f"Missing {num_imus - num_imus_ok} IMUs. Retry later. Exiting")
			return False
	print("All expected IMUs are alive")
	return True

async def setup_all_campaigns():
	num_imus_ok = 0
	for imu_addr, client in connected_clients.items():
		try:
			# moved later
			# await client.start_notify(RAWSENSOR_UUID, notification_handler(client))
			# print("Subscribed to ", RAWSENSOR_UUID, " for IMU ", connected_imus[imu_addr])
			await send_timestamp(client)
			print("Timestamp")
			await send_campaign_parameters(client)
			print("Configuration")
			response = await client.read_gatt_char(ACK_UUID)
			if response:
				print("ACK received")
				ready_imus.add(imu_addr)
				num_imus_ok += 1
		except:
			print(f"Failed connecting to {connected_imus[imu_addr]}")
	result = (num_imus_ok == len(BSN_IMUS))
	return result

async def send_STOP_to_all_imus():
	## send stop to all, as in parallel as possible, it is anyhow sequential
    startALL = False
    print("Stopping all IMUs")
    tasks = [asyncio.create_task(send_stop_to_imu(client, imu_addr)) for imu_addr, client in connected_clients.items()]
    await asyncio.gather(*tasks)

async def send_stop_to_imu(client, imu_addr):
	data = bytearray([STOP_CAMPAIGN])
	try:
		await client.write_gatt_char(ACTIVITY_UUID, data)
		print(f"Sent STOP to {connected_imus[imu_addr]} ({imu_addr})")
	except Exception as e:
		print(f"Failed to send STOP to {connected_imus[imu_addr]} ({imu_addr}): {e}")

async def send_START_to_all_imus():
	## send start to all, as in parallel as possible, it is anyhow sequential
	tasks = [asyncio.create_task(send_start_to_imu(client, imu_addr)) for imu_addr, client in connected_clients.items()]
	await asyncio.gather(*tasks)

async def send_start_to_imu(client, imu_addr):
	data = bytearray([START_CAMPAIGN])
	try:
		await client.write_gatt_char(ACTIVITY_UUID, data)
		print(f"Sent START to {connected_imus[imu_addr]} ({imu_addr})")
	except Exception as e:
		print(f"Failed to send START to {connected_imus[imu_addr]} ({imu_addr}): {e}")
		startALL = False

async def collect_data_from_all_imus():
	try:
#		print("[collect_data_from_all_imus]")
		tasks = [asyncio.create_task(collect_data_from_imu(client, imu_addr)) for imu_addr, client in connected_clients.items()]
		await asyncio.gather(*tasks)
	except:
		print("Problem during data collection")
		# not stopping, hoping to reconnect without restarting
		# await stop_imus()

async def collect_data_from_imu(client, imu_addr):
	try:
		#prepare payload
#		print("[collect_data_from_imu]", imu_addr)
		imu = connected_imus[imu_addr][0]
		# make a copy of the payload dictionary to have all fields set for each imu
		# it is there prepared
		sensor_reading_payload[imu] = SENSOR_PACKET_DICT.copy()
		await client.start_notify(RAWSENSOR_UUID, notification_handler(client))
		print(f"Subscribed to [{connected_imus[imu_addr]}] ", RAWSENSOR_UUID)
		try: ## if the battery is not avaiable, still collecting the rest
			# battery
			await client.start_notify(BATTERY_LEVEL_UID, notification_handler_battery(client))
			print(f"included battery level] ", BATTERY_LEVEL_UID)
		except:
			print(f"collect_data_from_imu({imu_addr}) not sending battery data, only sensor")
        # Keep listening
		while True:
			await asyncio.sleep(LOOP_DELAY)
	except Exception as e:
		print(f"collect_data_from_imu({imu_addr}) Error: {e}")

async def collect_data_from_one_imu(client, imu_addr):
	try:
		tasks = [asyncio.create_task(collect_data_from_imu(client, imu_addr))]
		await asyncio.gather(*tasks)
	except Exception as e:
		print(f"collect_data_from_one_imu: Problem during data collection: {e}")


async def disconnect_imus():
    for imu_addr, client in connected_clients.items():
        imu_name = connected_imus.get(imu_addr)
        del connected_imus[imu_addr]
        await client.disconnect()
        print(f"Successfully disconnected from {imu_name}")  

async def stop_imus():
    for imu_addr, client in connected_clients.items():
        imu_name = connected_imus.get(imu_addr)
        await client.stop_notify(RAWSENSOR_UUID)
        ready_imus.remove(imu_addr)
        print("Successfully stopped {imu_name}")	

## Try to reconnect
async def handle_unexpected_disconnection(client):
	imu_addr = client.address
	if imu_addr in connected_imus:
		imu_name = connected_imus.get(imu_addr)[0]
		print(f"Trying to reconnect to {imu_name} within {WAIT_ON_DISCONNECT} seconds")
		start_time = time.time()
		reconnected = False
		while time.time() - start_time < WAIT_ON_DISCONNECT:
			client_rec = BleakClient(imu_addr, disconnected_callback=disconnected_callback)
			try:
				print(f"Trying to reconnect to {imu_name}")
				await client_rec.connect()
				if client_rec.is_connected:
					print(f"Connected to {imu_name}")
					del missing_imus[imu_addr]
					connected_clients[imu_addr] = client_rec
					await collect_data_from_one_imu(client_rec, imu_addr)
					#for the next disconnection
					reconnected = True
				else:
					print(f"{imu_name} did not reconnect ...")
			except Exception as e:
				print(f"Failed to reconnect to {imu_name} | {e}")
				await asyncio.sleep(1)
		if not reconnected:
			available_imus.remove(imu_addr)
			del connected_clients[imu_addr]
			stop_event.set()
#			await ending_campaign()

async def ending_campaign():
	print("Cleaning up: stopping and disconnecting.")
	await send_STOP_to_all_imus()
	await disconnect_imus()
	print("Finished")
#	stop_event.set()

async def main_process(blnStartCampaign):
	result = await setup_all_connections()
	if result:
		if blnStartCampaign:
			result = await setup_all_campaigns()
			if result:
				await send_START_to_all_imus()

		if startALL:
            # start everything
			print("ALL IMUs started")
			collect_task = asyncio.create_task(collect_data_from_all_imus())
			await stop_event.wait()  
			print("Ending campaign")
			collect_task.cancel()  # ferma la raccolta
			await ending_campaign()

	else: # not all IMUs are available
		await disconnect_imus()

if __name__ == '__main__':
   # Run the asyncio event loop
	try:
		# IMUX-IMUY-IMUZ
		BSN_IMUS = sys.argv[1].split("-")
		CAMPAIGN_TEMPLATE["name"] = sys.argv[2]
		CAMPAIGN_TEMPLATE["init_counter"] = int(sys.argv[3])
		CAMPAIGN_TEMPLATE["sampling_frequency"] = int(sys.argv[4])
		CAMPAIGN_TEMPLATE["raw"] = bool(sys.argv[5])
	  
		blnStartCampaign = not (len(sys.argv) > 6)
		if blnStartCampaign:
			print(CAMPAIGN_TEMPLATE)
		asyncio.run(main_process(blnStartCampaign))

	except KeyboardInterrupt:
		print("Stopped by user.")
		stop_event.set()
#		stop_imus()

cysko7927 avatar Nov 19 '25 14:11 cysko7927

  1. Are there known limits or recommended patterns for using Bleak with many concurrent BLE connections?

There is a known issue that built-in Bluetooth on Raspberry Pi doesn't work well if Wi-Fi is also enabled.

https://bleak.readthedocs.io/en/latest/troubleshooting.html#occasional-not-connected-errors-or-missing-advertisments-on-raspberry-pi

I don't have enough experience with concurrent connections to have any advice there. I tend to try to avoid them because they tend to be problematic in general.

2. Does Bleak support or recommend a single-process “connection manager” approach?

Generally, we recommend to only call connect() on one device at a time. Your code looks correct in this regard.

3. Are there recommended Bleak APIs or settings for stabilizing many-notification setups?

Not sure on this one. If there is a way to control the notification rate on the peripherals, then slowing it down could help.

And it is a long shot, but there is some chance that https://github.com/hbldh/bleak/pull/1856 could help with performance of receiving notifications.

4. Is it expected that BlueZ aborts connections when multiple processes simultaneously use the adapter?

I've never dug deep enough to understand why BlueZ does that.

5. Does Bleak have a way to serialise or stagger connections in order to reduce the load on the controller?

Nothing built-in.

dlech avatar Nov 19 '25 15:11 dlech