1212For drivers in add-ons, this must be done in a global plugin.
1313"""
1414
15+ from dataclasses import dataclass , field
16+ from functools import partial
1517import itertools
1618import threading
1719from concurrent .futures import ThreadPoolExecutor , Future
@@ -99,15 +101,32 @@ class DeviceMatch(NamedTuple):
99101
100102
101103MatchFuncT = Callable [[DeviceMatch ], bool ]
102- DriverDictT = defaultdict [DeviceType , set [str ] | MatchFuncT ]
103104
104- _driverDevices = OrderedDict [str , DriverDictT ]()
105105
106- fallBackDevices : set [tuple [str , DeviceType , str ]] = set ()
107- """
108- Used to store fallback devices.
109- When registered as a fallback device, it will be yielded last among the connected USB devices.
110- """
106+ @dataclass (frozen = True )
107+ class _UsbDeviceRegistryEntry :
108+ """An internal class that contains information specific to an USB device registration."""
109+
110+ id : str
111+ """The identifier of the device."""
112+ useAsFallback : bool = field (default = False , compare = False )
113+ """
114+ determine how a device is associated with a driver.
115+ If False (default), the device is immediately available for use with the driver.
116+ If True, the device is added to a fallback list and is used only if the primary driver cannot use
117+ initial devices, serving as a backup option in case of compatibility issues.
118+ This provides flexibility and robustness in managing driver-device connections.
119+ """
120+ matchFunc : MatchFuncT | None = field (default = None , compare = False )
121+ """
122+ An optional function which determines whether a given device matches.
123+ It takes a L{DeviceMatch} as its only argument
124+ and returns a C{bool} indicating whether it matched."""
125+
126+
127+ DriverDictT = defaultdict [DeviceType , set [_UsbDeviceRegistryEntry ] | MatchFuncT ]
128+
129+ _driverDevices = OrderedDict [str , DriverDictT ]()
111130
112131scanForDevices = extensionPoints .Chain [Tuple [str , DeviceMatch ]]()
113132"""
@@ -164,12 +183,25 @@ def getDriversForConnectedUsbDevices(
164183 for driver , devs in _driverDevices .items ():
165184 if limitToDevices and driver not in limitToDevices :
166185 continue
167- for type , ids in devs .items ():
168- if match .type == type and match .id in ids :
169- if (driver , match .type , match .id ) in fallBackDevices :
186+ for type , typeDefs in devs .items ():
187+ if (
188+ match .type == type
189+ and (
190+ typeDef := next (
191+ (
192+ t
193+ for t in typeDefs
194+ if isinstance (t , _UsbDeviceRegistryEntry ) and t .id == match .id
195+ ),
196+ None ,
197+ )
198+ )
199+ and (not callable (typeDef .matchFunc ) or typeDef .matchFunc (match ))
200+ ):
201+ if typeDef .useAsFallback :
170202 fallbackDriversAndMatches .append ({driver , match })
171203 else :
172- yield driver , match
204+ yield ( zdriver , match )
173205
174206 hidName = _getStandardHidDriverName ()
175207 if limitToDevices and hidName not in limitToDevices :
@@ -179,13 +211,10 @@ def getDriversForConnectedUsbDevices(
179211 # This ensures that a vendor specific driver is preferred over the braille HID protocol.
180212 # This preference may change in the future.
181213 if _isHIDBrailleMatch (match ):
182- if (driver , match .type , match .id ) in fallBackDevices :
183- fallbackDriversAndMatches .append ({hidName , match })
184- else :
185- yield hidName , match
214+ yield (hidName , match )
186215
187216 for driver , match in fallbackDriversAndMatches :
188- yield driver , match
217+ yield ( driver , match )
189218
190219
191220def _getStandardHidDriverName () -> str :
@@ -195,8 +224,16 @@ def _getStandardHidDriverName() -> str:
195224 return brailleDisplayDrivers .hidBrailleStandard .HidBrailleDriver .name
196225
197226
227+ def _isHIDUsagePageMatch (match : DeviceMatch , usagePage : int ) -> bool :
228+ return match .type == DeviceType .HID and match .deviceInfo .get ("HIDUsagePage" ) == usagePage
229+
230+
198231def _isHIDBrailleMatch (match : DeviceMatch ) -> bool :
199- return match .type == DeviceType .HID and match .deviceInfo .get ("HIDUsagePage" ) == HID_USAGE_PAGE_BRAILLE
232+ return _isHIDUsagePageMatch (match , HID_USAGE_PAGE_BRAILLE )
233+
234+
235+ def HIDUsagePageMatchFuncFactory (usagePage : int ) -> MatchFuncT :
236+ return partial (_isHIDUsagePageMatch , usagePage = usagePage )
200237
201238
202239def getDriversForPossibleBluetoothDevices (
@@ -234,7 +271,7 @@ def getDriversForPossibleBluetoothDevices(
234271 if not callable (matchFunc ):
235272 continue
236273 if matchFunc (match ):
237- yield driver , match
274+ yield ( driver , match )
238275
239276 hidName = _getStandardHidDriverName ()
240277 if limitToDevices and hidName not in limitToDevices :
@@ -467,8 +504,6 @@ def terminate(self):
467504 appModuleHandler .post_appSwitch .unregister (self .pollBluetoothDevices )
468505 messageWindow .pre_handleWindowMessage .unregister (self .handleWindowMessage )
469506 self ._stopBgScan ()
470- # Clear the fallback devices
471- fallBackDevices .clear ()
472507 # Clear the cache of bluetooth devices so new devices can be picked up with a new instance.
473508 deviceInfoFetcher .btDevsCache = None
474509 self ._executor .shutdown (wait = False )
@@ -501,15 +536,25 @@ def getConnectedUsbDevicesForDriver(driver: str) -> Iterator[DeviceMatch]:
501536 for match in usbDevs :
502537 if driver == _getStandardHidDriverName ():
503538 if _isHIDBrailleMatch (match ):
504- if (driver , match .type , match .id ) in fallBackDevices :
505- fallbackMatches .append (match )
506- else :
507- yield match
539+ yield match
508540 else :
509541 devs = _driverDevices [driver ]
510- for type , ids in devs .items ():
511- if match .type == type and match .id in ids :
512- if (driver , match .type , match .id ) in fallBackDevices :
542+ for type , typeDefs in devs .items ():
543+ if (
544+ match .type == type
545+ and (
546+ typeDef := next (
547+ (
548+ t
549+ for t in typeDefs
550+ if isinstance (t , _UsbDeviceRegistryEntry ) and t .id == match .id
551+ ),
552+ None ,
553+ )
554+ )
555+ and (not callable (typeDef .matchFunc ) or typeDef .matchFunc (match ))
556+ ):
557+ if typeDef .useAsFallback :
513558 fallbackMatches .append (match )
514559 else :
515560 yield match
@@ -602,7 +647,7 @@ def getBrailleDisplayDriversEnabledForDetection() -> Generator[str, Any, Any]:
602647
603648def initialize ():
604649 """Initializes bdDetect, such as detection data.
605- Calls to addUsbDevices, and addBluetoothDevices.
650+ Calls to addUsbDevice, addUsbDevices, and addBluetoothDevices.
606651 Specify the requirements for a detected device to be considered a
607652 match for a specific driver.
608653 """
@@ -646,18 +691,58 @@ def _getDriverDict(self) -> DriverDictT:
646691 ret = _driverDevices [self ._driver ] = DriverDictT (set )
647692 return ret
648693
649- def addUsbDevices (self , type : DeviceType , ids : set [str ], useAsFallBack : bool = False ):
694+ def addUsbDevice (
695+ self ,
696+ type : DeviceType ,
697+ id : str ,
698+ useAsFallback : bool = False ,
699+ matchFunc : MatchFuncT | None = None ,
700+ ):
701+ """Associate an USB device with the driver on this instance.
702+ :param type: The type of the driver.
703+ :param id: A USB ID in the form C{"VID_xxxx&PID_XXXX"}.
704+ Note that alphabetical characters in hexadecimal numbers should be uppercase.
705+ :param useAsFallback: A boolean flag to determine how this USB device is associated with the driver.
706+ If False (default), the device is added directly to the primary driver list for the specified type,
707+ meaning it is immediately available for use with the driver.
708+ If True, the device is used only if the primary driver cannot use
709+ the initial devices, serving as a backup option in case of compatibility issues.
710+ This provides flexibility and robustness in managing driver-device connections.
711+ @param matchFunc: An optional function which determines whether a given device matches.
712+ It takes a L{DeviceMatch} as its only argument
713+ and returns a C{bool} indicating whether it matched.
714+ It can be used to further constrain a device registration, such as for a specific HID usage page.
715+ :raise ValueError: When the provided ID is malformed.
716+ """
717+ if not isinstance (id , str ) or not USB_ID_REGEX .match (id ):
718+ raise ValueError (
719+ f"Invalid ID provided for driver { self ._driver !r} , type { type !r} : " f"{ id !r} " ,
720+ )
721+ devs = self ._getDriverDict ()
722+ driverUsb = devs [type ]
723+ driverUsb .add (_UsbDeviceRegistryEntry (id , useAsFallback , matchFunc ))
724+
725+ def addUsbDevices (
726+ self ,
727+ type : DeviceType ,
728+ ids : set [str ],
729+ useAsFallback : bool = False ,
730+ matchFunc : MatchFuncT = None ,
731+ ):
650732 """Associate USB devices with the driver on this instance.
651733 :param type: The type of the driver.
652734 :param ids: A set of USB IDs in the form C{"VID_xxxx&PID_XXXX"}.
653735 Note that alphabetical characters in hexadecimal numbers should be uppercase.
654- :param useAsFallBack: A boolean flag to determine how USB devices are associated with the driver.
655-
736+ :param useAsFallback: A boolean flag to determine how USB devices are associated with the driver.
656737 If False (default), the devices are added directly to the primary driver list for the specified type,
657738 meaning they are immediately available for use with the driver.
658739 If True, the devices are added to a fallback list and are used only if the primary driver cannot use
659740 the initial devices, serving as a backup option in case of compatibility issues.
660741 This provides flexibility and robustness in managing driver-device connections.
742+ @param matchFunc: An optional function which determines whether a given device matches.
743+ It takes a L{DeviceMatch} as its only argument
744+ and returns a C{bool} indicating whether it matched.
745+ It can be used to further constrain device registrations, such as for a specific HID usage page.
661746 :raise ValueError: When one of the provided IDs is malformed.
662747 """
663748 malformedIds = [id for id in ids if not isinstance (id , str ) or not USB_ID_REGEX .match (id )]
@@ -666,12 +751,9 @@ def addUsbDevices(self, type: DeviceType, ids: set[str], useAsFallBack: bool = F
666751 f"Invalid IDs provided for driver { self ._driver !r} , type { type !r} : "
667752 f"{ ', ' .join (malformedIds )} " ,
668753 )
669- if useAsFallBack :
670- fallBackDevices .update ((self ._driver , type , id ) for id in ids )
671-
672754 devs = self ._getDriverDict ()
673755 driverUsb = devs [type ]
674- driverUsb .update (ids )
756+ driverUsb .update (( _UsbDeviceRegistryEntry ( id , useAsFallback , matchFunc ) for id in ids ) )
675757
676758 def addBluetoothDevices (self , matchFunc : MatchFuncT ):
677759 """Associate Bluetooth HID or COM ports with the driver on this instance.
0 commit comments