Skip to content

Commit

Permalink
Merge 98d01bc into e80d782
Browse files Browse the repository at this point in the history
  • Loading branch information
LeonarddeR authored Dec 20, 2024
2 parents e80d782 + 98d01bc commit 2574273
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 56 deletions.
152 changes: 117 additions & 35 deletions source/bdDetect.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
For drivers in add-ons, this must be done in a global plugin.
"""

from dataclasses import dataclass, field
from functools import partial
import itertools
import threading
from concurrent.futures import ThreadPoolExecutor, Future
Expand Down Expand Up @@ -99,15 +101,32 @@ class DeviceMatch(NamedTuple):


MatchFuncT = Callable[[DeviceMatch], bool]
DriverDictT = defaultdict[DeviceType, set[str] | MatchFuncT]

_driverDevices = OrderedDict[str, DriverDictT]()

fallBackDevices: set[tuple[str, DeviceType, str]] = set()
"""
Used to store fallback devices.
When registered as a fallback device, it will be yielded last among the connected USB devices.
"""
@dataclass(frozen=True)
class _UsbDeviceRegistryEntry:
"""An internal class that contains information specific to an USB device registration."""

id: str
"""The identifier of the device."""
useAsFallback: bool = field(default=False, compare=False)
"""
determine how a device is associated with a driver.
If False (default), the device is immediately available for use with the driver.
If True, the device is added to a fallback list and is used only if the primary driver cannot use
initial devices, serving as a backup option in case of compatibility issues.
This provides flexibility and robustness in managing driver-device connections.
"""
matchFunc: MatchFuncT | None = field(default=None, compare=False)
"""
An optional function which determines whether a given device matches.
It takes a L{DeviceMatch} as its only argument
and returns a C{bool} indicating whether it matched."""


DriverDictT = defaultdict[DeviceType, set[_UsbDeviceRegistryEntry] | MatchFuncT]

_driverDevices = OrderedDict[str, DriverDictT]()

scanForDevices = extensionPoints.Chain[Tuple[str, DeviceMatch]]()
"""
Expand Down Expand Up @@ -164,12 +183,25 @@ def getDriversForConnectedUsbDevices(
for driver, devs in _driverDevices.items():
if limitToDevices and driver not in limitToDevices:
continue
for type, ids in devs.items():
if match.type == type and match.id in ids:
if (driver, match.type, match.id) in fallBackDevices:
for type, typeDefs in devs.items():
if (
match.type == type
and (
typeDef := next(
(
t
for t in typeDefs
if isinstance(t, _UsbDeviceRegistryEntry) and t.id == match.id
),
None,
)
)
and (not callable(typeDef.matchFunc) or typeDef.matchFunc(match))
):
if typeDef.useAsFallback:
fallbackDriversAndMatches.append({driver, match})
else:
yield driver, match
yield (zdriver, match)

hidName = _getStandardHidDriverName()
if limitToDevices and hidName not in limitToDevices:
Expand All @@ -179,13 +211,10 @@ def getDriversForConnectedUsbDevices(
# This ensures that a vendor specific driver is preferred over the braille HID protocol.
# This preference may change in the future.
if _isHIDBrailleMatch(match):
if (driver, match.type, match.id) in fallBackDevices:
fallbackDriversAndMatches.append({hidName, match})
else:
yield hidName, match
yield (hidName, match)

for driver, match in fallbackDriversAndMatches:
yield driver, match
yield (driver, match)


def _getStandardHidDriverName() -> str:
Expand All @@ -195,8 +224,16 @@ def _getStandardHidDriverName() -> str:
return brailleDisplayDrivers.hidBrailleStandard.HidBrailleDriver.name


def _isHIDUsagePageMatch(match: DeviceMatch, usagePage: int) -> bool:
return match.type == DeviceType.HID and match.deviceInfo.get("HIDUsagePage") == usagePage


def _isHIDBrailleMatch(match: DeviceMatch) -> bool:
return match.type == DeviceType.HID and match.deviceInfo.get("HIDUsagePage") == HID_USAGE_PAGE_BRAILLE
return _isHIDUsagePageMatch(match, HID_USAGE_PAGE_BRAILLE)


def HIDUsagePageMatchFuncFactory(usagePage: int) -> MatchFuncT:
return partial(_isHIDUsagePageMatch, usagePage=usagePage)


def getDriversForPossibleBluetoothDevices(
Expand Down Expand Up @@ -234,7 +271,7 @@ def getDriversForPossibleBluetoothDevices(
if not callable(matchFunc):
continue
if matchFunc(match):
yield driver, match
yield (driver, match)

hidName = _getStandardHidDriverName()
if limitToDevices and hidName not in limitToDevices:
Expand Down Expand Up @@ -467,8 +504,6 @@ def terminate(self):
appModuleHandler.post_appSwitch.unregister(self.pollBluetoothDevices)
messageWindow.pre_handleWindowMessage.unregister(self.handleWindowMessage)
self._stopBgScan()
# Clear the fallback devices
fallBackDevices.clear()
# Clear the cache of bluetooth devices so new devices can be picked up with a new instance.
deviceInfoFetcher.btDevsCache = None
self._executor.shutdown(wait=False)
Expand Down Expand Up @@ -501,15 +536,25 @@ def getConnectedUsbDevicesForDriver(driver: str) -> Iterator[DeviceMatch]:
for match in usbDevs:
if driver == _getStandardHidDriverName():
if _isHIDBrailleMatch(match):
if (driver, match.type, match.id) in fallBackDevices:
fallbackMatches.append(match)
else:
yield match
yield match
else:
devs = _driverDevices[driver]
for type, ids in devs.items():
if match.type == type and match.id in ids:
if (driver, match.type, match.id) in fallBackDevices:
for type, typeDefs in devs.items():
if (
match.type == type
and (
typeDef := next(
(
t
for t in typeDefs
if isinstance(t, _UsbDeviceRegistryEntry) and t.id == match.id
),
None,
)
)
and (not callable(typeDef.matchFunc) or typeDef.matchFunc(match))
):
if typeDef.useAsFallback:
fallbackMatches.append(match)
else:
yield match
Expand Down Expand Up @@ -602,7 +647,7 @@ def getBrailleDisplayDriversEnabledForDetection() -> Generator[str, Any, Any]:

def initialize():
"""Initializes bdDetect, such as detection data.
Calls to addUsbDevices, and addBluetoothDevices.
Calls to addUsbDevice, addUsbDevices, and addBluetoothDevices.
Specify the requirements for a detected device to be considered a
match for a specific driver.
"""
Expand Down Expand Up @@ -646,18 +691,58 @@ def _getDriverDict(self) -> DriverDictT:
ret = _driverDevices[self._driver] = DriverDictT(set)
return ret

def addUsbDevices(self, type: DeviceType, ids: set[str], useAsFallBack: bool = False):
def addUsbDevice(
self,
type: DeviceType,
id: str,
useAsFallback: bool = False,
matchFunc: MatchFuncT | None = None,
):
"""Associate an USB device with the driver on this instance.
:param type: The type of the driver.
:param id: A USB ID in the form C{"VID_xxxx&PID_XXXX"}.
Note that alphabetical characters in hexadecimal numbers should be uppercase.
:param useAsFallback: A boolean flag to determine how this USB device is associated with the driver.
If False (default), the device is added directly to the primary driver list for the specified type,
meaning it is immediately available for use with the driver.
If True, the device is used only if the primary driver cannot use
the initial devices, serving as a backup option in case of compatibility issues.
This provides flexibility and robustness in managing driver-device connections.
@param matchFunc: An optional function which determines whether a given device matches.
It takes a L{DeviceMatch} as its only argument
and returns a C{bool} indicating whether it matched.
It can be used to further constrain a device registration, such as for a specific HID usage page.
:raise ValueError: When the provided ID is malformed.
"""
if not isinstance(id, str) or not USB_ID_REGEX.match(id):
raise ValueError(
f"Invalid ID provided for driver {self._driver!r}, type {type!r}: " f"{id!r}",
)
devs = self._getDriverDict()
driverUsb = devs[type]
driverUsb.add(_UsbDeviceRegistryEntry(id, useAsFallback, matchFunc))

def addUsbDevices(
self,
type: DeviceType,
ids: set[str],
useAsFallback: bool = False,
matchFunc: MatchFuncT = None,
):
"""Associate USB devices with the driver on this instance.
:param type: The type of the driver.
:param ids: A set of USB IDs in the form C{"VID_xxxx&PID_XXXX"}.
Note that alphabetical characters in hexadecimal numbers should be uppercase.
:param useAsFallBack: A boolean flag to determine how USB devices are associated with the driver.
:param useAsFallback: A boolean flag to determine how USB devices are associated with the driver.
If False (default), the devices are added directly to the primary driver list for the specified type,
meaning they are immediately available for use with the driver.
If True, the devices are added to a fallback list and are used only if the primary driver cannot use
the initial devices, serving as a backup option in case of compatibility issues.
This provides flexibility and robustness in managing driver-device connections.
@param matchFunc: An optional function which determines whether a given device matches.
It takes a L{DeviceMatch} as its only argument
and returns a C{bool} indicating whether it matched.
It can be used to further constrain device registrations, such as for a specific HID usage page.
:raise ValueError: When one of the provided IDs is malformed.
"""
malformedIds = [id for id in ids if not isinstance(id, str) or not USB_ID_REGEX.match(id)]
Expand All @@ -666,12 +751,9 @@ def addUsbDevices(self, type: DeviceType, ids: set[str], useAsFallBack: bool = F
f"Invalid IDs provided for driver {self._driver!r}, type {type!r}: "
f"{', '.join(malformedIds)}",
)
if useAsFallBack:
fallBackDevices.update((self._driver, type, id) for id in ids)

devs = self._getDriverDict()
driverUsb = devs[type]
driverUsb.update(ids)
driverUsb.update((_UsbDeviceRegistryEntry(id, useAsFallback, matchFunc) for id in ids))

def addBluetoothDevices(self, matchFunc: MatchFuncT):
"""Associate Bluetooth HID or COM ports with the driver on this instance.
Expand Down
13 changes: 4 additions & 9 deletions source/brailleDisplayDrivers/albatross/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):

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

@classmethod
Expand Down Expand Up @@ -168,11 +168,6 @@ def _searchPorts(self, originalPort: str):
"""
for self._baudRate in BAUD_RATE:
for portType, portId, port, portInfo in self._getTryPorts(originalPort):
# Block port if its vid and pid are correct but bus reported
# device description is not "Albatross Braille Display".
if portId == VID_AND_PID and portInfo.get("busReportedDeviceDescription") != BUS_DEVICE_DESC:
log.debug(f"port {port} blocked; port information: {portInfo}")
continue
# For reconnection
self._currentPort = port
self._tryToConnect = True
Expand Down
42 changes: 30 additions & 12 deletions source/brailleDisplayDrivers/brailliantB.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
HR_KEYS = b"\x04"
HR_BRAILLE = b"\x05"
HR_POWEROFF = b"\x07"
HID_USAGE_PAGE = 0x93

KEY_NAMES = {
1: "power", # Brailliant BI 32, 40 and 80.
Expand Down Expand Up @@ -93,12 +94,18 @@ def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
{
"VID_1C71&PID_C111", # Mantis Q 40
"VID_1C71&PID_C101", # Chameleon 20
"VID_1C71&PID_C131", # Brailliant BI 40X
"VID_1C71&PID_C141", # Brailliant BI 20X
},
matchFunc=bdDetect.HIDUsagePageMatchFuncFactory(HID_USAGE_PAGE),
)
driverRegistrar.addUsbDevices(
bdDetect.DeviceType.HID,
{
"VID_1C71&PID_C121", # Humanware BrailleOne 20 HID
"VID_1C71&PID_CE01", # NLS eReader 20 HID
"VID_1C71&PID_C006", # Brailliant BI 32, 40 and 80
"VID_1C71&PID_C022", # Brailliant BI 14
"VID_1C71&PID_C131", # Brailliant BI 40X
"VID_1C71&PID_C141", # Brailliant BI 20X
"VID_1C71&PID_C00A", # BrailleNote Touch
"VID_1C71&PID_C00E", # BrailleNote Touch v2
},
Expand All @@ -120,16 +127,24 @@ def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
or (
m.type == bdDetect.DeviceType.HID
and m.deviceInfo.get("manufacturer") == "Humanware"
and m.deviceInfo.get("product")
in (
"Brailliant HID",
"APH Chameleon 20",
"APH Mantis Q40",
"Humanware BrailleOne",
"NLS eReader",
"NLS eReader Humanware",
"Brailliant BI 40X",
"Brailliant BI 20X",
and (
(
m.deviceInfo.get("product")
in (
"APH Chameleon 20",
"APH Mantis Q40",
"Brailliant BI 40X",
"Brailliant BI 20X",
)
and bdDetect._isHIDUsagePageMatch(m, HID_USAGE_PAGE)
)
or m.deviceInfo.get("product")
in (
"Brailliant HID",
"Humanware BrailleOne",
"NLS eReader",
"NLS eReader Humanware",
)
)
),
)
Expand All @@ -147,6 +162,9 @@ def __init__(self, port="auto"):
# Try talking to the display.
try:
if self.isHid:
if (usasePage := portInfo.get("HIDUsagePage")) != HID_USAGE_PAGE:
log.debugWarning(f"Ignoring device {port!r} with usage page {usasePage!r}")
continue
self._dev = hwIo.Hid(port, onReceive=self._hidOnReceive)
else:
self._dev = hwIo.Serial(
Expand Down
6 changes: 6 additions & 0 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ Add-ons will need to be re-tested and have their manifest updated.
* Added the following extension points (#17428, @ctoth):
* `inputCore.decide_handleRawKey`: called on each keypress
* `speech.extensions.post_speechPaused`: called when speech is paused or unpaused
* Changes to braille display auto detection registration in `bdDetect.DriverRegistrar`: (#17521, @LeonarddeR)
* Added the `addUsbDevice` method to register one USB device at a time.
* Added the `matchFunc` parameter to `addUsbDevices` which is also available on `addUsbDevice`.
* 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.
* See the method documentation as well as examples in the albatross and brailliantB drivers for more information.

#### API Breaking Changes

Expand Down Expand Up @@ -151,6 +156,7 @@ As the NVDA update check URL is now configurable directly within NVDA, no replac
* 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)
* The following symbols have been removed with no replacement: `languageHandler.getLanguageCliArgs`, `__main__.quitGroup` and `__main__.installGroup` . (#17486, @CyrilleB79)
* Prefix matching on command line flags, e.g. using `--di` for `--disable-addons` is no longer supported. (#11644, @CyrilleB79)
* The `useAsFallBack` keyword argument of `bdDetect.DriverRegistrar` has been renamed to `useAsFallback`. (#17521, @LeonarddeR)

#### Deprecations

Expand Down

0 comments on commit 2574273

Please sign in to comment.