mirror of
https://github.com/meshcore-dev/meshcore_py.git
synced 2026-09-29 08:56:38 +00:00
fix(ble): serialise writes to the RX characteristic
Two overlapping write_gatt_char() calls on the same characteristic drop the BLE link outright. Observed on macOS/CoreBluetooth as "BLE write failed: 19", after which the connection is gone and the pending command never completes. Nothing above the transport guaranteed callers were sequential: schedulers, health checks, periodic status queries and user commands all issue commands independently, so any unlucky overlap could take the radio down. The existing _mesh_request_lock only guards a few binary-request helpers, not the transport. Reproduced on a companion radio over BLE by issuing send_device_query() and send_node_discover_req() concurrently: before: BLE write failed: 19, connected=False, command hung >45s after: both complete in 0.12s, connected=True Issued sequentially the same two commands take 0.09s each and are fine, so it is specifically the overlap. Ruled out as causes beforehand: notification load (three discovers under a 91-packet firehose kept writes at 0.06-0.17s with the link stable) and the discover command itself. The lock is created lazily so it binds to the running loop, and is released on the failure path so one failed write cannot wedge every later command.
This commit is contained in:
+12
-1
@@ -4,6 +4,7 @@ mccli.py : CLI interface to MeschCore BLE companion app
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# Make bleak optional - only fail if BLE operations are attempted
|
||||
@@ -52,6 +53,13 @@ class BLEConnection:
|
||||
self.rx_char = None
|
||||
self._disconnect_callback = None
|
||||
self._background_tasks: set[asyncio.Task] = set()
|
||||
# Serialises write_gatt_char(). Two overlapping writes to the same
|
||||
# characteristic drop the link outright (observed on macOS/CoreBluetooth:
|
||||
# "BLE write failed: 19", connection gone). Nothing above this layer
|
||||
# guarantees callers are sequential -- schedulers, health checks and
|
||||
# user commands all issue commands independently -- so the transport has
|
||||
# to enforce it. Lazily created so it binds to the running loop.
|
||||
self._write_lock: Optional[asyncio.Lock] = None
|
||||
|
||||
def _spawn_background(self, coro) -> asyncio.Task:
|
||||
"""Create a tracked background task (prevents GC of fire-and-forget tasks)."""
|
||||
@@ -199,8 +207,11 @@ class BLEConnection:
|
||||
if not self.rx_char:
|
||||
logger.error("RX characteristic not found")
|
||||
return False
|
||||
if self._write_lock is None:
|
||||
self._write_lock = asyncio.Lock()
|
||||
try:
|
||||
await self.client.write_gatt_char(self.rx_char, bytes(data), response=True)
|
||||
async with self._write_lock:
|
||||
await self.client.write_gatt_char(self.rx_char, bytes(data), response=True)
|
||||
except Exception as exc:
|
||||
logger.warning(f"BLE write failed: {exc}")
|
||||
if self._disconnect_callback:
|
||||
|
||||
Reference in New Issue
Block a user