Skip to content

Commit 532ca54

Browse files
committed
fix bug: we were never calling BLE.disconnect() which...
on linux breaks all but the first connection attempts. Also remove unneeded event stuff and arbitrary timeouts, better just to use thread.join()
1 parent 898018e commit 532ca54

1 file changed

Lines changed: 45 additions & 46 deletions

File tree

meshtastic/ble_interface.py

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Bluetooth interface
22
"""
33
import asyncio
4+
import atexit
45
import logging
56
import struct
67
import time
7-
from threading import Event, Thread
8+
from threading import Thread
89
from typing import Optional
9-
from print_color import print
1010

1111
from bleak import BleakClient, BleakScanner, BLEDevice
12+
from print_color import print
1213

1314
from meshtastic.mesh_interface import MeshInterface
1415
from meshtastic.util import our_exit
@@ -20,18 +21,13 @@
2021
LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2"
2122

2223

23-
2424
class BLEInterface(MeshInterface):
2525
"""MeshInterface using BLE to connect to devices."""
2626

2727
class BLEError(Exception):
2828
"""An exception class for BLE errors."""
29-
pass
3029

31-
class BLEState: # pylint: disable=C0115
32-
THREADS = False
33-
BLE = False
34-
MESH = False
30+
pass
3531

3632
def __init__(
3733
self,
@@ -40,53 +36,54 @@ def __init__(
4036
debugOut=None,
4137
noNodes: bool = False,
4238
):
43-
self.state = BLEInterface.BLEState()
39+
MeshInterface.__init__(
40+
self, debugOut=debugOut, noProto=noProto, noNodes=noNodes
41+
)
4442

4543
self.should_read = False
4644

4745
logging.debug("Threads starting")
48-
self._receiveThread = Thread(target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True)
49-
self._receiveThread_started = Event()
50-
self._receiveThread_stopped = Event()
46+
self._want_receive = True
47+
self._receiveThread: Optional[Thread] = Thread(
48+
target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True
49+
)
5150
self._receiveThread.start()
52-
self._receiveThread_started.wait(1)
53-
self.state.THREADS = True
5451
logging.debug("Threads running")
5552

5653
try:
5754
logging.debug(f"BLE connecting to: {address if address else 'any'}")
58-
self.client = self.connect(address)
59-
self.state.BLE = True
55+
self.client: Optional[BLEClient] = self.connect(address)
6056
logging.debug("BLE connected")
6157
except BLEInterface.BLEError as e:
6258
self.close()
6359
raise e
6460

6561
self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler)
6662

67-
logging.debug("Mesh init starting")
68-
MeshInterface.__init__(
69-
self, debugOut=debugOut, noProto=noProto, noNodes=noNodes
70-
)
63+
logging.debug("Mesh configure starting")
7164
self._startConfig()
7265
if not self.noProto:
7366
self._waitConnected(timeout=60.0)
7467
self.waitForConfig()
75-
self.state.MESH = True
7668
logging.debug("Mesh init finished")
7769

7870
logging.debug("Register FROMNUM notify callback")
7971
self.client.start_notify(FROMNUM_UUID, self.from_num_handler)
8072

73+
# We MUST run atexit (if we can) because otherwise (at least on linux) the BLE device is not disconnected
74+
# and future connection attempts will fail. (BlueZ kinda sucks)
75+
self._exit_handler = atexit.register(self.close)
76+
8177
def from_num_handler(self, _, b): # pylint: disable=C0116
8278
"""Handle callbacks for fromnum notify.
83-
Note: this method does not need to be async because it is just setting a bool."""
79+
Note: this method does not need to be async because it is just setting a bool.
80+
"""
8481
from_num = struct.unpack("<I", bytes(b))[0]
8582
logging.debug(f"FROMNUM notify: {from_num}")
8683
self.should_read = True
8784

88-
async def log_radio_handler(self, _, b): # pylint: disable=C0116
89-
log_radio = b.decode('utf-8').replace('\n', '')
85+
async def log_radio_handler(self, _, b): # pylint: disable=C0116
86+
log_radio = b.decode("utf-8").replace("\n", "")
9087
if log_radio.startswith("DEBUG"):
9188
print(log_radio, color="cyan", end=None)
9289
elif log_radio.startswith("INFO"):
@@ -103,7 +100,9 @@ def scan() -> list[BLEDevice]:
103100
"""Scan for available BLE devices."""
104101
with BLEClient() as client:
105102
logging.info("Scanning for BLE devices (takes 10 seconds)...")
106-
response = client.discover(timeout=10, return_adv=True, service_uuids=[SERVICE_UUID])
103+
response = client.discover(
104+
timeout=10, return_adv=True, service_uuids=[SERVICE_UUID]
105+
)
107106

108107
devices = response.values()
109108

@@ -153,8 +152,7 @@ def connect(self, address: Optional[str] = None):
153152
return client
154153

155154
def _receiveFromRadioImpl(self):
156-
self._receiveThread_started.set()
157-
while self._receiveThread_started.is_set():
155+
while self._want_receive:
158156
if self.should_read:
159157
self.should_read = False
160158
retries = 0
@@ -172,44 +170,49 @@ def _receiveFromRadioImpl(self):
172170
logging.debug(f"FROMRADIO read: {b.hex()}")
173171
self._handleFromRadio(b)
174172
else:
175-
time.sleep(0.1)
176-
self._receiveThread_stopped.set()
173+
time.sleep(0.01)
177174

178175
def _sendToRadioImpl(self, toRadio):
179176
b = toRadio.SerializeToString()
180177
if b:
181178
logging.debug(f"TORADIO write: {b.hex()}")
182179
try:
183-
self.client.write_gatt_char(TORADIO_UUID, b, response=True) # FIXME: or False?
180+
self.client.write_gatt_char(
181+
TORADIO_UUID, b, response=True
182+
) # FIXME: or False?
184183
# search Bleak src for org.bluez.Error.InProgress
185184
except Exception as e:
186-
raise BLEInterface.BLEError("Error writing BLE (are you in the 'bluetooth' user group? did you enter the pairing PIN on your computer?)") from e
185+
raise BLEInterface.BLEError(
186+
"Error writing BLE (are you in the 'bluetooth' user group? did you enter the pairing PIN on your computer?)"
187+
) from e
187188
# Allow to propagate and then make sure we read
188189
time.sleep(0.01)
189190
self.should_read = True
190191

191192
def close(self):
192-
if self.state.MESH:
193-
MeshInterface.close(self)
193+
atexit.unregister(self._exit_handler)
194+
MeshInterface.close(self)
194195

195-
if self.state.THREADS:
196-
self._receiveThread_started.clear()
197-
self._receiveThread_stopped.wait(5)
196+
if self._want_receive:
197+
self.want_receive = False # Tell the thread we want it to stop
198+
self._receiveThread.join()
199+
self._receiveThread = None
198200

199-
if self.state.BLE:
201+
if self.client:
200202
self.client.disconnect()
201203
self.client.close()
204+
self.client = None
202205

203206

204207
class BLEClient:
205208
"""Client for managing connection to a BLE device"""
206209

207210
def __init__(self, address=None, **kwargs):
208-
self._eventThread = Thread(target=self._run_event_loop, name="BLEClient", daemon=True)
209-
self._eventThread_started = Event()
210-
self._eventThread_stopped = Event()
211+
self._eventLoop = asyncio.new_event_loop()
212+
self._eventThread = Thread(
213+
target=self._run_event_loop, name="BLEClient", daemon=True
214+
)
211215
self._eventThread.start()
212-
self._eventThread_started.wait(1)
213216

214217
if not address:
215218
logging.debug("No address provided - only discover method will work.")
@@ -240,7 +243,7 @@ def start_notify(self, *args, **kwargs): # pylint: disable=C0116
240243

241244
def close(self): # pylint: disable=C0116
242245
self.async_run(self._stop_event_loop())
243-
self._eventThread_stopped.wait(5)
246+
self._eventThread.join()
244247

245248
def __enter__(self):
246249
return self
@@ -255,14 +258,10 @@ def async_run(self, coro): # pylint: disable=C0116
255258
return asyncio.run_coroutine_threadsafe(coro, self._eventLoop)
256259

257260
def _run_event_loop(self):
258-
# I don't know if the event loop can be initialized in __init__ so silencing pylint
259-
self._eventLoop = asyncio.new_event_loop() # pylint: disable=W0201
260-
self._eventThread_started.set()
261261
try:
262262
self._eventLoop.run_forever()
263263
finally:
264264
self._eventLoop.close()
265-
self._eventThread_stopped.set()
266265

267266
async def _stop_event_loop(self):
268267
self._eventLoop.stop()

0 commit comments

Comments
 (0)