Skip to content

Commit 6df89f5

Browse files
committed
fix BLE scan with latest Bleak
1 parent 81266e7 commit 6df89f5

3 files changed

Lines changed: 72 additions & 59 deletions

File tree

.vscode/launch.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
"module": "meshtastic",
1212
"justMyCode": false,
1313
"args": ["--debug", "--ble", "24:62:AB:DD:DF:3A"]
14+
},
15+
{
16+
"name": "meshtastic BLE scan",
17+
"type": "python",
18+
"request": "launch",
19+
"module": "meshtastic",
20+
"justMyCode": false,
21+
"args": ["--debug", "--ble-scan"]
1422
},
1523
{
1624
"name": "meshtastic admin",

meshtastic/__main__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,12 +1041,8 @@ def common():
10411041
subscribe()
10421042
if args.ble_scan:
10431043
logging.debug("BLE scan starting")
1044-
client = BLEInterface(None, debugOut=logfile, noProto=args.noproto)
1045-
try:
1046-
for x in client.scan():
1047-
print(f"Found: name='{x[1].local_name}' address='{x[0].address}'")
1048-
finally:
1049-
client.close()
1044+
for x in BLEInterface.scan():
1045+
print(f"Found: name='{x.name}' address='{x.address}'")
10501046
meshtastic.util.our_exit("BLE scan finished", 0)
10511047
return
10521048
elif args.ble:

meshtastic/ble_interface.py

Lines changed: 62 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""Bluetooth interface
22
"""
3+
import asyncio
34
import logging
4-
import time
55
import struct
6-
import asyncio
7-
from threading import Thread, Event
6+
import time
7+
from threading import Event, Thread
88
from typing import Optional
99

10-
from bleak import BleakScanner, BleakClient, BLEDevice
10+
from bleak import BleakClient, BleakScanner, BLEDevice
1111

1212
from meshtastic.mesh_interface import MeshInterface
1313
from meshtastic.util import our_exit
@@ -20,25 +20,32 @@
2020

2121
class BLEInterface(MeshInterface):
2222
"""MeshInterface using BLE to connect to devices"""
23+
2324
class BLEError(Exception):
2425
"""An exception class for BLE errors"""
26+
2527
def __init__(self, message):
2628
self.message = message
2729
super().__init__(self.message)
2830

29-
class BLEState(): # pylint: disable=C0115
31+
class BLEState: # pylint: disable=C0115
3032
THREADS = False
3133
BLE = False
3234
MESH = False
3335

34-
35-
def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None, noNodes: bool = False):
36+
def __init__(
37+
self,
38+
address: Optional[str],
39+
noProto: bool = False,
40+
debugOut=None,
41+
noNodes: bool = False,
42+
):
3643
self.state = BLEInterface.BLEState()
3744

3845
self.should_read = False
3946

4047
logging.debug("Threads starting")
41-
self._receiveThread = Thread(target = self._receiveFromRadioImpl)
48+
self._receiveThread = Thread(target=self._receiveFromRadioImpl)
4249
self._receiveThread_started = Event()
4350
self._receiveThread_stopped = Event()
4451
self._receiveThread.start()
@@ -56,72 +63,75 @@ def __init__(self, address: Optional[str], noProto: bool = False, debugOut = Non
5663
raise e
5764

5865
logging.debug("Mesh init starting")
59-
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes)
66+
MeshInterface.__init__(
67+
self, debugOut=debugOut, noProto=noProto, noNodes=noNodes
68+
)
6069
self._startConfig()
6170
if not self.noProto:
62-
self._waitConnected(timeout = 60.0)
71+
self._waitConnected(timeout=60.0)
6372
self.waitForConfig()
6473
self.state.MESH = True
6574
logging.debug("Mesh init finished")
6675

6776
logging.debug("Register FROMNUM notify callback")
6877
self.client.start_notify(FROMNUM_UUID, self.from_num_handler)
6978

70-
71-
async def from_num_handler(self, _, b): # pylint: disable=C0116
72-
from_num = struct.unpack('<I', bytes(b))[0]
79+
async def from_num_handler(self, _, b): # pylint: disable=C0116
80+
from_num = struct.unpack("<I", bytes(b))[0]
7381
logging.debug(f"FROMNUM notify: {from_num}")
7482
self.should_read = True
7583

76-
77-
def scan(self) -> list[BLEDevice]:
84+
@staticmethod
85+
def scan() -> list[BLEDevice]:
7886
"""Scan for available BLE devices."""
7987
with BLEClient() as client:
80-
response = client.discover(
81-
return_adv = True,
82-
service_uuids=[SERVICE_UUID]
83-
)
88+
response = client.discover(return_adv=True, service_uuids=[SERVICE_UUID])
8489

8590
devices = response.values()
8691

8792
# bleak sometimes returns devices we didn't ask for, so filter the response
8893
# to only return true meshtastic devices
8994
# d[0] is the device. d[1] is the advertisement data
90-
devices = list(filter(lambda d: SERVICE_UUID in d[1].service_uuids, devices))
95+
devices = list(
96+
filter(lambda d: SERVICE_UUID in d[1].service_uuids, devices)
97+
)
9198
return list(map(lambda d: d[0], devices))
9299

93-
94100
def find_device(self, address: Optional[str]) -> BLEDevice:
95-
"""Find a device by address"""
96-
addressed_devices = self.scan()
101+
"""Find a device by address."""
102+
addressed_devices = BLEInterface.scan()
97103

98104
if address:
99-
addressed_devices = list(filter(lambda x: address == x.name or address == x.address, addressed_devices))
105+
addressed_devices = list(
106+
filter(
107+
lambda x: address == x.name or address == x.address,
108+
addressed_devices,
109+
)
110+
)
100111

101112
if len(addressed_devices) == 0:
102-
raise BLEInterface.BLEError(f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it.")
113+
raise BLEInterface.BLEError(
114+
f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it."
115+
)
103116
if len(addressed_devices) > 1:
104-
raise BLEInterface.BLEError(f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found.")
117+
raise BLEInterface.BLEError(
118+
f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found."
119+
)
105120
return addressed_devices[0]
106121

107-
def _sanitize_address(address): # pylint: disable=E0213
108-
"Standardize BLE address by removing extraneous characters and lowercasing"
109-
return address \
110-
.replace("-", "") \
111-
.replace("_", "") \
112-
.replace(":", "") \
113-
.lower()
122+
def _sanitize_address(address): # pylint: disable=E0213
123+
"Standardize BLE address by removing extraneous characters and lowercasing."
124+
return address.replace("-", "").replace("_", "").replace(":", "").lower()
114125

115126
def connect(self, address: Optional[str] = None):
116-
"Connect to a device by address"
127+
"Connect to a device by address."
117128

118129
# Bleak docs recommend always doing a scan before connecting (even if we know addr)
119130
device = self.find_device(address)
120131
client = BLEClient(device.address)
121132
client.connect()
122133
return client
123134

124-
125135
def _receiveFromRadioImpl(self):
126136
self._receiveThread_started.set()
127137
while self._receiveThread_started.is_set():
@@ -146,12 +156,11 @@ def _sendToRadioImpl(self, toRadio):
146156
b = toRadio.SerializeToString()
147157
if b:
148158
logging.debug(f"TORADIO write: {b.hex()}")
149-
self.client.write_gatt_char(TORADIO_UUID, b, response = True)
159+
self.client.write_gatt_char(TORADIO_UUID, b, response=True)
150160
# Allow to propagate and then make sure we read
151161
time.sleep(0.1)
152162
self.should_read = True
153163

154-
155164
def close(self):
156165
if self.state.MESH:
157166
MeshInterface.close(self)
@@ -165,10 +174,11 @@ def close(self):
165174
self.client.close()
166175

167176

168-
class BLEClient():
177+
class BLEClient:
169178
"""Client for managing connection to a BLE device"""
170-
def __init__(self, address = None, **kwargs):
171-
self._eventThread = Thread(target = self._run_event_loop)
179+
180+
def __init__(self, address=None, **kwargs):
181+
self._eventThread = Thread(target=self._run_event_loop)
172182
self._eventThread_started = Event()
173183
self._eventThread_stopped = Event()
174184
self._eventThread.start()
@@ -180,47 +190,46 @@ def __init__(self, address = None, **kwargs):
180190

181191
self.bleak_client = BleakClient(address, **kwargs)
182192

183-
184-
def discover(self, **kwargs): # pylint: disable=C0116
193+
def discover(self, **kwargs): # pylint: disable=C0116
185194
return self.async_await(BleakScanner.discover(**kwargs))
186195

187-
def pair(self, **kwargs): # pylint: disable=C0116
196+
def pair(self, **kwargs): # pylint: disable=C0116
188197
return self.async_await(self.bleak_client.pair(**kwargs))
189198

190-
def connect(self, **kwargs): # pylint: disable=C0116
199+
def connect(self, **kwargs): # pylint: disable=C0116
191200
return self.async_await(self.bleak_client.connect(**kwargs))
192201

193-
def disconnect(self, **kwargs): # pylint: disable=C0116
202+
def disconnect(self, **kwargs): # pylint: disable=C0116
194203
self.async_await(self.bleak_client.disconnect(**kwargs))
195204

196-
def read_gatt_char(self, *args, **kwargs): # pylint: disable=C0116
205+
def read_gatt_char(self, *args, **kwargs): # pylint: disable=C0116
197206
return self.async_await(self.bleak_client.read_gatt_char(*args, **kwargs))
198207

199-
def write_gatt_char(self, *args, **kwargs): # pylint: disable=C0116
208+
def write_gatt_char(self, *args, **kwargs): # pylint: disable=C0116
200209
self.async_await(self.bleak_client.write_gatt_char(*args, **kwargs))
201210

202-
def start_notify(self, *args, **kwargs): # pylint: disable=C0116
211+
def start_notify(self, *args, **kwargs): # pylint: disable=C0116
203212
self.async_await(self.bleak_client.start_notify(*args, **kwargs))
204213

205-
def close(self): # pylint: disable=C0116
214+
def close(self): # pylint: disable=C0116
206215
self.async_run(self._stop_event_loop())
207216
self._eventThread_stopped.wait(5)
208217

209218
def __enter__(self):
210219
return self
211-
220+
212221
def __exit__(self, _type, _value, _traceback):
213222
self.close()
214223

215-
def async_await(self, coro, timeout = None): # pylint: disable=C0116
224+
def async_await(self, coro, timeout=None): # pylint: disable=C0116
216225
return self.async_run(coro).result(timeout)
217226

218-
def async_run(self, coro): # pylint: disable=C0116
227+
def async_run(self, coro): # pylint: disable=C0116
219228
return asyncio.run_coroutine_threadsafe(coro, self._eventLoop)
220229

221230
def _run_event_loop(self):
222231
# I don't know if the event loop can be initialized in __init__ so silencing pylint
223-
self._eventLoop = asyncio.new_event_loop() # pylint: disable=W0201
232+
self._eventLoop = asyncio.new_event_loop() # pylint: disable=W0201
224233
self._eventThread_started.set()
225234
try:
226235
self._eventLoop.run_forever()

0 commit comments

Comments
 (0)