Skip to content

Commit 2574273

Browse files
authored
Merge 98d01bc into e80d782
2 parents e80d782 + 98d01bc commit 2574273

4 files changed

Lines changed: 157 additions & 56 deletions

File tree

source/bdDetect.py

Lines changed: 117 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
For 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
1517
import itertools
1618
import threading
1719
from concurrent.futures import ThreadPoolExecutor, Future
@@ -99,15 +101,32 @@ class DeviceMatch(NamedTuple):
99101

100102

101103
MatchFuncT = 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

112131
scanForDevices = 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

191220
def _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+
198231
def _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

202239
def 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

603648
def 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.

source/brailleDisplayDrivers/albatross/driver.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
8484

8585
@classmethod
8686
def registerAutomaticDetection(cls, driverRegistrar: DriverRegistrar):
87-
driverRegistrar.addUsbDevices(
87+
driverRegistrar.addUsbDevice(
8888
DeviceType.SERIAL,
89-
{
90-
"VID_0403&PID_6001", # Caiku Albatross 46/80
91-
},
89+
VID_AND_PID, # Caiku Albatross 46/80
90+
# Filter for bus reported device description, which should be "Albatross Braille Display".
91+
matchFunc=lambda match: match.deviceInfo.get("busReportedDeviceDescription") == BUS_DEVICE_DESC,
9292
)
9393

9494
@classmethod
@@ -168,11 +168,6 @@ def _searchPorts(self, originalPort: str):
168168
"""
169169
for self._baudRate in BAUD_RATE:
170170
for portType, portId, port, portInfo in self._getTryPorts(originalPort):
171-
# Block port if its vid and pid are correct but bus reported
172-
# device description is not "Albatross Braille Display".
173-
if portId == VID_AND_PID and portInfo.get("busReportedDeviceDescription") != BUS_DEVICE_DESC:
174-
log.debug(f"port {port} blocked; port information: {portInfo}")
175-
continue
176171
# For reconnection
177172
self._currentPort = port
178173
self._tryToConnect = True

source/brailleDisplayDrivers/brailliantB.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
HR_KEYS = b"\x04"
3636
HR_BRAILLE = b"\x05"
3737
HR_POWEROFF = b"\x07"
38+
HID_USAGE_PAGE = 0x93
3839

3940
KEY_NAMES = {
4041
1: "power", # Brailliant BI 32, 40 and 80.
@@ -93,12 +94,18 @@ def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
9394
{
9495
"VID_1C71&PID_C111", # Mantis Q 40
9596
"VID_1C71&PID_C101", # Chameleon 20
97+
"VID_1C71&PID_C131", # Brailliant BI 40X
98+
"VID_1C71&PID_C141", # Brailliant BI 20X
99+
},
100+
matchFunc=bdDetect.HIDUsagePageMatchFuncFactory(HID_USAGE_PAGE),
101+
)
102+
driverRegistrar.addUsbDevices(
103+
bdDetect.DeviceType.HID,
104+
{
96105
"VID_1C71&PID_C121", # Humanware BrailleOne 20 HID
97106
"VID_1C71&PID_CE01", # NLS eReader 20 HID
98107
"VID_1C71&PID_C006", # Brailliant BI 32, 40 and 80
99108
"VID_1C71&PID_C022", # Brailliant BI 14
100-
"VID_1C71&PID_C131", # Brailliant BI 40X
101-
"VID_1C71&PID_C141", # Brailliant BI 20X
102109
"VID_1C71&PID_C00A", # BrailleNote Touch
103110
"VID_1C71&PID_C00E", # BrailleNote Touch v2
104111
},
@@ -120,16 +127,24 @@ def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
120127
or (
121128
m.type == bdDetect.DeviceType.HID
122129
and m.deviceInfo.get("manufacturer") == "Humanware"
123-
and m.deviceInfo.get("product")
124-
in (
125-
"Brailliant HID",
126-
"APH Chameleon 20",
127-
"APH Mantis Q40",
128-
"Humanware BrailleOne",
129-
"NLS eReader",
130-
"NLS eReader Humanware",
131-
"Brailliant BI 40X",
132-
"Brailliant BI 20X",
130+
and (
131+
(
132+
m.deviceInfo.get("product")
133+
in (
134+
"APH Chameleon 20",
135+
"APH Mantis Q40",
136+
"Brailliant BI 40X",
137+
"Brailliant BI 20X",
138+
)
139+
and bdDetect._isHIDUsagePageMatch(m, HID_USAGE_PAGE)
140+
)
141+
or m.deviceInfo.get("product")
142+
in (
143+
"Brailliant HID",
144+
"Humanware BrailleOne",
145+
"NLS eReader",
146+
"NLS eReader Humanware",
147+
)
133148
)
134149
),
135150
)
@@ -147,6 +162,9 @@ def __init__(self, port="auto"):
147162
# Try talking to the display.
148163
try:
149164
if self.isHid:
165+
if (usasePage := portInfo.get("HIDUsagePage")) != HID_USAGE_PAGE:
166+
log.debugWarning(f"Ignoring device {port!r} with usage page {usasePage!r}")
167+
continue
150168
self._dev = hwIo.Hid(port, onReceive=self._hidOnReceive)
151169
else:
152170
self._dev = hwIo.Serial(

user_docs/en/changes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ Add-ons will need to be re-tested and have their manifest updated.
124124
* Added the following extension points (#17428, @ctoth):
125125
* `inputCore.decide_handleRawKey`: called on each keypress
126126
* `speech.extensions.post_speechPaused`: called when speech is paused or unpaused
127+
* Changes to braille display auto detection registration in `bdDetect.DriverRegistrar`: (#17521, @LeonarddeR)
128+
* Added the `addUsbDevice` method to register one USB device at a time.
129+
* Added the `matchFunc` parameter to `addUsbDevices` which is also available on `addUsbDevice`.
130+
* This way device detection can be constrained further in cases where a VID/PID-combination is shared by multiple devices across multiple drivers, or when a HID device offers multiple endpoints, for example.
131+
* See the method documentation as well as examples in the albatross and brailliantB drivers for more information.
127132

128133
#### API Breaking Changes
129134

@@ -151,6 +156,7 @@ As the NVDA update check URL is now configurable directly within NVDA, no replac
151156
* In `NVDAObjects.window.scintilla.ScintillaTextInfo`, if no text is selected, the `collapse` method is overriden to expand to line if the `end` parameter is set to `True` (#17431, @nvdaes)
152157
* The following symbols have been removed with no replacement: `languageHandler.getLanguageCliArgs`, `__main__.quitGroup` and `__main__.installGroup` . (#17486, @CyrilleB79)
153158
* Prefix matching on command line flags, e.g. using `--di` for `--disable-addons` is no longer supported. (#11644, @CyrilleB79)
159+
* The `useAsFallBack` keyword argument of `bdDetect.DriverRegistrar` has been renamed to `useAsFallback`. (#17521, @LeonarddeR)
154160

155161
#### Deprecations
156162

0 commit comments

Comments
 (0)