1313"""
1414
1515import itertools
16- from collections import namedtuple , defaultdict , OrderedDict
16+ from collections import defaultdict , OrderedDict
1717import threading
1818from concurrent .futures import ThreadPoolExecutor , Future
1919import typing
2626from baseObject import AutoPropertyObject
2727import re
2828from winAPI import messageWindow
29+ import extensionPoints
2930
3031
3132HID_USAGE_PAGE_BRAILLE = 0x41
3233
33-
3434DBT_DEVNODES_CHANGED = 7
3535
3636_driverDevices = OrderedDict ()
3737USB_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
235268class 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
0 commit comments