11"""Bluetooth interface
22"""
33import asyncio
4+ import atexit
45import logging
56import struct
67import time
7- from threading import Event , Thread
8+ from threading import Thread
89from typing import Optional
9- from print_color import print
1010
1111from bleak import BleakClient , BleakScanner , BLEDevice
12+ from print_color import print
1213
1314from meshtastic .mesh_interface import MeshInterface
1415from meshtastic .util import our_exit
2021LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2"
2122
2223
23-
2424class 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
204207class 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