Skip to content

Commit

Permalink
Allow excluding display drivers from automatic detection (#15200)
Browse files Browse the repository at this point in the history
closes #15196

Summary of the issue:
It is currently not possible to disable display drivers for automatic detection, other than the HID braille standard
There is no obvious way for add-on bundled braille display drivers to register themselves in automatic detection.
Description of user facing changes
In the braille display selection dialog, when automatic is selected, there is now an additional checklistbox that allows you to disable drivers. The disabled drivers are saved in config.
The advanced option for to disable HID braille has been removed. Users should use the new checklistbox instead. There is a config profile upgrade step to remember the user's decision about explicitly disabling HID.
It is now possible to select the HID braille driver manually when it is disabled for automatic detection. The old option in advanced setting disabled HID completely, i.e. the driver wasn't available for manual selection either.
Description of development approach
Added a registerAutomaticDetection classmethod to BrailleDisplayDriver. This method is called by bdDetect.initialize. The classmethod receives a bdDetect.DriverRegistrar that contains the following methods:
addUsbDevices: to register USB devices
addBluetoothDevices: To register bluetooth devices
addDeviceScanner: convenience method to register a handler to the scanForDevices extension point
Added a supportsAutomaticDetection boolean attribute on braille display drivers. This should be set to True for bdDetect to recognize the driver.
Removed the option from the config to enable/disable HID, instead rely on a new excludeDisplays lisst in the config that is populated by the new option in the braille display selection dialog. Note that exclusions are saved, not inclusions. This ensures that new drivers will be opt-out.
bdDetect.Detector._queueBgScan has a backwards compatible change for its limitToDevices parameter. When None, it falls back to the list of enabled drivers. When no drivers are disabled, it checks all drivers.
Logic from bdDetect.initialize() has been moved to the respective display drivers, calling the replacement methods on the driverRegistrar and omitting the name of the driver in the method call.
  • Loading branch information
LeonarddeR authored Sep 1, 2023
1 parent ba9215c commit 104de7c
Show file tree
Hide file tree
Showing 20 changed files with 748 additions and 532 deletions.
526 changes: 183 additions & 343 deletions source/bdDetect.py

Large diffs are not rendered by default.

102 changes: 77 additions & 25 deletions source/braille.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generator,
Iterable,
Expand Down Expand Up @@ -398,6 +399,7 @@ def _getDisplayDriver(moduleName: str, caseSensitive: bool = True) -> Type["Brai
else:
raise initialException


def getDisplayList(excludeNegativeChecks=True) -> List[Tuple[str, str]]:
"""Gets a list of available display driver names with their descriptions.
@param excludeNegativeChecks: excludes all drivers for which the check method returns C{False}.
Expand All @@ -407,30 +409,23 @@ def getDisplayList(excludeNegativeChecks=True) -> List[Tuple[str, str]]:
displayList = []
# The display that should be placed at the end of the list.
lastDisplay = None
for loader, name, isPkg in pkgutil.iter_modules(brailleDisplayDrivers.__path__):
if name.startswith('_'):
continue
try:
display = _getDisplayDriver(name)
except:
log.error("Error while importing braille display driver %s" % name,
exc_info=True)
continue
for display in getDisplayDrivers():
try:
if not excludeNegativeChecks or display.check():
if display.name == "noBraille":
lastDisplay = (display.name, display.description)
else:
displayList.append((display.name, display.description))
else:
log.debugWarning("Braille display driver %s reports as unavailable, excluding" % name)
log.debugWarning(f"Braille display driver {display.name} reports as unavailable, excluding")
except:
log.error("", exc_info=True)
displayList.sort(key=lambda d: strxfrm(d[1]))
if lastDisplay:
displayList.append(lastDisplay)
return displayList


class Region(object):
"""A region of braille to be displayed.
Each portion of braille to be displayed is represented by a region.
Expand Down Expand Up @@ -2613,9 +2608,19 @@ def handlePostConfigProfileSwitch(self):
# The display in the new profile is equal to the last requested display name
display == self._lastRequestedDisplayName
# or the new profile uses auto detection, which supports detection of the currently active display.
or (display == AUTO_DISPLAY_NAME and bdDetect.driverSupportsAutoDetection(self.display.name))
or (display == AUTO_DISPLAY_NAME and bdDetect.driverIsEnabledForAutoDetection(self.display.name))
):
self.setDisplayByName(display)
elif (
# Auto detection should be active
display == AUTO_DISPLAY_NAME and self._detector is not None
# And the current display should be no braille.
# If not, there is an active detector for the current driver
# to switch from bluetooth to USB.
and self.display.name == NO_BRAILLE_DISPLAY_NAME
):
self._detector._limitToDevices = bdDetect.getBrailleDisplayDriversEnabledForDetection()

self._tether = config.conf["braille"]["tetherTo"]

def handleDisplayUnavailable(self):
Expand Down Expand Up @@ -2757,6 +2762,11 @@ class BrailleDisplayDriver(driverHandler.Driver):
At a minimum, drivers must set L{name} and L{description} and override the L{check} method.
To display braille, L{numCells} and L{display} must be implemented.
To support automatic detection of braille displays belonging to this driver:
* The driver must be thread safe and L{isThreadSafe} should be set to C{True}
* L{supportsAutomaticDetection} must be set to C{True}.
* L{registerAutomaticDetection} must be implemented.
Drivers should dispatch input such as presses of buttons, wheels or other controls
using the L{inputCore} framework.
They should subclass L{BrailleDisplayGesture}
Expand All @@ -2779,19 +2789,19 @@ class BrailleDisplayDriver(driverHandler.Driver):
#: which means the rest of NVDA is not blocked while this occurs,
#: thus resulting in better performance.
#: This is also required to use the L{hwIo} module.
#: @type: bool
isThreadSafe = False
isThreadSafe: bool = False
#: Whether this driver is supported for automatic detection of braille displays.
supportsAutomaticDetection: bool = False
#: Whether displays for this driver return acknowledgements for sent packets.
#: L{_handleAck} should be called when an ACK is received.
#: Note that thread safety is required for the generic implementation to function properly.
#: If a display is not thread safe, a driver should manually implement ACK processing.
#: @type: bool
receivesAckPackets = False
receivesAckPackets: bool = False
#: Whether this driver is awaiting an Ack for a connected display.
#: This is set to C{True} after displaying cells when L{receivesAckPackets} is True,
#: and set to C{False} by L{_handleAck} or when C{timeout} has elapsed.
#: This is for internal use by NVDA core code only and shouldn't be touched by a driver itself.
_awaitingAck = False
_awaitingAck: bool = False
#: Maximum timeout to use for communication with a device (in seconds).
#: This can be used for serial connections.
#: Furthermore, it is used to stop waiting for missed acknowledgement packets.
Expand All @@ -2809,24 +2819,43 @@ def __init__(self, port: typing.Union[None, str, bdDetect.DeviceMatch] = None):
super().__init__()

@classmethod
def check(cls):
def check(cls) -> bool:
"""Determine whether this braille display is available.
The display will be excluded from the list of available displays if this method returns C{False}.
For example, if this display is not present, C{False} should be returned.
@return: C{True} if this display is available, C{False} if not.
@rtype: bool
"""
if cls.isThreadSafe:
if bdDetect.driverHasPossibleDevices(cls.name):
return True
try:
next(cls.getManualPorts())
except (StopIteration, NotImplementedError):
pass
else:
supportsAutomaticDetection = cls.supportsAutomaticDetection
if not supportsAutomaticDetection and NVDAState._allowDeprecatedAPI() and version_year < 2024:
log.warning(
"Starting from NVDA 2024.1, drivers that rely on bdDetect for the default check method "
"should have supportsAutomaticDetection set to True"
)
supportsAutomaticDetection = True
if supportsAutomaticDetection and bdDetect.driverHasPossibleDevices(cls.name):
return True
try:
next(cls.getManualPorts())
except (StopIteration, NotImplementedError):
pass
else:
return True
return False

@classmethod
def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
"""
This method may register the braille display driver in the braille display automatic detection framework.
The framework provides a L{bdDetect.DriverRegistrar} object as its only parameter.
The methods on the driver registrar can be used to register devices or device scanners.
This method should only register itself with the bdDetect framework,
and should refrain from doing anything else.
Drivers with L{supportsAutomaticDetection} set to C{True} must implement this method.
@param driverRegistrar: An object containing several methods to register device identifiers for this driver.
"""
raise NotImplementedError

def terminate(self):
"""Terminate this display driver.
This will be called when NVDA is finished with this display driver.
Expand Down Expand Up @@ -3227,3 +3256,26 @@ def getSerialPorts(filterFunc=None) -> typing.Iterator[typing.Tuple[str, str]]:
yield (info["port"],
# Translators: Name of a serial communications port.
_("Serial: {portName}").format(portName=info["friendlyName"]))


def getDisplayDrivers(
filterFunc: Optional[Callable[[Type[BrailleDisplayDriver]], bool]] = None
) -> Generator[Type[BrailleDisplayDriver], Any, Any]:
"""Gets an iterator of braille display drivers meeting the given filter callable.
@param filterFunc: an optional callable that receives a driver as its only argument and returns
either True or False.
@return: Iterator of braille display drivers.
"""
for loader, name, isPkg in pkgutil.iter_modules(brailleDisplayDrivers.__path__):
if name.startswith('_'):
continue
try:
display = _getDisplayDriver(name)
except Exception:
log.error(
f"Error while importing braille display driver {name}",
exc_info=True
)
continue
if not filterFunc or filterFunc(display):
yield display
8 changes: 8 additions & 0 deletions source/brailleDisplayDrivers/albatross/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import time

from collections import deque
from bdDetect import KEY_SERIAL, DriverRegistrar
from logHandler import log
from serial.win32 import (
PURGE_RXABORT,
Expand Down Expand Up @@ -79,6 +80,13 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
# Translators: Names of braille displays.
description = _("Caiku Albatross 46/80")
isThreadSafe = True
supportsAutomaticDetection = True

@classmethod
def registerAutomaticDetection(cls, driverRegistrar: DriverRegistrar):
driverRegistrar.addUsbDevices(KEY_SERIAL, {
"VID_0403&PID_6001", # Caiku Albatross 46/80
})

@classmethod
def getManualPorts(cls):
Expand Down
11 changes: 11 additions & 0 deletions source/brailleDisplayDrivers/alva.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,22 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver, ScriptableObject):
# Translators: The name of a braille display.
description = _("Optelec ALVA 6 series/protocol converter")
isThreadSafe = True
supportsAutomaticDetection = True
timeout = 0.2
supportedSettings = (
braille.BrailleDisplayDriver.HIDInputSetting(useConfig=False),
)

@classmethod
def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
driverRegistrar.addUsbDevices(bdDetect.KEY_HID, {
"VID_0798&PID_0640", # BC640
"VID_0798&PID_0680", # BC680
"VID_0798&PID_0699", # USB protocol converter
})

driverRegistrar.addBluetoothDevices(lambda m: m.id.startswith("ALVA "))

@classmethod
def getManualPorts(cls):
return braille.getSerialPorts(filterFunc=lambda info: info.get("bluetoothName","").startswith("ALVA "))
Expand Down
61 changes: 61 additions & 0 deletions source/brailleDisplayDrivers/baum.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,67 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
# Translators: Names of braille displays.
description = _("Baum/HumanWare/APH/Orbit braille displays")
isThreadSafe = True
supportsAutomaticDetection = True

@classmethod
def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
driverRegistrar.addUsbDevices(bdDetect.KEY_HID, {
"VID_0904&PID_3001", # RefreshaBraille 18
"VID_0904&PID_6101", # VarioUltra 20
"VID_0904&PID_6103", # VarioUltra 32
"VID_0904&PID_6102", # VarioUltra 40
"VID_0904&PID_4004", # Pronto! 18 V3
"VID_0904&PID_4005", # Pronto! 40 V3
"VID_0904&PID_4007", # Pronto! 18 V4
"VID_0904&PID_4008", # Pronto! 40 V4
"VID_0904&PID_6001", # SuperVario2 40
"VID_0904&PID_6002", # SuperVario2 24
"VID_0904&PID_6003", # SuperVario2 32
"VID_0904&PID_6004", # SuperVario2 64
"VID_0904&PID_6005", # SuperVario2 80
"VID_0904&PID_6006", # Brailliant2 40
"VID_0904&PID_6007", # Brailliant2 24
"VID_0904&PID_6008", # Brailliant2 32
"VID_0904&PID_6009", # Brailliant2 64
"VID_0904&PID_600A", # Brailliant2 80
"VID_0904&PID_6201", # Vario 340
"VID_0483&PID_A1D3", # Orbit Reader 20
"VID_0904&PID_6301", # Vario 4
})

driverRegistrar.addUsbDevices(bdDetect.KEY_SERIAL, {
"VID_0403&PID_FE70", # Vario 40
"VID_0403&PID_FE71", # PocketVario
"VID_0403&PID_FE72", # SuperVario/Brailliant 40
"VID_0403&PID_FE73", # SuperVario/Brailliant 32
"VID_0403&PID_FE74", # SuperVario/Brailliant 64
"VID_0403&PID_FE75", # SuperVario/Brailliant 80
"VID_0904&PID_2001", # EcoVario 24
"VID_0904&PID_2002", # EcoVario 40
"VID_0904&PID_2007", # VarioConnect/BrailleConnect 40
"VID_0904&PID_2008", # VarioConnect/BrailleConnect 32
"VID_0904&PID_2009", # VarioConnect/BrailleConnect 24
"VID_0904&PID_2010", # VarioConnect/BrailleConnect 64
"VID_0904&PID_2011", # VarioConnect/BrailleConnect 80
"VID_0904&PID_2014", # EcoVario 32
"VID_0904&PID_2015", # EcoVario 64
"VID_0904&PID_2016", # EcoVario 80
"VID_0904&PID_3000", # RefreshaBraille 18
})

driverRegistrar.addBluetoothDevices(lambda m: any(m.id.startswith(prefix) for prefix in (
"Baum SuperVario",
"Baum PocketVario",
"Baum SVario",
"HWG Brailliant",
"Refreshabraille",
"VarioConnect",
"BrailleConnect",
"Pronto!",
"VarioUltra",
"Orbit Reader 20",
"Vario 4",
)))

@classmethod
def getManualPorts(cls):
Expand Down
24 changes: 20 additions & 4 deletions source/brailleDisplayDrivers/brailleNote.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#brailleDisplayDrivers/brailleNote.py
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2011-2018 NV access Limited, Rui Batista, Joseph Lee

""" Braille Display driver for the BrailleNote notetakers in terminal mode.
Expand All @@ -13,6 +12,7 @@
from typing import List, Optional

import serial
import bdDetect
import braille
import brailleInput
import inputCore
Expand Down Expand Up @@ -125,6 +125,22 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
# Translators: Names of braille displays
description = _("HumanWare BrailleNote")
isThreadSafe = True
supportsAutomaticDetection = True

@classmethod
def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
driverRegistrar.addUsbDevices(bdDetect.KEY_SERIAL, {
"VID_1C71&PID_C004", # Apex
})
driverRegistrar.addBluetoothDevices(lambda m: (
any(
first <= m.deviceInfo.get("bluetoothAddress", 0) <= last
for first, last in (
(0x0025EC000000, 0x0025EC01869F), # Apex
)
)
or m.id.startswith("Braillenote")
))

@classmethod
def getManualPorts(cls):
Expand Down
Loading

0 comments on commit 104de7c

Please sign in to comment.