Skip to content

Commit f748e46

Browse files
authored
Merge b798c88 into f62986e
2 parents f62986e + b798c88 commit f748e46

7 files changed

Lines changed: 425 additions & 113 deletions

File tree

source/bdDetect.py

Lines changed: 114 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"""
1414

1515
import itertools
16-
from collections import namedtuple, defaultdict, OrderedDict
16+
from collections import defaultdict, OrderedDict
1717
import threading
1818
from concurrent.futures import ThreadPoolExecutor, Future
1919
import typing
@@ -26,28 +26,43 @@
2626
from baseObject import AutoPropertyObject
2727
import re
2828
from winAPI import messageWindow
29+
import extensionPoints
2930

3031

3132
HID_USAGE_PAGE_BRAILLE = 0x41
3233

33-
3434
DBT_DEVNODES_CHANGED=7
3535

3636
_driverDevices = OrderedDict()
3737
USB_ID_REGEX = re.compile(r"^VID_[0-9A-F]{4}&PID_[0-9A-F]{4}$", re.U)
3838

39-
class DeviceMatch(
40-
namedtuple("DeviceMatch", ("type","id", "port", "deviceInfo"))
41-
):
39+
40+
class DeviceMatch(typing.NamedTuple):
4241
"""Represents a detected device.
43-
@ivar id: The identifier of the device.
44-
@type id: str
45-
@ivar port: The port that can be used by a driver to communicate with a device.
46-
@type port: str
47-
@ivar deviceInfo: all known information about a device.
48-
@type deviceInfo: dict
4942
"""
50-
__slots__ = ()
43+
type: str
44+
"""The type of the device."""
45+
id: str
46+
"""The identifier of the device."""
47+
port: str
48+
"""The port that can be used by a driver to communicate with a device."""
49+
deviceInfo: typing.Dict[str, str]
50+
"""all known information about a device."""
51+
52+
53+
scanForDevices = extensionPoints.Chain[typing.Tuple[str, DeviceMatch]]()
54+
"""
55+
A Chain that can be iterated to scan for devices.
56+
Registered handlers should yield a tuple containing a driver name as str and DeviceMatch
57+
Handlers are called with these keyword arguments:
58+
@param detectUsb: Whether the handler is expected to yield USB devices.
59+
@type detectUsb: bool
60+
@param detectBluetooth: Whether the handler is expected to yield USB devices.
61+
@type detectBluetooth: bool
62+
@param limitToDevices: Drivers to which detection should be limited.
63+
C{None} if no driver filtering should occur.
64+
"""
65+
5166

5267
# Device type constants
5368
#: Key constant for HID devices
@@ -210,6 +225,24 @@ class _DeviceInfoFetcher(AutoPropertyObject):
210225
"""Utility class that caches fetched info for available devices for the duration of one core pump cycle."""
211226
cachePropertiesByDefault = True
212227

228+
def __init__(self):
229+
self._btDevsLock = threading.Lock
230+
self._btDevsCache: typing.Optional[typing.Tuple[str, DeviceMatch]] = None
231+
232+
#: Type info for auto property: _get_btDevsCache
233+
btDevsCache: typing.Optional[typing.List[typing.Tuple[str, DeviceMatch]]]
234+
235+
def _get_btDevsCache(self):
236+
with self._btDevsLock():
237+
return self._btDevsCache
238+
239+
def _set_btDevsCache(
240+
self,
241+
cache: typing.Optional[typing.List[typing.Tuple[str, DeviceMatch]]]
242+
):
243+
with self._btDevsLock():
244+
self._btDevsCache = cache
245+
213246
#: Type info for auto property: _get_comPorts
214247
comPorts: typing.List[typing.Dict]
215248

@@ -228,42 +261,27 @@ def _get_usbDevices(self) -> typing.List[typing.Dict]:
228261
def _get_hidDevices(self) -> typing.List[typing.Dict]:
229262
return list(hwPortUtils.listHidDevices(onlyAvailable=True))
230263

231-
#: The single instance of the device info fetcher.
232-
#: @type: L{_DeviceInfoFetcher}
233-
deviceInfoFetcher = _DeviceInfoFetcher()
264+
265+
deviceInfoFetcher: _DeviceInfoFetcher
266+
234267

235268
class Detector(object):
236269
"""Detector class used to automatically detect braille displays.
237270
This should only be used by the L{braille} module.
238271
"""
239272

240-
def __init__(
241-
self,
242-
usb: bool = True,
243-
bluetooth: bool = True,
244-
limitToDevices: typing.Optional[typing.List[str]] = None
245-
):
273+
def __init__(self):
246274
"""Constructor.
247-
The keyword arguments initialize the detector in a particular state.
248-
On an initialized instance, these initial arguments can be overridden by calling
249-
L{_queueBgScan} or L{rescan}.
250-
@param usb: Whether this instance should detect USB devices initially.
251-
@param bluetooth: Whether this instance should detect Bluetooth devices initially.
252-
@param limitToDevices: Drivers to which detection should be limited initially.
253-
C{None} if no driver filtering should occur.
275+
After construction, a scan should be queued with L{queueBgScan}.
254276
"""
255277
self._executor = ThreadPoolExecutor(1)
256-
self._btDevsLock = threading.Lock()
257-
self._btDevs: typing.Optional[typing.Tuple[str, DeviceMatch]] = None
258278
self._queuedFuture: typing.Optional[Future] = None
259279
messageWindow.pre_handleWindowMessage.register(self.handleWindowMessage)
260280
appModuleHandler.post_appSwitch.register(self.pollBluetoothDevices)
261281
self._stopEvent = threading.Event()
262-
self._detectUsb = usb
263-
self._detectBluetooth = bluetooth
264-
self._limitToDevices = limitToDevices
265-
# Perform initial scan.
266-
self._queueBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
282+
self._detectUsb = True
283+
self._detectBluetooth = True
284+
self._limitToDevices = None
267285

268286
def _queueBgScan(
269287
self,
@@ -296,50 +314,46 @@ def _stopBgScan(self):
296314
# If this future belongs to a scan that is currently running or finished, this does nothing.
297315
self._queuedFuture.cancel()
298316

299-
def _bgScanUsb(self, limitToDevices: typing.Optional[typing.List[str]]):
300-
"""Helper method to perform background scanning for USB devices.
301-
@param limitToDevices: Drivers to which detection should be limited for this scan.
302-
C{None} if no driver filtering should occur.
317+
@staticmethod
318+
def _bgScanUsb(
319+
detectUsb: bool = True,
320+
limitToDevices: typing.Optional[typing.List[str]] = None,
321+
):
322+
"""Handler for L{scanForDevices} that yields USB devices.
323+
See the L{scanForDevices} documentation for information about the parameters.
303324
"""
304-
if self._stopEvent.isSet():
325+
if not detectUsb:
305326
return
306327
for driver, match in getDriversForConnectedUsbDevices():
307-
if self._stopEvent.isSet():
308-
return
309328
if limitToDevices and driver not in limitToDevices:
310329
continue
311-
if braille.handler.setDisplayByName(driver, detected=match):
312-
return
330+
yield (driver, match)
313331

314-
def _bgScanBluetooth(self, limitToDevices: typing.Optional[typing.List[str]]):
315-
"""Helper method to perform background scanning for Bluetooth devices.
316-
@param limitToDevices: Drivers to which detection should be limited for this scan.
317-
C{None} if no driver filtering should occur.
332+
@staticmethod
333+
def _bgScanBluetooth(
334+
detectBluetooth: bool = True,
335+
limitToDevices: typing.Optional[typing.List[str]] = None,
336+
):
337+
"""Handler for L{scanForDevices} that yields Bluetooth devices and keeps an internal cache of devices.
338+
See the L{scanForDevices} documentation for information about the parameters.
318339
"""
319-
if self._stopEvent.isSet():
340+
if not detectBluetooth:
320341
return
321-
with self._btDevsLock:
322-
if self._btDevs is None:
323-
btDevs = list(getDriversForPossibleBluetoothDevices())
324-
# Cache Bluetooth devices for next time.
325-
btDevsCache = []
326-
else:
327-
btDevs = self._btDevs
328-
btDevsCache = btDevs
342+
btDevs: typing.Optional[typing.Iterable[typing.Tuple[str, DeviceMatch]]] = _DeviceInfoFetcher.btDevsCache
343+
if btDevs is None:
344+
btDevs = getDriversForPossibleBluetoothDevices()
345+
# Cache Bluetooth devices for next time.
346+
btDevsCache = []
347+
else:
348+
btDevsCache = btDevs
329349
for driver, match in btDevs:
330-
if self._stopEvent.isSet():
331-
return
332350
if limitToDevices and driver not in limitToDevices:
333351
continue
334352
if btDevsCache is not btDevs:
335353
btDevsCache.append((driver, match))
336-
if braille.handler.setDisplayByName(driver, detected=match):
337-
return
338-
if self._stopEvent.isSet():
339-
return
354+
yield (driver, match)
340355
if btDevsCache is not btDevs:
341-
with self._btDevsLock:
342-
self._btDevs = btDevsCache
356+
_DeviceInfoFetcher.btDevsCache = btDevsCache
343357

344358
def _bgScan(
345359
self,
@@ -357,10 +371,18 @@ def _bgScan(
357371
# Clear the stop event before a scan is started.
358372
# Since a scan can take some time to complete, another thread can set the stop event to cancel it.
359373
self._stopEvent.clear()
360-
if detectUsb:
361-
self._bgScanUsb(limitToDevices)
362-
if detectBluetooth:
363-
self._bgScanBluetooth(limitToDevices)
374+
iterator = scanForDevices.iter(
375+
detectUsb=detectUsb,
376+
detectBluetooth=detectBluetooth,
377+
limitToDevices=limitToDevices,
378+
)
379+
for driver, match in iterator:
380+
if self._stopEvent.is_set():
381+
return
382+
if braille.handler.setDisplayByName(driver, detected=match):
383+
return
384+
if self._stopEvent.is_set():
385+
return
364386

365387
def rescan(self, usb=True, bluetooth=True, limitToDevices=None):
366388
"""Stop a current scan when in progress, and start scanning from scratch.
@@ -372,9 +394,8 @@ def rescan(self, usb=True, bluetooth=True, limitToDevices=None):
372394
C{None} if no driver filtering should occur.
373395
"""
374396
self._stopBgScan()
375-
with self._btDevsLock:
376-
# A Bluetooth com port or HID device might have been added.
377-
self._btDevs = None
397+
# Clear the cache of bluetooth devices so new devices can be picked up.
398+
_DeviceInfoFetcher.btDevsCache = None
378399
self._queueBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
379400

380401
def handleWindowMessage(self, msg=None, wParam=None):
@@ -387,15 +408,16 @@ def pollBluetoothDevices(self):
387408
if not self._detectBluetooth:
388409
# Do not poll bluetooth devices at all when bluetooth is disabled.
389410
return
390-
with self._btDevsLock:
391-
if not self._btDevs:
392-
return
411+
if not _DeviceInfoFetcher.btDevsCache:
412+
return
393413
self._queueBgScan(bluetooth=self._detectBluetooth, limitToDevices=self._limitToDevices)
394414

395415
def terminate(self):
396416
appModuleHandler.post_appSwitch.unregister(self.pollBluetoothDevices)
397417
messageWindow.pre_handleWindowMessage.unregister(self.handleWindowMessage)
398418
self._stopBgScan()
419+
# Clear the cache of bluetooth devices so new devices can be picked up with a new instance.
420+
_DeviceInfoFetcher.btDevsCache = None
399421
self._executor.shutdown(wait=False)
400422

401423

@@ -479,12 +501,19 @@ def driverSupportsAutoDetection(driver):
479501
return driver in _driverDevices
480502

481503

482-
def initializeDetectionData():
483-
""" Initialize detection data.
504+
def initialize():
505+
""" Initializes bdDetect, such as detection data.
484506
Calls to addUsbDevices, and addBluetoothDevices.
485507
Specify the requirements for a detected device to be considered a
486508
match for a specific driver.
487509
"""
510+
global deviceInfoFetcher
511+
deviceInfoFetcher = _DeviceInfoFetcher()
512+
513+
scanForDevices.register(Detector._bgScanUsb)
514+
scanForDevices.register(Detector._bgScanBluetooth)
515+
516+
# Add devices
488517
# alva
489518
addUsbDevices("alva", KEY_HID, {
490519
"VID_0798&PID_0640", # BC640
@@ -733,3 +762,11 @@ def initializeDetectionData():
733762
"seikantk",
734763
isSeikaBluetoothDeviceMatch
735764
)
765+
766+
767+
def terminate():
768+
global deviceInfoFetcher
769+
_driverDevices.clear()
770+
scanForDevices.unregister(Detector._bgScanBluetooth)
771+
scanForDevices.unregister(Detector._bgScanUsb)
772+
del deviceInfoFetcher

source/braille.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1892,15 +1892,14 @@ class BrailleHandler(baseObject.AutoPropertyObject):
18921892
@type currentCellCount: bool
18931893
"""
18941894

1895-
filter_displaySize: extensionPoints.Filter
1895+
filter_displaySize: extensionPoints.Filter[int]
18961896
"""
18971897
Filter that allows components or add-ons to change the display size used for braille output.
18981898
For example, when a system is controlled by a remote system while having a 80 cells display connected,
18991899
the display size should be lowered to 40 whenever the remote system has a 40 cells display connected.
19001900
@param value: the number of cells of the current display.
19011901
@type value: int
19021902
"""
1903-
19041903
displaySizeChanged: extensionPoints.Action
19051904
"""
19061905
Action that allows components or add-ons to be notified of display size changes.
@@ -2089,7 +2088,6 @@ def setDisplayByName( # noqa: C901
20892088
# or situations where the user hasn't set any port.
20902089
if port:
20912090
kwargs["port"] = port
2092-
20932091
try:
20942092
newDisplay = _getDisplayDriver(name)
20952093
oldDisplay = self.display
@@ -2517,8 +2515,9 @@ def _enableDetection(self, usb=True, bluetooth=True, keepCurrentDisplay=False, l
25172515
config.conf["braille"]["display"] = AUTO_DISPLAY_NAME
25182516
if not keepCurrentDisplay:
25192517
self.setDisplayByName("noBraille", isFallback=True)
2520-
self._detector = bdDetect.Detector(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
2518+
self._detector = bdDetect.Detector()
25212519
self._detectionEnabled = True
2520+
self._detector._queueBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
25222521

25232522
def _disableDetection(self):
25242523
"""Disables automatic detection of braille displays."""
@@ -2596,7 +2595,6 @@ def initialize():
25962595
newTableName = brailleTables.RENAMED_TABLES.get(oldTableName)
25972596
if newTableName:
25982597
config.conf["braille"]["translationTable"] = newTableName
2599-
bdDetect.initializeDetectionData()
26002598
handler = BrailleHandler()
26012599
# #7459: the syncBraille has been dropped in favor of the native hims driver.
26022600
# Migrate to renamed drivers as smoothly as possible.

source/core.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ def resetConfiguration(factoryDefaults=False):
212212
import speech
213213
import vision
214214
import inputCore
215+
import bdDetect
215216
import hwIo
216217
import tones
217218
log.debug("Terminating vision")
@@ -224,6 +225,8 @@ def resetConfiguration(factoryDefaults=False):
224225
speech.terminate()
225226
log.debug("terminating tones")
226227
tones.terminate()
228+
log.debug("Terminating background braille display detection")
229+
bdDetect.terminate()
227230
log.debug("Terminating background i/o")
228231
hwIo.terminate()
229232
log.debug("terminating addonHandler")
@@ -243,6 +246,8 @@ def resetConfiguration(factoryDefaults=False):
243246
# Hardware background i/o
244247
log.debug("initializing background i/o")
245248
hwIo.initialize()
249+
log.debug("Initializing background braille display detection")
250+
bdDetect.initialize()
246251
# Tones
247252
tones.initialize()
248253
#Speech
@@ -521,6 +526,9 @@ def main():
521526
log.debug("initializing background i/o")
522527
import hwIo
523528
hwIo.initialize()
529+
log.debug("Initializing background braille display detection")
530+
import bdDetect
531+
bdDetect.initialize()
524532
log.debug("Initializing tones")
525533
import tones
526534
tones.initialize()
@@ -791,6 +799,7 @@ def _doPostNvdaStartupAction():
791799
_terminate(brailleInput)
792800
_terminate(braille)
793801
_terminate(speech)
802+
_terminate(bdDetect)
794803
_terminate(hwIo)
795804
_terminate(addonHandler)
796805
_terminate(garbageHandler)

0 commit comments

Comments
 (0)