From 846a998b363fd5abaf447e5148bc272fefdf161c Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Tue, 17 Jul 2018 08:57:13 +0200 Subject: [PATCH] Add automatic background braille display detection (#7741) * Implement braille display auto detection. This new background mechanism can be activated by choosing the Automatic option from NVDA's braille display settings. See #1271 for an in depth discussion of funcionality. * brailliantB, use generic implementation for USB serial devices * Use generic check function for braille display drivers supporting bdDetect * Make auto detection the default * Set a timeout for joining the bgThread when auto detection was on * Poll for bluetooth devices on app switches * Support bluetooth HID in bdDetect * Use a separate thread for background scanning. * Make the bdDetect thread a daemon thread * Disable auto detection within the unit test framework. If we don't do this, detection will occur and unit tests will fail. * in braille.handler.handleGainFocus, check whether the focused object has an active tree interceptor. If so, focus the tree interceptor instead * Revert the use a separate thread for background scanning and make sure recursion does not occur when using an APC This reverts commit 5b97f3952348fe80db8ab06b1e9a008c105d48d0. * Created Detector._scanQueuedSafe which wraps changing the state of _scanQueued within a lock * Fix malformed VID and PID for Brailliant, add an extra check * NO longer filter serial ports for Brailliant * Updated changes.t2t --- source/appModuleHandler.py | 10 + source/bdDetect.py | 530 ++++++++++++++++++++ source/braille.py | 313 ++++++++++-- source/brailleDisplayDrivers/alva.py | 57 +-- source/brailleDisplayDrivers/baum.py | 123 +---- source/brailleDisplayDrivers/brailleNote.py | 70 +-- source/brailleDisplayDrivers/brailliantB.py | 72 +-- source/brailleDisplayDrivers/eurobraille.py | 70 +-- source/brailleDisplayDrivers/handyTech.py | 100 +--- source/brailleDisplayDrivers/hims.py | 125 +---- source/brailleDisplayDrivers/superBrl.py | 44 +- source/config/configSpec.py | 2 +- source/core.py | 14 + source/globalCommands.py | 13 +- source/gui/settingsDialogs.py | 39 +- source/hwIo.py | 28 +- source/hwPortUtils.py | 29 +- source/winKernel.py | 74 ++- source/winUser.py | 3 + tests/unit/__init__.py | 2 + user_docs/en/changes.t2t | 8 + user_docs/en/userGuide.t2t | 44 +- 22 files changed, 1094 insertions(+), 676 deletions(-) create mode 100644 source/bdDetect.py diff --git a/source/appModuleHandler.py b/source/appModuleHandler.py index e0a3b058ae9..082e76ed9d9 100644 --- a/source/appModuleHandler.py +++ b/source/appModuleHandler.py @@ -33,6 +33,7 @@ import api import appModules import watchdog +import extensionPoints #Dictionary of processID:appModule paires used to hold the currently running modules runningTable={} @@ -40,6 +41,12 @@ NVDAProcessID=None _importers=None _getAppModuleLock=threading.RLock() +#: Notifies when another application is taking foreground. +#: This allows components to react upon application switches. +#: For example, braille triggers bluetooth polling for braille displaysf necessary. +#: Handlers are called with no arguments. +post_appSwitch = extensionPoints.Action() + class processEntry32W(ctypes.Structure): _fields_ = [ @@ -237,6 +244,9 @@ def handleAppSwitch(oldMods, newMods): processed = set() nextStage = [] + if not oldMods or oldMods[-1].appName != newMods[-1].appName: + post_appSwitch.notify() + # Determine all apps that are losing focus and fire appropriate events. for mod in reversed(oldMods): if mod in processed: diff --git a/source/bdDetect.py b/source/bdDetect.py new file mode 100644 index 00000000000..fb2ba0d07ed --- /dev/null +++ b/source/bdDetect.py @@ -0,0 +1,530 @@ +#bdDetect.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. +#Copyright (C) 2013-2017 NV Access Limited + +"""Support for braille display detection. +This allows devices to be automatically detected and used when they become available, +as well as providing utilities to query for possible devices for a particular driver. +To support detection for a driver, devices need to be associated +using the C{add*} functions. +Drivers distributed with NVDA do this at the bottom of this module. +For drivers in add-ons, this must be done in a global plugin. +""" + +import itertools +from collections import namedtuple, defaultdict, OrderedDict +import threading +import wx +import hwPortUtils +import braille +import winKernel +import core +import ctypes +from logHandler import log +import config +import time +import thread +from win32con import WM_DEVICECHANGE, DBT_DEVNODES_CHANGED +import appModuleHandler +from baseObject import AutoPropertyObject +import re + +_driverDevices = OrderedDict() +USB_ID_REGEX = re.compile(r"^VID_[0-9A-F]{4}&PID_[0-9A-F]{4}$", re.U) + +class DeviceMatch( + namedtuple("DeviceMatch", ("type","id", "port", "deviceInfo")) +): + """Represents a detected device. + @ivar id: The identifier of the device. + @type id: unicode + @ivar port: The port that can be used by a driver to communicate with a device. + @type port: unicode + @ivar deviceInfo: all known information about a device. + @type deviceInfo: dict + """ + __slots__ = () + +# Device type constants +#: Key constant for HID devices +KEY_HID = "hid" +#: Key for serial devices (COM ports) +KEY_SERIAL = "serial" +#: Key for devices with a manufacturer specific driver +KEY_CUSTOM = "custom" +#: Key for bluetooth devices +KEY_BLUETOOTH = "bluetooth" + +# Constants for USB and bluetooth detection to be used by the background thread scanner. +DETECT_USB = 1 +DETECT_BLUETOOTH = 2 + +def _isDebug(): + return config.conf["debugLog"]["hwIo"] + +def _getDriver(driver): + try: + return _driverDevices[driver] + except KeyError: + ret = _driverDevices[driver] = defaultdict(set) + return ret + +def addUsbDevices(driver, type, ids): + """Associate USB devices with a driver. + @param driver: The name of the driver. + @type driver: str + @param type: The type of the driver, either C{KEY_HID}, C{KEY_SERIAL} or C{KEY_CUSTOM}. + @type type: str + @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. + @type ids: set of str + @raise ValueError: When one of the provided IDs is malformed. + """ + malformedIds = [id for id in ids if not isinstance(id, basestring) or not USB_ID_REGEX.match(id)] + if malformedIds: + raise ValueError("Invalid IDs provided for driver %s, type %s: %s" + % (driver, type, ", ".join(wrongIds))) + devs = _getDriver(driver) + driverUsb = devs[type] + driverUsb.update(ids) + +def addBluetoothDevices(driver, matchFunc): + """Associate Bluetooth HID or COM ports with a driver. + @param driver: The name of the driver. + @type driver: str + @param matchFunc: A function which determines whether a given Bluetooth device matches. + It takes a L{DeviceMatch} as its only argument + and returns a C{bool} indicating whether it matched. + @type matchFunc: callable + """ + devs = _getDriver(driver) + devs[KEY_BLUETOOTH] = matchFunc + +def getDriversForConnectedUsbDevices(): + """Get any matching drivers for connected USB devices. + @return: Pairs of drivers and device information. + @rtype: generator of (str, L{DeviceMatch}) tuples + """ + usbDevs = itertools.chain( + (DeviceMatch(KEY_CUSTOM, port["usbID"], port["devicePath"], port) + for port in deviceInfoFetcher.usbDevices), + (DeviceMatch(KEY_HID, port["usbID"], port["devicePath"], port) + for port in deviceInfoFetcher.hidDevices if port["provider"]=="usb"), + (DeviceMatch(KEY_SERIAL, port["usbID"], port["port"], port) + for port in deviceInfoFetcher.comPorts if "usbID" in port) + ) + for match in usbDevs: + for driver, devs in _driverDevices.iteritems(): + for type, ids in devs.iteritems(): + if match.type==type and match.id in ids: + yield driver, match + +def getDriversForPossibleBluetoothDevices(): + """Get any matching drivers for possible Bluetooth devices. + @return: Pairs of drivers and port information. + @rtype: generator of (str, L{DeviceMatch}) tuples + """ + btDevs = itertools.chain( + (DeviceMatch(KEY_SERIAL, port["bluetoothName"], port["port"], port) + for port in deviceInfoFetcher.comPorts + if "bluetoothName" in port), + (DeviceMatch(KEY_HID, port["hardwareID"], port["devicePath"], port) + for port in deviceInfoFetcher.hidDevices if port["provider"]=="bluetooth"), + ) + for match in btDevs: + for driver, devs in _driverDevices.iteritems(): + matchFunc = devs[KEY_BLUETOOTH] + if not callable(matchFunc): + continue + if matchFunc(match): + yield driver, match + +class _DeviceInfoFetcher(AutoPropertyObject): + """Utility class that caches fetched info for available devices for the duration of one core pump cycle.""" + cachePropertiesByDefault = True + + def _get_comPorts(self): + return list(hwPortUtils.listComPorts(onlyAvailable=True)) + + def _get_usbDevices(self): + return list(hwPortUtils.listUsbDevices(onlyAvailable=True)) + + def _get_hidDevices(self): + return list(hwPortUtils.listHidDevices(onlyAvailable=True)) + +#: The single instance of the device info fetcher. +#: @type: L{_DeviceInfoFetcher} +deviceInfoFetcher = _DeviceInfoFetcher() + +class Detector(object): + """Automatically detect braille displays. + This should only be used by the L{braille} module. + """ + + def __init__(self): + self._BgScanApc = winKernel.PAPCFUNC(self._bgScan) + self._btDevsLock = threading.Lock() + self._btDevs = None + core.post_windowMessageReceipt.register(self.handleWindowMessage) + appModuleHandler.post_appSwitch.register(self.pollBluetoothDevices) + self._stopEvent = threading.Event() + self._queuedScanLock = threading.Lock() + self._scanQueued = False + self._detectUsb = False + self._detectBluetooth = False + self._runningApcLock = threading.Lock() + # Perform initial scan. + self._startBgScan(usb=True, bluetooth=True) + + @property + def _scanQueuedSafe(self): + """Returns L{_scanQueued} in a thread safe way by using L{_queuedScanLock}.""" + with self._queuedScanLock: + return self._scanQueued + + @_scanQueuedSafe.setter + def _scanQueuedSafe(self, state): + """Sets L{_scanQueued} in a thread safe way by using L{_queuedScanLock}.""" + with self._queuedScanLock: + self._scanQueued = state + + def _startBgScan(self, usb=False, bluetooth=False): + with self._queuedScanLock: + self._detectUsb = usb + self._detectBluetooth = bluetooth + if not self._scanQueued: + self._scanQueued = True + if self._runningApcLock.locked(): + # There's currently a scan in progress. + # Since the scan is embeded in a loop, it will automatically do another scan, + # unless a display has been found. + return + braille._BgThread.queueApc(self._BgScanApc) + + def _stopBgScan(self): + """Stops the current scan as soon as possible and prevents a queued scan to start.""" + if not self._runningApcLock.locked(): + # No scan to stop + return + self._stopEvent.set() + self._scanQueuedSafe = False + + def _bgScan(self, param): + if self._runningApcLock.locked(): + log.debugWarning("Braille display detection background scan APC executed while one is already running") + return + with self._runningApcLock: + while self._scanQueuedSafe: + # Clear the stop event before a scan is started. + # Since a scan can take some time to complete, another thread can set the stop event to cancel it. + self._stopEvent.clear() + with self._queuedScanLock: + self._scanQueued = False + detectUsb = self._detectUsb + detectBluetooth = self._detectBluetooth + if detectUsb: + if self._stopEvent.isSet(): + continue + for driver, match in getDriversForConnectedUsbDevices(): + if self._stopEvent.isSet(): + continue + if braille.handler.setDisplayByName(driver, detected=match): + return + if detectBluetooth: + if self._stopEvent.isSet(): + continue + with self._btDevsLock: + if self._btDevs is None: + btDevs = list(getDriversForPossibleBluetoothDevices()) + # Cache Bluetooth devices for next time. + btDevsCache = [] + else: + btDevs = self._btDevs + btDevsCache = btDevs + for driver, match in btDevs: + if self._stopEvent.isSet(): + continue + if btDevsCache is not btDevs: + btDevsCache.append((driver, match)) + if braille.handler.setDisplayByName(driver, detected=match): + return + if self._stopEvent.isSet(): + continue + if btDevsCache is not btDevs: + with self._btDevsLock: + self._btDevs = btDevsCache + + def rescan(self): + """Stop a current scan when in progress, and start scanning from scratch.""" + self._stopBgScan() + with self._btDevsLock: + # A Bluetooth com port or HID device might have been added. + self._btDevs = None + self._startBgScan(usb=True, bluetooth=True) + + def handleWindowMessage(self, msg=None, wParam=None): + if msg == WM_DEVICECHANGE and wParam == DBT_DEVNODES_CHANGED: + self.rescan() + + def pollBluetoothDevices(self): + """Poll bluetooth devices that might be in range. + This does not cancel the current scan.""" + with self._btDevsLock: + if not self._btDevs: + return + self._startBgScan(bluetooth=True) + + def terminate(self): + appModuleHandler.post_appSwitch.unregister(self.pollBluetoothDevices) + core.post_windowMessageReceipt.unregister(self.handleWindowMessage) + self._stopBgScan() + +def getConnectedUsbDevicesForDriver(driver): + """Get any connected USB devices associated with a particular driver. + @param driver: The name of the driver. + @type driver: str + @return: Device information for each device. + @rtype: generator of L{DeviceMatch} + @raise LookupError: If there is no detection data for this driver. + """ + devs = _driverDevices[driver] + usbDevs = itertools.chain( + (DeviceMatch(KEY_CUSTOM, port["usbID"], port["devicePath"], port) + for port in deviceInfoFetcher.usbDevices), + (DeviceMatch(KEY_HID, port["usbID"], port["devicePath"], port) + for port in deviceInfoFetcher.hidDevices if port["provider"]=="usb"), + (DeviceMatch(KEY_SERIAL, port["usbID"], port["port"], port) + for port in deviceInfoFetcher.comPorts if "usbID" in port) + ) + for match in usbDevs: + for type, ids in devs.iteritems(): + if match.type==type and match.id in ids: + yield match + +def getPossibleBluetoothDevicesForDriver(driver): + """Get any possible Bluetooth devices associated with a particular driver. + @param driver: The name of the driver. + @type driver: str + @return: Port information for each port. + @rtype: generator of L{DeviceMatch} + @raise LookupError: If there is no detection data for this driver. + """ + matchFunc = _driverDevices[driver][KEY_BLUETOOTH] + if not callable(matchFunc): + return + btDevs = itertools.chain( + (DeviceMatch(KEY_SERIAL, port["bluetoothName"], port["port"], port) + for port in deviceInfoFetcher.comPorts + if "bluetoothName" in port), + (DeviceMatch(KEY_HID, port["hardwareID"], port["devicePath"], port) + for port in deviceInfoFetcher.hidDevices if port["provider"]=="bluetooth"), + ) + for match in btDevs: + if matchFunc(match): + yield match + +def driverHasPossibleDevices(driver): + """Determine whether there are any possible devices associated with a given driver. + @param driver: The name of the driver. + @type driver: str + @return: C{True} if there are possible devices, C{False} otherwise. + @rtype: bool + @raise LookupError: If there is no detection data for this driver. + """ + return bool(next(itertools.chain( + getConnectedUsbDevicesForDriver(driver), + getPossibleBluetoothDevicesForDriver(driver) + ), None)) + +def driverSupportsAutoDetection(driver): + """Returns whether the provided driver supports automatic detection of displays. + @param driver: The name of the driver. + @type driver: str + @return: C{True} if de driver supports auto detection, C{False} otherwise. + @rtype: bool + """ + return driver in _driverDevices + +### Detection data +# alva +addUsbDevices("alva", KEY_HID, { + "VID_0798&PID_0640", # BC640 + "VID_0798&PID_0680", # BC680 + "VID_0798&PID_0699", # USB protocol converter +}) + +addBluetoothDevices("alva", lambda m: m.id.startswith("ALVA ")) + +# baum +addUsbDevices("baum", 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 +}) + +addUsbDevices("baum", 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 +}) + +addBluetoothDevices("baum", 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", +))) + +# brailleNote +addUsbDevices("brailleNote", KEY_SERIAL, { + "VID_1C71&PID_C004", # Apex +}) +addBluetoothDevices("brailleNote", lambda m: + any(first <= m.deviceInfo.get("bluetoothAddress",0) <= last for first, last in ( + (0x0025EC000000, 0x0025EC01869F), # Apex + )) or m.id.startswith("Braillenote")) + +# brailliantB +addUsbDevices("brailliantB", KEY_HID, { + "VID_1C71&PID_C006", # Brailliant BI 32, 40 and 80 + "VID_1C71&PID_C022", # Brailliant BI 14 + "VID_1C71&PID_C00A", # BrailleNote Touch +}) +addUsbDevices("brailliantB", KEY_SERIAL, { + "VID_1C71&PID_C005", # Brailliant BI 32, 40 and 80 + "VID_1C71&PID_C021", # Brailliant BI 14 +}) +addBluetoothDevices("brailliantB", lambda m: ( + m.type==KEY_SERIAL + and (m.id.startswith("Brailliant B") + or m.id == "Brailliant 80" + or "BrailleNote Touch" in m.id + )) or (m.type==KEY_HID + and m.deviceInfo.get("manufacturer") == "Humanware" + and m.deviceInfo.get("product") == "Brailliant HID" +)) + +# eurobraille +addUsbDevices("eurobraille", KEY_HID, { + "VID_C251&PID_1122", # Esys (version < 3.0, no SD card + "VID_C251&PID_1123", # Esys (version >= 3.0, with HID keyboard, no SD card + "VID_C251&PID_1124", # Esys (version < 3.0, with SD card + "VID_C251&PID_1125", # Esys (version >= 3.0, with HID keyboard, with SD card + "VID_C251&PID_1126", # Esys (version >= 3.0, no SD card + "VID_C251&PID_1127", # Reserved + "VID_C251&PID_1128", # Esys (version >= 3.0, with SD card + "VID_C251&PID_1129", # Reserved + "VID_C251&PID_112A", # Reserved + "VID_C251&PID_112B", # Reserved + "VID_C251&PID_112C", # Reserved + "VID_C251&PID_112D", # Reserved + "VID_C251&PID_112E", # Reserved + "VID_C251&PID_112F", # Reserved + "VID_C251&PID_1130", # Esytime + "VID_C251&PID_1131", # Reserved + "VID_C251&PID_1132", # Reserved +}) + +addBluetoothDevices("eurobraille", lambda m: m.id.startswith("Esys")) + +# handyTech +addUsbDevices("handyTech", KEY_SERIAL, { + "VID_0403&PID_6001", # FTDI chip + "VID_0921&PID_1200", # GoHubs chip +}) + +# Newer Handy Tech displays have a native HID processor +addUsbDevices("handyTech", KEY_HID, { + "VID_1FE4&PID_0054", # Active Braille + "VID_1FE4&PID_0081", # Basic Braille 16 + "VID_1FE4&PID_0082", # Basic Braille 20 + "VID_1FE4&PID_0083", # Basic Braille 32 + "VID_1FE4&PID_0084", # Basic Braille 40 + "VID_1FE4&PID_008A", # Basic Braille 48 + "VID_1FE4&PID_0086", # Basic Braille 64 + "VID_1FE4&PID_0087", # Basic Braille 80 + "VID_1FE4&PID_008B", # Basic Braille 160 + "VID_1FE4&PID_0061", # Actilino + "VID_1FE4&PID_0064", # Active Star 40 +}) + +# Some older HT displays use a HID converter and an internal serial interface +addUsbDevices("handyTech", KEY_HID, { + "VID_1FE4&PID_0003", # USB-HID adapter + "VID_1FE4&PID_0074", # Braille Star 40 + "VID_1FE4&PID_0044", # Easy Braille +}) + +addBluetoothDevices("handyTech", lambda m: any(m.id.startswith(prefix) for prefix in ( + "Actilino AL", + "Active Braille AB", + "Active Star AS", + "Basic Braille BB", + "Braille Star 40 BS", + "Braillino BL", + "Braille Wave BW", + "Easy Braille EBR", +))) + +# hims +# Bulk devices +addUsbDevices("hims", KEY_CUSTOM, { + "VID_045E&PID_930A", # Braille Sense & Smart Beetle + "VID_045E&PID_930B", # Braille EDGE 40 +}) + +# Sync Braille, serial device +addUsbDevices("hims", KEY_SERIAL, { + "VID_0403&PID_6001", +}) + +addBluetoothDevices("hims", lambda m: any(m.id.startswith(prefix) for prefix in ( + "BrailleSense", + "BrailleEDGE", + "SmartBeetle", +))) + +# superBrl +addUsbDevices("superBrl", KEY_SERIAL, { + "VID_10C4&PID_EA60", # SuperBraille 3.2 +}) + diff --git a/source/braille.py b/source/braille.py index 9b274cc346d..667a4fddb03 100644 --- a/source/braille.py +++ b/source/braille.py @@ -25,9 +25,13 @@ import brailleDisplayDrivers import inputCore import brailleTables -from collections import namedtuple import re import scriptHandler +import collections +import extensionPoints +import hwPortUtils +import bdDetect +import winUser roleLabels = { # Translators: Displayed in braille for an object which is a @@ -271,7 +275,25 @@ ] #: Named tuple for a region with start and end positions in a buffer -RegionWithPositions = namedtuple("RegionWithPositions",("region","start","end")) +RegionWithPositions = collections.namedtuple("RegionWithPositions",("region","start","end")) + +#: Automatic constant to be used by braille displays that support the "automatic" port +#: and automatic braille display detection +#: @type: tuple +# Translators: String representing automatic port selection for braille displays. +AUTOMATIC_PORT = ("auto", _("Automatic")) +#: Used in place of a specific braille display driver name to indicate that +#: braille displays should be automatically detected and used. +#: @type: str +AUTO_DISPLAY_NAME = AUTOMATIC_PORT[0] +#: A port name which indicates that USB should be used. +#: @type: tuple +# Translators: String representing the USB port selection for braille displays. +USB_PORT = ("usb", _("USB")) +#: A port name which indicates that Bluetooth should be used. +#: @type: tuple +# Translators: String representing the Bluetooth port selection for braille displays. +BLUETOOTH_PORT = ("bluetooth", _("Bluetooth")) def NVDAObjectHasUsefulText(obj): import displayModel @@ -1528,8 +1550,12 @@ def __init__(self): self._cursorBlinkTimer = None config.configProfileSwitched.register(self.handleConfigProfileSwitch) self._tether = config.conf["braille"]["tetherTo"] + self._detectionEnabled = False + self._detector = None def terminate(self): + bgThreadStopTimeout = 2.5 if self._detectionEnabled else None + self._disableDetection() if self._messageCallLater: self._messageCallLater.Stop() self._messageCallLater = None @@ -1540,7 +1566,7 @@ def terminate(self): if self.display: self.display.terminate() self.display = None - _BgThread.stop() + _BgThread.stop(timeout=bgThreadStopTimeout) def getTether(self): return self._tether @@ -1566,34 +1592,58 @@ def _set_tether(self, tether): def _get_shouldAutoTether(self): return self.enabled and config.conf["braille"]["autoTether"] - def setDisplayByName(self, name, isFallback=False): - if not name: - self.display = None - self.displaySize = 0 - return - # See if the user have defined a specific port to connect to - if name not in config.conf["braille"]: - # No port was set. - config.conf["braille"][name] = {"port" : ""} - port = config.conf["braille"][name].get("port") - # Here we try to keep compatible with old drivers that don't support port setting - # or situations where the user hasn't set any port. + _lastRequestedDisplayName=None #: the name of the last requested braille display driver with setDisplayByName, even if it failed and has fallen back to no braille. + def setDisplayByName(self, name, isFallback=False, detected=None): + if not isFallback: + # #8032: Take note of the display requested, even if it is going to fail. + self._lastRequestedDisplayName=name + if name == AUTO_DISPLAY_NAME: + self._enableDetection() + return True + elif not isFallback and not detected: + self._disableDetection() + kwargs = {} - if port: - kwargs["port"] = port + if detected: + kwargs["port"]=detected + else: + # See if the user has defined a specific port to connect to + if name not in config.conf["braille"]: + # No port was set. + config.conf["braille"][name] = {"port" : ""} + port = config.conf["braille"][name].get("port") + # Here we try to keep compatible with old drivers that don't support port setting + # or situations where the user hasn't set any port. + if port: + kwargs["port"] = port + try: newDisplay = _getDisplayDriver(name) + if detected and bdDetect._isDebug(): + log.debug("Possibly detected display '%s'" % newDisplay.description) if newDisplay == self.display.__class__: # This is the same driver as was already set, so just re-initialise it. + log.debug("Reinitializing %s braille display"%name) self.display.terminate() newDisplay = self.display - newDisplay.__init__(**kwargs) + try: + newDisplay.__init__(**kwargs) + except TypeError: + # Re-initialize with supported kwargs. + extensionPoints.callWithSupportedKwargs(newDisplay.__init__, **kwargs) else: - if newDisplay.isThreadSafe: + if newDisplay.isThreadSafe and not detected: # Start the thread if it wasn't already. + # Auto detection implies the thread is already started. _BgThread.start() - newDisplay = newDisplay(**kwargs) + try: + newDisplay = newDisplay(**kwargs) + except TypeError: + newDisplay = newDisplay.__new__(newDisplay) + # initialize with supported kwargs. + extensionPoints.callWithSupportedKwargs(newDisplay.__init__, **kwargs) if self.display: + log.debug("Switching braille display from %s to %s"%(self.display.name,name)) try: self.display.terminate() except: @@ -1601,12 +1651,21 @@ def setDisplayByName(self, name, isFallback=False): self.display = newDisplay self.displaySize = newDisplay.numCells self.enabled = bool(self.displaySize) - if not isFallback: + if isFallback: + self._resumeDetection() + elif not detected: config.conf["braille"]["display"] = name + else: # detected: + self._disableDetection() log.info("Loaded braille display driver %s, current display has %d cells." %(name, self.displaySize)) + self.initialDisplay() return True except: - log.error("Error initializing display driver", exc_info=True) + # For auto display detection, logging an error for every failure is too obnoxious. + if not detected: + log.error("Error initializing display driver for kwargs %r"%kwargs, exc_info=True) + elif bdDetect._isDebug(): + log.debugWarning("Couldn't initialize display driver for kwargs %r"%(kwargs,), exc_info=True) self.setDisplayByName("noBraille", isFallback=True) return False @@ -1622,7 +1681,9 @@ def _updateDisplay(self): blinkRate = config.conf["braille"]["cursorBlinkRate"] if cursorShouldBlink and blinkRate: self._cursorBlinkTimer = wx.PyTimer(self._blink) - self._cursorBlinkTimer.Start(blinkRate) + # This is called from the background thread when a display is auto detected. + # Make sure we start the blink timer from the main thread to avoid wx assertions + wx.CallAfter(self._cursorBlinkTimer.Start,blinkRate) def _writeCells(self, cells): if not self.display.isThreadSafe: @@ -1630,7 +1691,7 @@ def _writeCells(self, cells): self.display.display(cells) except: log.error("Error displaying cells. Disabling display", exc_info=True) - self.setDisplayByName("noBraille", isFallback=True) + self.handleDisplayUnavailable() return with _BgThread.queuedWriteLock: alreadyQueued = _BgThread.queuedWrite @@ -1741,6 +1802,8 @@ def handleGainFocus(self, obj, shouldAutoTether=True): self.setTether(self.TETHER_FOCUS, auto=True) if self._tether != self.TETHER_FOCUS: return + if getattr(obj, "treeInterceptor", None) and not obj.treeInterceptor.passThrough: + obj = obj.treeInterceptor self._doNewObject(itertools.chain(getFocusContextRegions(obj, oldFocusRegions=self.mainBuffer.regions), getFocusRegions(obj))) def _doNewObject(self, regions): @@ -1872,12 +1935,67 @@ def handleReviewMove(self, shouldAutoTether=True): # We're reviewing a different object. self._doNewObject(getFocusRegions(reviewPos.obj, review=True)) + def initialDisplay(self): + if not self.enabled or not api.getDesktopObject(): + # Braille is disabled or focus/review hasn't yet been initialised. + return + if self.tether == self.TETHER_FOCUS: + self.handleGainFocus(api.getFocusObject(), shouldAutoTether=False) + else: + self.handleReviewMove(shouldAutoTether=False) + def handleConfigProfileSwitch(self): display = config.conf["braille"]["display"] - if display != self.display.name: + # Do not choose a new display if: + if not ( + # 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)) + ): self.setDisplayByName(display) self._tether = config.conf["braille"]["tetherTo"] + def handleDisplayUnavailable(self): + """Called when the braille display becomes unavailable. + This logs an error and disables the display. + This is called when displaying cells raises an exception, + but drivers can also call it themselves if appropriate. + """ + log.error("Braille display unavailable. Disabling", exc_info=True) + self._detectionEnabled = config.conf["braille"]["display"] == AUTO_DISPLAY_NAME + self.setDisplayByName("noBraille", isFallback=True) + + def _enableDetection(self): + """Enables automatic detection of braille displays. + When auto detection is already active, this will force a rescan for devices. + """ + if self._detectionEnabled and self._detector: + self._detector.rescan() + return + _BgThread.start() + config.conf["braille"]["display"] = AUTO_DISPLAY_NAME + self.setDisplayByName("noBraille", isFallback=True) + self._detector = bdDetect.Detector() + self._detectionEnabled = True + + def _disableDetection(self): + """Disables automatic detection of braille displays.""" + if not self._detectionEnabled: + return + if self._detector: + self._detector.terminate() + self._detector = None + self._detectionEnabled = False + + def _resumeDetection(self): + """Resumes automatic detection of braille displays. + This is executed when auto detection should be resumed due to loss of display connectivity. + """ + if not self._detectionEnabled or self._detector: + return + self._detector = bdDetect.Detector() + class _BgThread: """A singleton background thread used for background writes and raw braille display I/O. """ @@ -1898,11 +2016,11 @@ def start(cls): cls.ackTimerHandle = winKernel.createWaitableTimer() @classmethod - def queueApc(cls, func): - ctypes.windll.kernel32.QueueUserAPC(func, cls.handle, 0) + def queueApc(cls, func, param=0): + ctypes.windll.kernel32.QueueUserAPC(func, cls.handle, param) @classmethod - def stop(cls): + def stop(cls, timeout=None): if not cls.thread: return cls.exit = True @@ -1912,7 +2030,7 @@ def stop(cls): cls.ackTimerHandle = None # Wake up the thread. It will exit when it sees exit is True. cls.queueApc(cls.executor) - cls.thread.join() + cls.thread.join(timeout) cls.exit = False winKernel.closeHandle(cls.handle) cls.handle = None @@ -1940,7 +2058,7 @@ def executor(param): handler.display.display(data) except: log.error("Error displaying cells. Disabling display", exc_info=True) - handler.setDisplayByName("noBraille", isFallback=True) + handler.handleDisplayUnavailable() else: if handler.display.receivesAckPackets: handler.display._awaitingAck = True @@ -1989,15 +2107,6 @@ def initialize(): config.conf["braille"]["display"] = newDriverName handler.setDisplayByName(config.conf["braille"]["display"]) - # Update the display to the current focus/review position. - if not handler.enabled or not api.getDesktopObject(): - # Braille is disabled or focus/review hasn't yet been initialised. - return - if handler.tether == handler.TETHER_FOCUS: - handler.handleGainFocus(api.getFocusObject(), shouldAutoTether=False) - else: - handler.handleReviewMove(shouldAutoTether=False) - def pumpAll(): """Runs tasks at the end of each core cycle. For now just caret updates.""" handler.handlePendingCaretUpdate() @@ -2060,6 +2169,15 @@ def check(cls): @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: + return True return False def terminate(self): @@ -2091,20 +2209,103 @@ def display(self, cells): """ #: Automatic port constant to be used by braille displays that support the "automatic" port - #: @type: Tupple - # Translators: String representing the automatic port selection for braille displays. - AUTOMATIC_PORT = ("auto", _("Automatic")) + #: Kept for backwards compatibility + AUTOMATIC_PORT = AUTOMATIC_PORT @classmethod def getPossiblePorts(cls): """ Returns possible hardware ports for this driver. - If the driver supports automatic port setting it should return as the first port L{brailleDisplayDriver.AUTOMATIC_PORT} - + Generally, drivers shouldn't implement this method directly. + Instead, they should provide automatic detection data via L{bdDetect} + and implement L{getPossibleManualPorts} if they support manual ports + such as serial ports. @return: ordered dictionary of name : description for each port @rtype: OrderedDict """ + try: + next(bdDetect.getConnectedUsbDevicesForDriver(cls.name)) + usb = True + except (LookupError, StopIteration): + usb = False + try: + next(bdDetect.getPossibleBluetoothDevicesForDriver(cls.name)) + bluetooth = True + except (LookupError, StopIteration): + bluetooth = False + ports = collections.OrderedDict() + if usb or bluetooth: + ports.update((AUTOMATIC_PORT,)) + if usb: + ports.update((USB_PORT,)) + if bluetooth: + ports.update((BLUETOOTH_PORT,)) + try: + ports.update(cls.getManualPorts()) + except NotImplementedError: + pass + return ports + + @classmethod + def _getAutoPorts(cls, usb=True, bluetooth=True): + """Returns possible ports to connect to using L{bdDetect} automatic detection data. + @param usb: Whether to search for USB devices. + @type usb: bool + @param bluetooth: Whether to search for bluetooth devices. + @type bluetooth: bool + @return: The device match for each port. + @rtype: iterable of L{DeviceMatch} + """ + iters = [] + if usb: + iters.append(bdDetect.getConnectedUsbDevicesForDriver(cls.name)) + if bluetooth: + iters.append(bdDetect.getPossibleBluetoothDevicesForDriver(cls.name)) + + try: + for match in itertools.chain(*iters): + yield match + except LookupError: + pass + + @classmethod + def getManualPorts(cls): + """Get possible manual hardware ports for this driver. + This is for ports which cannot be detected automatically + such as serial ports. + @return: The name and description for each port. + @rtype: iterable of basestring, basestring + """ raise NotImplementedError + @classmethod + def _getTryPorts(cls, port): + """Returns the ports for this driver to which a connection attempt should be made. + This generator function is usually used in L{__init__} to connect to the desired display. + @param port: the port to connect to. + @type port: one of basestring or L{bdDetect.DeviceMatch} + @return: The name and description for each port. + @rtype: iterable of basestring, basestring + """ + if isinstance(port, bdDetect.DeviceMatch): + yield port + elif isinstance(port, basestring): + isUsb = port in (AUTOMATIC_PORT[0], USB_PORT[0]) + isBluetooth = port in (AUTOMATIC_PORT[0], BLUETOOTH_PORT[0]) + if not isUsb and not isBluetooth: + # Assume we are connecting to a com port, since these are the only manual ports supported. + try: + portInfo = next(info for info in hwPortUtils.listComPorts() if info["port"]==port) + except StopIteration: + pass + else: + if "bluetoothName" in portInfo: + yield bdDetect.DeviceMatch(bdDetect.KEY_SERIAL, portInfo["bluetoothName"], portInfo["port"], portInfo) + else: + yield bdDetect.DeviceMatch(bdDetect.KEY_SERIAL, portInfo["friendlyName"], portInfo["port"], portInfo) + else: + for match in cls._getAutoPorts(usb=isUsb, bluetooth=isBluetooth): + yield match + #: Global input gesture map for this display driver. #: @type: L{inputCore.GlobalGestureMap} gestureMap = None @@ -2291,3 +2492,27 @@ def getDisplayTextForIdentifier(cls, identifier): return handler.display.description, key inputCore.registerGestureSource("br", BrailleDisplayGesture) + + +def getSerialPorts(filterFunc=None): + """Get available serial ports in a format suitable for L{BrailleDisplayDriver.getManualPorts}. + @param filterFunc: a function executed on every dictionary retrieved using L{hwPortUtils.listComPorts}. + For example, this can be used to filter by USB or Bluetooth com ports. + @type filterFunc: callable + """ + if filterFunc and not callable(filterFunc): + raise ValueError("The provided filterFunc is not callable") + for info in hwPortUtils.listComPorts(): + if filterFunc and not filterFunc(info): + continue + if "bluetoothName" in info: + yield (info["port"], + # Translators: Name of a Bluetooth serial communications port. + _("Bluetooth Serial: {port} ({deviceName})").format( + port=info["port"], + deviceName=info["bluetoothName"] + )) + else: + yield (info["port"], + # Translators: Name of a serial communications port. + _("Serial: {portName}").format(portName=info["friendlyName"])) diff --git a/source/brailleDisplayDrivers/alva.py b/source/brailleDisplayDrivers/alva.py index ecd721934d2..2108a7697d8 100644 --- a/source/brailleDisplayDrivers/alva.py +++ b/source/brailleDisplayDrivers/alva.py @@ -5,7 +5,7 @@ #Copyright (C) 2009-2018 NV Access Limited, Davy Kager, Leonard de Ruijter, Optelec B.V. import serial -import hwPortUtils +import bdDetect import braille from logHandler import log import inputCore @@ -15,7 +15,6 @@ from globalCommands import SCRCAT_BRAILLE import ui from baseObject import ScriptableObject -import wx import time import datetime @@ -89,12 +88,6 @@ "control", "windows", "space", "alt", "enter"), } -USB_IDS = { - "VID_0798&PID_0640", # BC640 - "VID_0798&PID_0680", # BC680 - "VID_0798&PID_0699", # USB protocol converter -} - class BrailleDisplayDriver(braille.BrailleDisplayDriver, ScriptableObject): name = "alva" # Translators: The name of a braille display. @@ -103,34 +96,8 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver, ScriptableObject): timeout = 0.2 @classmethod - def check(cls): - return True - - @classmethod - def getPossiblePorts(cls): - ports = OrderedDict() - comPorts = list(hwPortUtils.listComPorts(onlyAvailable=True)) - try: - next(cls._getAutoPorts(comPorts)) - ports.update((cls.AUTOMATIC_PORT,)) - except StopIteration: - pass - for portInfo in comPorts: - if not portInfo.get("bluetoothName","").startswith("ALVA "): - continue - # Translators: Name of a bluetooth serial communications port. - ports[portInfo["port"]] = _("Bluetooth serial: {portName}").format(portName=portInfo["friendlyName"]) - return ports - - @classmethod - def _getAutoPorts(cls, comPorts): - for portInfo in hwPortUtils.listHidDevices(): - if portInfo.get("usbID","") in USB_IDS: - yield portInfo["devicePath"], "USB HID", portInfo["usbID"] - for portInfo in comPorts: - if not portInfo.get("bluetoothName","").startswith("ALVA "): - continue - yield portInfo["port"], "bluetooth", portInfo["bluetoothName"] + def getManualPorts(cls): + return braille.getSerialPorts(filterFunc=lambda info: info.get("bluetoothName","").startswith("ALVA ")) def _get_model(self): if not self._deviceId: @@ -172,17 +139,14 @@ def __init__(self, port="auto"): self.numCells = 0 self._rawKeyboardInput = False self._deviceId = None - if port == "auto": - tryPorts = self._getAutoPorts(hwPortUtils.listComPorts(onlyAvailable=True)) - else: - tryPorts = ((port, "bluetooth", "ALVA"),) - for port, portType, identifier in tryPorts: - self.isHid = portType == "USB HID" + + for portType, portId, port, portInfo in self._getTryPorts(port): + self.isHid = portType == bdDetect.KEY_HID # Try talking to the display. try: if self.isHid: self._dev = hwIo.Hid(port, onReceive=self._hidOnReceive) - self._deviceId = int(identifier[-2:],16) + self._deviceId = int(portId[-2:],16) else: self._dev = hwIo.Serial(port, timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._ser6OnReceive) # Get the device ID @@ -194,6 +158,7 @@ def __init__(self, port="auto"): else: # No response from display continue except EnvironmentError: + log.debugWarning("", exc_info=True) continue self._updateSettings() if self.numCells: @@ -204,7 +169,7 @@ def __init__(self, port="auto"): self._dev.close() else: - raise RuntimeError("No display found") + raise RuntimeError("No ALVA display found") self._keysDown = set() self._ignoreKeyReleases = False @@ -255,9 +220,7 @@ def _handleInput(self, group, number): # Some internal settings have changed. # For example, split point could have been set, in which case the number of cells changed. # We must handle these properly. - # Call this on the main thread, to make sure that we can wait for reads when in non-HID mode. - # This can probably be changed when #1271 is implemented. - wx.CallAfter(self._updateSettings) + self._updateSettings() return isRelease = bool(group & ALVA_RELEASE_MASK) group = group & ~ALVA_RELEASE_MASK diff --git a/source/brailleDisplayDrivers/baum.py b/source/brailleDisplayDrivers/baum.py index 58237a07a6a..ac334693e4c 100644 --- a/source/brailleDisplayDrivers/baum.py +++ b/source/brailleDisplayDrivers/baum.py @@ -3,16 +3,16 @@ #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) 2010-2017 NV Access Limited +#Copyright (C) 2010-2017 NV Access Limited, Babbage B.V. import time from collections import OrderedDict from cStringIO import StringIO -import hwPortUtils import braille import inputCore from logHandler import log import brailleInput +import bdDetect import hwIo TIMEOUT = 0.2 @@ -55,65 +55,6 @@ BAUM_JOYSTICK_KEYS: ("up", "left", "down", "right", "select"), } -USB_IDS_SER = { - "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_0403&PID_FE76", # VarioPro 80 - "VID_0403&PID_FE77", # VarioPro 64 - "VID_0904&PID_2000", # VarioPro 40 - "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 -} - -USB_IDS_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 -} - -BLUETOOTH_NAMES = ( - "Baum SuperVario", - "Baum PocketVario", - "Baum SVario", - "HWG Brailliant", - "Refreshabraille", - "VarioConnect", - "BrailleConnect", - "Pronto!", - "VarioUltra", - "Orbit Reader 20", -) - class BrailleDisplayDriver(braille.BrailleDisplayDriver): name = "baum" # Translators: Names of braille displays. @@ -121,73 +62,25 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): isThreadSafe = True @classmethod - def check(cls): - return True + def getManualPorts(cls): + return braille.getSerialPorts() - @classmethod - def getPossiblePorts(cls): - ports = OrderedDict() - comPorts = list(hwPortUtils.listComPorts(onlyAvailable=True)) - try: - next(cls._getAutoPorts(comPorts)) - ports.update((cls.AUTOMATIC_PORT,)) - except StopIteration: - pass - for portInfo in comPorts: - # Translators: Name of a serial communications port. - ports[portInfo["port"]] = _("Serial: {portName}").format(portName=portInfo["friendlyName"]) - return ports - - @classmethod - def _getAutoPorts(cls, comPorts): - for portInfo in hwPortUtils.listHidDevices(): - if portInfo.get("usbID") in USB_IDS_HID: - yield portInfo["devicePath"], "USB HID" - # Try bluetooth ports last. - for portInfo in sorted(comPorts, key=lambda item: "bluetoothName" in item): - port = portInfo["port"] - hwID = portInfo["hardwareID"] - if hwID.startswith(r"FTDIBUS\COMPORT"): - # USB. - portType = "USB serial" - try: - usbID = hwID.split("&", 1)[1] - except IndexError: - continue - if usbID not in USB_IDS_SER: - continue - elif hwID == r"USB\VID_0483&PID_5740&REV_0200": - # Generic STMicroelectronics Virtual COM Port used by Orbit Reader 20. - portType = "USB serial" - elif "bluetoothName" in portInfo: - # Bluetooth. - portType = "bluetooth" - btName = portInfo["bluetoothName"] - if not any(btName.startswith(prefix) for prefix in BLUETOOTH_NAMES): - continue - else: - continue - yield port, portType - - def __init__(self, port="Auto"): + def __init__(self, port="auto"): super(BrailleDisplayDriver, self).__init__() self.numCells = 0 self._deviceID = None - if port == "auto": - tryPorts = self._getAutoPorts(hwPortUtils.listComPorts(onlyAvailable=True)) - else: - tryPorts = ((port, "serial"),) - for port, portType in tryPorts: + for portType, portId, port, portInfo in self._getTryPorts(port): # At this point, a port bound to this display has been found. # Try talking to the display. - self.isHid = portType == "USB HID" + self.isHid = portType == bdDetect.KEY_HID try: if self.isHid: self._dev = hwIo.Hid(port, onReceive=self._onReceive) else: self._dev = hwIo.Serial(port, baudrate=BAUD_RATE, timeout=TIMEOUT, writeTimeout=TIMEOUT, onReceive=self._onReceive) except EnvironmentError: + log.debugWarning("", exc_info=True) continue if self.isHid: try: diff --git a/source/brailleDisplayDrivers/brailleNote.py b/source/brailleDisplayDrivers/brailleNote.py index 9845f6d230b..1daa540ef77 100644 --- a/source/brailleDisplayDrivers/brailleNote.py +++ b/source/brailleDisplayDrivers/brailleNote.py @@ -14,19 +14,10 @@ import serial import braille import brailleInput -import hwPortUtils import inputCore from logHandler import log import hwIo - -BLUETOOTH_NAMES = ("Braillenote",) -BLUETOOTH_ADDRS = ( - # (first, last), - (0x0025EC000000, 0x0025EC01869F), # Apex -) -USB_IDS = frozenset(( - "VID_1C71&PID_C004", # Apex - )) +import bdDetect BAUD_RATE = 38400 TIMEOUT = 0.1 @@ -135,69 +126,18 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): isThreadSafe = True @classmethod - def check(cls): - return True - - @classmethod - def _getUSBPorts(cls): - return (p["port"] for p in hwPortUtils.listComPorts() - if p["hardwareID"].startswith("USB\\") and any(p["hardwareID"][4:].startswith(id) for id in USB_IDS)) - - @classmethod - def _getBluetoothPorts(cls): - for p in hwPortUtils.listComPorts(): - try: - addr = p["bluetoothAddress"] - name = p["bluetoothName"] - except KeyError: - continue - if (any(first <= addr <= last for first, last in BLUETOOTH_ADDRS) - or any(name.startswith(prefix) for prefix in BLUETOOTH_NAMES)): - yield p["port"] - - @classmethod - def getPossiblePorts(cls): - ports = OrderedDict() - usb = bluetooth = False - # See if we have any USB ports available: - try: - cls._getUSBPorts().next() - usb = True - except StopIteration: - pass - # See if we have any bluetooth ports available: - try: - cls._getBluetoothPorts().next() - bluetooth = True - except StopIteration: - pass - if usb or bluetooth: - ports.update([cls.AUTOMATIC_PORT]) - if usb: - ports["usb"] = "USB" - if bluetooth: - ports["bluetooth"] = "Bluetooth" - for p in hwPortUtils.listComPorts(): - # Translators: Name of a serial communications port - ports[p["port"]] = _("Serial: {portName}").format(portName=p["friendlyName"]) - return ports + def getManualPorts(cls): + return braille.getSerialPorts() def __init__(self, port="auto"): super(BrailleDisplayDriver, self).__init__() self._serial = None - if port == "auto": - portsToTry = itertools.chain(self._getUSBPorts(), self._getBluetoothPorts()) - elif port == "usb": - portsToTry = self._getUSBPorts() - elif port == "bluetooth": - portsToTry = self._getBluetoothPorts() - else: - portsToTry = (port,) - for port in portsToTry: + for portType, portId, port, portInfo in self._getTryPorts(port): log.debug("Checking port %s for a BrailleNote", port) try: self._serial = hwIo.Serial(port, baudrate=BAUD_RATE, timeout=TIMEOUT, writeTimeout=TIMEOUT, parity=serial.PARITY_NONE, onReceive=self._onReceive) except EnvironmentError: + log.debugWarning("", exc_info=True) continue # Check for cell information if self._describe(): diff --git a/source/brailleDisplayDrivers/brailliantB.py b/source/brailleDisplayDrivers/brailliantB.py index e1d59cae27b..92cf354864c 100644 --- a/source/brailleDisplayDrivers/brailliantB.py +++ b/source/brailleDisplayDrivers/brailliantB.py @@ -2,18 +2,15 @@ #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) 2012-2017 NV Access Limited +#Copyright (C) 2012-2017 NV Access Limited, Babbage B.V. -import os -import _winreg -import itertools import time import serial -import hwPortUtils import braille import inputCore from logHandler import log import brailleInput +import bdDetect import hwIo TIMEOUT = 0.2 @@ -78,55 +75,6 @@ DOT8_KEY = 9 SPACE_KEY = 10 -USB_IDS_HID = { - "VID_1C71&PID_C006", # Brailliant BI 32, 40 and 80 - "VID_1C71&PID_C022", # Brailliant BI 14 - "VID_1C71&PID_C00A", # BrailleNote Touch -} -USB_IDS_SER = ( - "Vid_1c71&Pid_c005", # Brailliant BI 32, 40 and 80 - "Vid_1c71&Pid_c021", # Brailliant BI 14 -) - -def _getPorts(): - # HID. - for portInfo in hwPortUtils.listHidDevices(): - if portInfo.get("usbID") in USB_IDS_HID: - yield "USB HID", portInfo["devicePath"] - # In Windows 10, the Bluetooth vendor and product ids don't get recognised. - # Use strings instead. - elif portInfo.get("manufacturer") == "Humanware" and portInfo.get("product") == "Brailliant HID": - yield "Bluetooth HID", portInfo["devicePath"] - - # USB serial. - for usbId in USB_IDS_SER: - try: - rootKey = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, - r"SYSTEM\CurrentControlSet\Enum\USB\%s" % usbId) - except WindowsError: - # A display with this id has never been connected via USB. - continue - with rootKey: - for index in itertools.count(): - try: - keyName = _winreg.EnumKey(rootKey, index) - except WindowsError: - break # No more sub-keys. - try: - with _winreg.OpenKey(rootKey, os.path.join(keyName, "Device Parameters")) as paramsKey: - yield "USB serial", _winreg.QueryValueEx(paramsKey, "PortName")[0] - except WindowsError: - continue - - # Bluetooth serial. - for portInfo in hwPortUtils.listComPorts(onlyAvailable=True): - try: - btName = portInfo["bluetoothName"] - except KeyError: - continue - if btName.startswith("Brailliant B") or btName == "Brailliant 80" or "BrailleNote Touch" in btName: - yield "Bluetooth serial", portInfo["port"] - class BrailleDisplayDriver(braille.BrailleDisplayDriver): name = "brailliantB" # Translators: The name of a series of braille displays. @@ -134,20 +82,15 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): isThreadSafe = True @classmethod - def check(cls): - try: - next(_getPorts()) - except StopIteration: - # No possible ports found. - return False - return True + def getManualPorts(cls): + return braille.getSerialPorts() - def __init__(self): + def __init__(self, port="auto"): super(BrailleDisplayDriver, self).__init__() self.numCells = 0 - for portType, port in _getPorts(): - self.isHid = portType.endswith(" HID") + for portType, portId, port, portInfo in self._getTryPorts(port): + self.isHid = portType == bdDetect.KEY_HID # Try talking to the display. try: if self.isHid: @@ -155,6 +98,7 @@ def __init__(self): else: self._dev = hwIo.Serial(port, baudrate=BAUD_RATE, parity=PARITY, timeout=TIMEOUT, writeTimeout=TIMEOUT, onReceive=self._serOnReceive) except EnvironmentError: + log.debugWarning("", exc_info=True) continue # Couldn't connect. # The Brailliant can fail to init if you try immediately after connecting. time.sleep(DELAY_AFTER_CONNECT) diff --git a/source/brailleDisplayDrivers/eurobraille.py b/source/brailleDisplayDrivers/eurobraille.py index ba944a6ef1e..bd33c0eea57 100644 --- a/source/brailleDisplayDrivers/eurobraille.py +++ b/source/brailleDisplayDrivers/eurobraille.py @@ -8,7 +8,7 @@ from collections import OrderedDict, defaultdict from cStringIO import StringIO import serial -import hwPortUtils +import bdDetect import braille import inputCore from logHandler import log @@ -127,30 +127,6 @@ 0x11:"Esytime evo 32 standard", } -USB_IDS_HID = { - "VID_C251&PID_1122", # Esys (version < 3.0, no SD card - "VID_C251&PID_1123", # Esys (version >= 3.0, with HID keyboard, no SD card - "VID_C251&PID_1124", # Esys (version < 3.0, with SD card - "VID_C251&PID_1125", # Esys (version >= 3.0, with HID keyboard, with SD card - "VID_C251&PID_1126", # Esys (version >= 3.0, no SD card - "VID_C251&PID_1127", # Reserved - "VID_C251&PID_1128", # Esys (version >= 3.0, with SD card - "VID_C251&PID_1129", # Reserved - "VID_C251&PID_112A", # Reserved - "VID_C251&PID_112B", # Reserved - "VID_C251&PID_112C", # Reserved - "VID_C251&PID_112D", # Reserved - "VID_C251&PID_112E", # Reserved - "VID_C251&PID_112F", # Reserved - "VID_C251&PID_1130", # Esytime - "VID_C251&PID_1131", # Reserved - "VID_C251&PID_1132", # Reserved -} - -BLUETOOTH_NAMES = { - "Esys", -} - def bytesToInt(bytes): """Converts a basestring to its integral equivalent.""" return int(bytes.encode('hex'), 16) @@ -163,40 +139,8 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver, ScriptableObject): timeout = 0.2 @classmethod - def check(cls): - return True - - @classmethod - def getPossiblePorts(cls): - ports = OrderedDict() - comPorts = list(hwPortUtils.listComPorts(onlyAvailable = True)) - try: - next(cls._getAutoPorts(comPorts)) - ports.update((cls.AUTOMATIC_PORT,)) - except StopIteration: - pass - for portInfo in comPorts: - # Translators: Name of a serial communications port. - ports[portInfo["port"]] = _("Serial: {portName}").format(portName = portInfo["friendlyName"]) - return ports - - @classmethod - def _getAutoPorts(cls, comPorts): - for portInfo in hwPortUtils.listHidDevices(): - if portInfo.get("usbID") in USB_IDS_HID: - yield portInfo["devicePath"], "USB HID" - # Try bluetooth ports last. - for portInfo in sorted(comPorts, key=lambda item: "bluetoothName" in item): - port = portInfo["port"] - if "bluetoothName" in portInfo: - # Bluetooth. - portType = "bluetooth" - btName = portInfo["bluetoothName"] - if not any(btName.startswith(prefix) for prefix in BLUETOOTH_NAMES): - continue - else: - continue - yield port, portType + def getManualPorts(cls): + return braille.getSerialPorts() def __init__(self, port="Auto"): super(BrailleDisplayDriver, self).__init__() @@ -210,14 +154,10 @@ def __init__(self, port="Auto"): self._hidKeyboardInput = False self._hidInputBuffer = "" - if port == "auto": - tryPorts = self._getAutoPorts(hwPortUtils.listComPorts(onlyAvailable=True)) - else: - tryPorts = ((port, "serial"),) - for port, portType in tryPorts: + for portType, portId, port, portInfo in self._getTryPorts(port): # At this point, a port bound to this display has been found. # Try talking to the display. - self.isHid = portType == "USB HID" + self.isHid = portType == bdDetect.KEY_HID try: if self.isHid: self._dev = hwIo.Hid( diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index a2aa95b0e71..dd9047d177d 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -13,7 +13,6 @@ from cStringIO import StringIO import serial # pylint: disable=E0401 import weakref -import hwPortUtils import hwIo import braille import brailleInput @@ -22,6 +21,7 @@ from baseObject import ScriptableObject, AutoPropertyObject from globalCommands import SCRCAT_BRAILLE from logHandler import log +import bdDetect import time import datetime @@ -29,48 +29,14 @@ PARITY = serial.PARITY_ODD # pylint: disable=C0330 -USB_IDS_SER = { - "VID_0403&PID_6001", # FTDI chip - "VID_0921&PID_1200", # GoHubs chip -} - -# Newer displays have a native HID processor -# pylint: disable=C0330 -USB_IDS_HID_NATIVE = { - "VID_1FE4&PID_0054", # Active Braille - "VID_1FE4&PID_0081", # Basic Braille 16 - "VID_1FE4&PID_0082", # Basic Braille 20 - "VID_1FE4&PID_0083", # Basic Braille 32 - "VID_1FE4&PID_0084", # Basic Braille 40 - "VID_1FE4&PID_008A", # Basic Braille 48 - "VID_1FE4&PID_0086", # Basic Braille 64 - "VID_1FE4&PID_0087", # Basic Braille 80 - "VID_1FE4&PID_008B", # Basic Braille 160 - "VID_1FE4&PID_0061", # Actilino - "VID_1FE4&PID_0064", # Active Star 40 -} - -# Some older displays use a HID converter and an internal serial interface +# Some older Handy Tech displays use a HID converter and an internal serial interface. +# We need to keep these IDS around here to send additional data upon connection. USB_IDS_HID_CONVERTER = { "VID_1FE4&PID_0003", # USB-HID adapter "VID_1FE4&PID_0074", # Braille Star 40 "VID_1FE4&PID_0044", # Easy Braille } -USB_IDS_HID = USB_IDS_HID_NATIVE | USB_IDS_HID_CONVERTER - -# pylint: disable=C0330 -BLUETOOTH_NAMES = { - "Actilino AL", - "Active Braille AB", - "Active Star AS", - "Basic Braille BB", - "Braille Star 40 BS", - "Braillino BL", - "Braille Wave BW", - "Easy Braille EBR", -} - # Model identifiers # pylint: disable=C0103 MODEL_BRAILLE_WAVE = b"\x05" @@ -555,54 +521,8 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver, ScriptableObject): timeout = 0.2 @classmethod - def check(cls): - return True - - @classmethod - def getPossiblePorts(cls): - ports = OrderedDict() - comPorts = list(hwPortUtils.listComPorts(onlyAvailable=True)) - try: - next(cls._getAutoPorts(comPorts)) - ports.update((cls.AUTOMATIC_PORT,)) - except StopIteration: - pass - for portInfo in comPorts: - # Translators: Name of a serial communications port. - ports[portInfo["port"]] = _("Serial: {portName}").format( - portName=portInfo["friendlyName"]) - return ports - - @classmethod - def _getAutoPorts(cls, comPorts): - for portInfo in hwPortUtils.listHidDevices(): - if portInfo.get("usbID") in USB_IDS_HID_CONVERTER: - yield portInfo["devicePath"], "USB HID serial converter" - if portInfo.get("usbID") in USB_IDS_HID_NATIVE: - yield portInfo["devicePath"], "USB HID" - # Try bluetooth ports last. - for portInfo in sorted(comPorts, key=lambda item: "bluetoothName" in item): - port = portInfo["port"] - hwId = portInfo["hardwareID"] - if hwId.startswith(r"FTDIBUS\COMPORT"): - # USB. - # TODO: It seems there is also another chip (Gohubs) used in some models. See if we can autodetect that as well. - portType = "USB serial" - try: - usbId = hwId.split("&", 1)[1] - except IndexError: - continue - if usbId not in USB_IDS_SER: - continue - elif "bluetoothName" in portInfo: - # Bluetooth. - portType = "bluetooth" - btName = portInfo["bluetoothName"] - if not any(btName.startswith(prefix) for prefix in BLUETOOTH_NAMES): - continue - else: - continue - yield port, portType + def getManualPorts(cls): + return braille.getSerialPorts() def __init__(self, port="auto"): super(BrailleDisplayDriver, self).__init__() @@ -614,15 +534,11 @@ def __init__(self, port="auto"): self._hidSerialBuffer = b"" self._atc = False - if port == "auto": - tryPorts = self._getAutoPorts(hwPortUtils.listComPorts(onlyAvailable=True)) - else: - tryPorts = ((port, "serial"),) - for port, portType in tryPorts: + for portType, portId, port, portInfo in self._getTryPorts(port): # At this point, a port bound to this display has been found. # Try talking to the display. - self.isHid = portType.startswith("USB HID") - self.isHidSerial = portType == "USB HID serial converter" + self.isHid = portType == bdDetect.KEY_HID + self.isHidSerial = portId in USB_IDS_HID_CONVERTER try: if self.isHidSerial: # This is either the standalone HID adapter cable for older displays, diff --git a/source/brailleDisplayDrivers/hims.py b/source/brailleDisplayDrivers/hims.py index a3b1cd20d6c..2683f67a33b 100644 --- a/source/brailleDisplayDrivers/hims.py +++ b/source/brailleDisplayDrivers/hims.py @@ -5,12 +5,9 @@ #See the file COPYING for more details. #Copyright (C) 2010-2018 Gianluca Casalino, NV Access Limited, Babbage B.V., Leonard de Ruijter, Bram Duvigneau -import _winreg import serial from cStringIO import StringIO -import itertools import os -import hwPortUtils import hwIo import braille from logHandler import log @@ -20,6 +17,7 @@ from baseObject import AutoPropertyObject import weakref import time +import bdDetect BAUD_RATE = 115200 PARITY = serial.PARITY_NONE @@ -134,7 +132,7 @@ class SmartBeetle(BrailleSense4S): Furthermore, the key codes for f2 and f4 are swapped, and it has only two scroll keys. """ numCells=14 - bluetoothPrefix = "SmartBeetle" + bluetoothPrefix = "SmartBeetle(b)" name = "Smart Beetle" def _get_keys(self): @@ -180,10 +178,6 @@ def _get_keys(self): SyncBraille, )] -USB_IDS_BULK={BrailleEdge.usbId,BrailleSense.usbId} - -bluetoothPrefixes={modelCls.bluetoothPrefix for id, modelCls in modelMap if modelCls.bluetoothPrefix} - class BrailleDisplayDriver(braille.BrailleDisplayDriver): name = "hims" # Translators: The name of a series of braille displays. @@ -192,88 +186,17 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): timeout = 0.2 @classmethod - def check(cls): - return True - - @classmethod - def getPossiblePorts(cls): - ports = OrderedDict() - comPorts = list(hwPortUtils.listComPorts(onlyAvailable=True)) - try: - next(cls._getAutoPorts(comPorts)) - ports.update((cls.AUTOMATIC_PORT,)) - except StopIteration: - pass - for portInfo in comPorts: - if not "bluetoothName" in portInfo: - continue - # Translators: Name of a serial communications port. - ports[portInfo["port"]] = _("Serial: {portName}").format(portName=portInfo["friendlyName"]) - return ports - - @classmethod - def _getAutoPorts(cls, comPorts): - # USB bulk - for bulkId in USB_IDS_BULK: - portType = "USB bulk" - try: - rootKey = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Enum\USB\%s"%bulkId) - except WindowsError: - continue - else: - with rootKey: - for index in itertools.count(): - try: - keyName = _winreg.EnumKey(rootKey, index) - except WindowsError: - break - try: - with _winreg.OpenKey(rootKey, os.path.join(keyName, "Device Parameters")) as paramsKey: - yield _winreg.QueryValueEx(paramsKey, "SymbolicName")[0], portType, bulkId - except WindowsError: - continue - # Try bluetooth ports last. - for portInfo in sorted(comPorts, key=lambda item: "bluetoothName" in item): - port = portInfo["port"] - hwID = portInfo["hardwareID"] - if hwID.startswith(r"FTDIBUS\COMPORT"): - # USB. - portType = "USB serial" - try: - usbID = hwID.split("&", 1)[1] - except IndexError: - continue - if usbID!=SyncBraille.usbId: - continue - yield portInfo['port'], portType, usbID - elif "bluetoothName" in portInfo: - # Bluetooth. - portType = "bluetooth" - btName = portInfo["bluetoothName"] - for prefix in bluetoothPrefixes: - if btName.startswith(prefix): - btPrefix=prefix - break - else: - btPrefix = None - yield portInfo['port'], portType, btPrefix + def getManualPorts(cls): + return braille.getSerialPorts(filterFunc=lambda info: "bluetoothName" in info) def __init__(self, port="auto"): super(BrailleDisplayDriver, self).__init__() self.numCells = 0 self._model = None - if port == "auto": - tryPorts = self._getAutoPorts(hwPortUtils.listComPorts(onlyAvailable=True)) - else: - try: - btName = next(portInfo.get("bluetoothName","") for portInfo in hwPortUtils.listComPorts() if portInfo.get("port")==port) - btPrefix = next(prefix for prefix in bluetoothPrefixes if btName.startswith(prefix)) - tryPorts = ((port, "bluetooth", btPrefix),) - except StopIteration: - tryPorts = () - for port, portType, identifier in tryPorts: - self.isBulk = portType=="USB bulk" + for match in self._getTryPorts(port): + portType, portId, port, portInfo = match + self.isBulk = portType==bdDetect.KEY_CUSTOM # Try talking to the display. try: if self.isBulk: @@ -301,14 +224,7 @@ def __init__(self, port="auto"): log.debugWarning("No response from potential Hims display") self._dev.close() continue - if portType=="USB serial": - self._model = SyncBraille() - elif self.isBulk: - self._sendIdentificationRequests(usbId=identifier) - elif portType=="bluetooth" and identifier: - self._sendIdentificationRequests(bluetoothPrefix=identifier) - else: - self._sendIdentificationRequests() + self._sendIdentificationRequests(match) if self._model: # A display responded. log.info("Found {device} connected via {type} ({port})".format( @@ -327,25 +243,26 @@ def _sendCellCountRequest(self): log.debug("Sending cell count request...") self._sendPacket("\xfb","\x01","\x00"*32) - def _sendIdentificationRequests(self, usbId=None, bluetoothPrefix=None): - log.debug("Considering sending identification requests: usbId=%s, bluetoothPrefix=%s"%(usbId,bluetoothPrefix)) - if usbId and not bluetoothPrefix: - map=[modelTuple for modelTuple in modelMap if modelTuple[1].usbId==usbId] - elif not usbId and bluetoothPrefix: - map=[modelTuple for modelTuple in modelMap if modelTuple[1].bluetoothPrefix==bluetoothPrefix] - elif usbId and bluetoothPrefix: - map=[modelTuple for modelTuple in modelMap if modelTuple[1].usbId==usbId and modelCls.bluetoothPrefix==bluetoothPrefix] - else: # not usbId and not bluetoothPrefix - map=modelMap + def _sendIdentificationRequests(self, match): + log.debug("Considering sending identification requests for device %s"%str(match)) + if match.type==bdDetect.KEY_CUSTOM: # USB Bulk + map=[modelTuple for modelTuple in modelMap if modelTuple[1].usbId==match.id] + elif "bluetoothName" in match.deviceInfo: # Bluetooth + map=[modelTuple for modelTuple in modelMap if modelTuple[1].bluetoothPrefix and match.id.startswith(modelTuple[1].bluetoothPrefix)] + else: # The only serial device we support which is not bluetooth, is a Sync Braille + self._model = SyncBraille() + log.debug("Use %s as model without sending an additional identification request"%self._model.name) + return if not map: - raise ValueError("The specified criteria to send identification requests didn't yield any results") + log.debugWarning("The provided device match to send identification requests didn't yield any results") + map = modelMap if len(map)==1: modelCls = map[0][1] numCells = self.numCells or modelCls.numCells if numCells: # There is only one model matching the criteria, and we have the proper number of cells. # There's no point in sending an identification request at all, just use this model - log.debug("Chose %s as model without sending an additional identification request"%modelCls.name) + log.debug("Use %s as model without sending an additional identification request"%modelCls.name) self._model = modelCls() self.numCells = numCells return diff --git a/source/brailleDisplayDrivers/superBrl.py b/source/brailleDisplayDrivers/superBrl.py index d8fe5379836..1200d298b07 100644 --- a/source/brailleDisplayDrivers/superBrl.py +++ b/source/brailleDisplayDrivers/superBrl.py @@ -2,16 +2,16 @@ #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) 2017 NV Access Limited, Coscell Kao +#Copyright (C) 2017 NV Access Limited, Coscell Kao, Babbage B.V. import serial from collections import OrderedDict import braille -import hwPortUtils import hwIo import time import inputCore from logHandler import log +import bdDetect BAUD_RATE = 9600 TIMEOUT = 0.5 @@ -28,49 +28,17 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): description = _("SuperBraille") isThreadSafe=True - USB_IDs = { - "USB\\VID_10C4&PID_EA60", # SuperBraille 3.2 - } - @classmethod - def getPossiblePorts(cls): - ports = OrderedDict() - comPorts = list(hwPortUtils.listComPorts(onlyAvailable=True)) - try: - next(cls._getAutoPorts(comPorts)) - ports.update((cls.AUTOMATIC_PORT,)) - except StopIteration: - pass - for portInfo in comPorts: - # Translators: Name of a serial communications port. - ports[portInfo["port"]] = _("Serial: {portName}").format(portName=portInfo["friendlyName"]) - return ports - - @classmethod - def _getAutoPorts(cls, comPorts): - for portInfo in comPorts: - port = portInfo["port"] - hwID = portInfo["hardwareID"] - if any(hwID.startswith(x) for x in cls.USB_IDs): - portType = "USB serial" - else: - continue - yield port, portType - - @classmethod - def check(cls): - return True + def getManualPorts(cls): + return braille.getSerialPorts() def __init__(self,port="Auto"): super(BrailleDisplayDriver, self).__init__() - if port == "auto": - tryPorts = self._getAutoPorts(hwPortUtils.listComPorts(onlyAvailable=True)) - else: - tryPorts = ((port, "serial"),) - for port, portType in tryPorts: + for portType, portId, port, portInfo in self._getTryPorts(port): try: self._dev = hwIo.Serial(port, baudrate=BAUD_RATE, stopbits=serial.STOPBITS_ONE, parity=serial.PARITY_NONE, timeout=TIMEOUT, writeTimeout=TIMEOUT, onReceive=self._onReceive) except EnvironmentError: + log.debugWarning("", exc_info=True) continue # try to initialize the device and request number of cells diff --git a/source/config/configSpec.py b/source/config/configSpec.py index ba3459267c7..ba0212fc40b 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -49,7 +49,7 @@ # Braille settings [braille] - display = string(default=noBraille) + display = string(default=auto) translationTable = string(default=en-ueb-g1.ctb) inputTable = string(default=en-ueb-g1.ctb) expandAtCursor = boolean(default=true) diff --git a/source/core.py b/source/core.py index b5f5b9a951b..82050b3687d 100644 --- a/source/core.py +++ b/source/core.py @@ -28,6 +28,7 @@ import globalVars from logHandler import log import addonHandler +import extensionPoints import extensionPoints @@ -39,6 +40,18 @@ #: The thread identifier of the main thread. mainThreadId = thread.get_ident() +#: Notifies when a window message has been received by NVDA. +#: This allows components to perform an action when several system events occur, +#: such as power, screen orientation and hardware changes. +#: Handlers are called with three arguments. +#: @param msg: The window message. +#: @type msg: int +#: @param wParam: Additional message information. +#: @type wParam: int +#: @param lParam: Additional message information. +#: @type lParam: int +post_windowMessageReceipt = extensionPoints.Action() + _pump = None _isPumpPending = False @@ -290,6 +303,7 @@ def __init__(self, windowName=None): self.handlePowerStatusChange() def windowProc(self, hwnd, msg, wParam, lParam): + post_windowMessageReceipt.notify(msg=msg, wParam=wParam, lParam=lParam) if msg == self.WM_POWERBROADCAST and wParam == self.PBT_APMPOWERSTATUSCHANGE: self.handlePowerStatusChange() elif msg == self.WM_DISPLAYCHANGE: diff --git a/source/globalCommands.py b/source/globalCommands.py index 9c673b78911..a0098042339 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -1728,11 +1728,7 @@ def script_braille_toggleTether(self, gesture): if newTetherChoice==braille.handler.TETHER_REVIEW: braille.handler.handleReviewMove(shouldAutoTether=False) else: - focus = api.getFocusObject() - if focus.treeInterceptor and not focus.treeInterceptor.passThrough: - braille.handler.handleGainFocus(focus.treeInterceptor,shouldAutoTether=False) - else: - braille.handler.handleGainFocus(focus,shouldAutoTether=False) + braille.handler.handleGainFocus(api.getFocusObject(),shouldAutoTether=False) # Translators: Reports which position braille is tethered to # (braille can be tethered automatically or to either focus or review position). ui.message(_("Braille tethered %s") % labels[newIndex]) @@ -1965,12 +1961,7 @@ def script_braille_toFocus(self, gesture): braille.handler.mainBuffer.scrollTo(region, region.brailleSelectionStart) braille.handler.mainBuffer.updateDisplay() else: - # We just tethered to focus from review, - # Handle this case as we just focused the object - if obj.treeInterceptor and not obj.treeInterceptor.passThrough: - braille.handler.handleGainFocus(obj.treeInterceptor,shouldAutoTether=False) - else: - braille.handler.handleGainFocus(obj,shouldAutoTether=False) + braille.handler.handleGainFocus(obj,shouldAutoTether=False) # Translators: Input help mode message for a braille command. script_braille_toFocus.__doc__= _("Moves the braille display to the current focus") script_braille_toFocus.category=SCRCAT_BRAILLE diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 7e671de12ef..46df2c54c03 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2087,9 +2087,8 @@ def makeSettings(self, settingsSizer): displayBox = wx.StaticBox(self, label=displayLabel) displayGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(displayBox, wx.HORIZONTAL)) settingsSizerHelper.addItem(displayGroup) - - displayDesc = braille.handler.display.description - self.displayNameCtrl = ExpandoTextCtrl(self, size=(self.scaleSize(250), -1), value=displayDesc, style=wx.TE_READONLY) + self.displayNameCtrl = ExpandoTextCtrl(self, size=(self.scaleSize(250), -1), style=wx.TE_READONLY) + self.updateCurrentDisplay() # Translators: This is the label for the button used to change braille display, # it appears in the context of a braille display group on the braille settings panel. changeDisplayBtn = wx.Button(self, label=_("C&hange...")) @@ -2122,7 +2121,10 @@ def onChangeDisplay(self, evt): self.Thaw() def updateCurrentDisplay(self): - displayDesc = braille.handler.display.description + if config.conf["braille"]["display"] == braille.AUTO_DISPLAY_NAME: + displayDesc = BrailleDisplaySelectionDialog.getCurrentAutoDisplayDescription() + else: + displayDesc = braille.handler.display.description self.displayNameCtrl.SetValue(displayDesc) def onPanelActivated(self): @@ -2163,14 +2165,28 @@ def postInit(self): # Finally, ensure that focus is on the list of displays. self.displayList.SetFocus() + @staticmethod + def getCurrentAutoDisplayDescription(): + description = braille.AUTOMATIC_PORT[1] + if ( + config.conf["braille"]["display"] == braille.AUTO_DISPLAY_NAME + and braille.handler.display.name != "noBraille" + ): + description = "%s (%s)" % (description, braille.handler.display.description) + return description + def updateBrailleDisplayLists(self): - driverList = braille.getDisplayList() + driverList = [(braille.AUTO_DISPLAY_NAME, self.getCurrentAutoDisplayDescription())] + driverList.extend(braille.getDisplayList()) self.displayNames = [driver[0] for driver in driverList] displayChoices = [driver[1] for driver in driverList] self.displayList.Clear() self.displayList.AppendItems(displayChoices) try: - selection = self.displayNames.index(braille.handler.display.name) + if config.conf["braille"]["display"] == braille.AUTO_DISPLAY_NAME: + selection = 0 + else: + selection = self.displayNames.index(braille.handler.display.name) self.displayList.SetSelection(selection) except: pass @@ -2178,12 +2194,13 @@ def updateBrailleDisplayLists(self): def updatePossiblePorts(self): displayName = self.displayNames[self.displayList.GetSelection()] - displayCls = braille._getDisplayDriver(displayName) self.possiblePorts = [] - try: - self.possiblePorts.extend(displayCls.getPossiblePorts().iteritems()) - except NotImplementedError: - pass + if displayName != "auto": + displayCls = braille._getDisplayDriver(displayName) + try: + self.possiblePorts.extend(displayCls.getPossiblePorts().iteritems()) + except NotImplementedError: + pass if self.possiblePorts: self.portsList.SetItems([p[1] for p in self.possiblePorts]) try: diff --git a/source/hwIo.py b/source/hwIo.py index a86c0bd366d..db267b2e244 100644 --- a/source/hwIo.py +++ b/source/hwIo.py @@ -2,7 +2,7 @@ #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) 2015-2016 NV Access Limited +#Copyright (C) 2015-2018 NV Access Limited, Babbage B.V. """Raw input/output for braille displays via serial and HID. See the L{Serial} and L{Hid} classes. @@ -20,6 +20,7 @@ import braille from logHandler import log import config +import time LPOVERLAPPED_COMPLETION_ROUTINE = ctypes.WINFUNCTYPE(None, DWORD, DWORD, serial.win32.LPOVERLAPPED) @@ -51,7 +52,7 @@ def __init__(self, fileHandle, onReceive, writeFileHandle=None, onReceiveSize=1, self._writeSize = writeSize self._readBuf = ctypes.create_string_buffer(onReceiveSize) self._readOl = OVERLAPPED() - self._recvEvt = threading.Event() + self._recvEvt = winKernel.createEvent() self._ioDoneInst = LPOVERLAPPED_COMPLETION_ROUTINE(self._ioDone) self._writeOl = OVERLAPPED() # Do the initial read. @@ -72,12 +73,20 @@ def waitForRead(self, timeout): C{False} if not. @rtype: bool """ - if not self._recvEvt.wait(timeout): - if _isDebug(): - log.debug("Wait timed out") - return False - self._recvEvt.clear() - return True + timeout= int(timeout*1000) + while True: + curTime = time.time() + res = winKernel.waitForSingleObjectEx(self._recvEvt, timeout, True) + if res==winKernel.WAIT_OBJECT_0: + return True + elif res==winKernel.WAIT_TIMEOUT: + if _isDebug(): + log.debug("Wait timed out") + return False + elif res==winKernel.WAIT_IO_COMPLETION: + if _isDebug(): + log.debug("Waiting interrupted by completed i/o") + timeout -= int((time.time()-curTime)*1000) def write(self, data): if _isDebug(): @@ -101,6 +110,7 @@ def close(self): ctypes.windll.kernel32.CancelIoEx(self._file, byref(self._readOl)) if hasattr(self, "_writeFile") and self._writeFile not in (self._file, INVALID_HANDLE_VALUE): ctypes.windll.kernel32.CancelIoEx(self._writeFile, byref(self._readOl)) + winKernel.closeHandle(self._recvEvt) def __del__(self): try: @@ -123,7 +133,7 @@ def _ioDone(self, error, bytes, overlapped): elif error != 0: raise ctypes.WinError(error) self._notifyReceive(self._readBuf[:bytes]) - self._recvEvt.set() + winKernel.kernel32.SetEvent(self._recvEvt) self._asyncRead() def _notifyReceive(self, data): diff --git a/source/hwPortUtils.py b/source/hwPortUtils.py index 51a1b882152..2c75ecaa0e9 100644 --- a/source/hwPortUtils.py +++ b/source/hwPortUtils.py @@ -1,6 +1,6 @@ #hwPortUtils.py #A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2001-2016 Chris Liechti, NV Access Limited +#Copyright (C) 2001-2018 Chris Liechti, NV Access Limited, Babbage B.V. # Based on serial scanner code by Chris Liechti from https://raw.githubusercontent.com/pyserial/pyserial/81167536e796cc2e13aa16abd17a14634dc3aed1/pyserial/examples/scanwin32.py """Utilities for working with hardware connection ports. @@ -94,6 +94,16 @@ class dummy(ctypes.Structure): SetupDiGetDeviceRegistryProperty.argtypes = (HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, ctypes.c_void_p, DWORD, PDWORD) SetupDiGetDeviceRegistryProperty.restype = BOOL +SetupDiEnumDeviceInfo = ctypes.windll.setupapi.SetupDiEnumDeviceInfo +SetupDiEnumDeviceInfo.argtypes = (HDEVINFO, DWORD, PSP_DEVINFO_DATA) +SetupDiEnumDeviceInfo.restype = BOOL + +CM_Get_Device_ID = ctypes.windll.cfgmgr32.CM_Get_Device_IDW +CM_Get_Device_ID.argtypes = (DWORD, ctypes.c_wchar_p, ULONG, ULONG) +CM_Get_Device_ID.restype = DWORD +CR_SUCCESS = 0 +MAX_DEVICE_ID_LEN = 200 + GUID_CLASS_COMPORT = GUID(0x86e0d1e0L, 0x8089, 0x11d0, (ctypes.c_ubyte*8)(0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73)) GUID_DEVINTERFACE_USB_DEVICE = GUID(0xA5DCBF10, 0x6530, 0x11D2, @@ -117,7 +127,7 @@ def listComPorts(onlyAvailable=True): """List com ports on the system. @param onlyAvailable: Only return ports that are currently available. @type onlyAvailable: bool - @return: Generates dicts including keys of port, friendlyName and hardwareID. + @return: Dicts including keys of port, friendlyName and hardwareID. @rtype: generator of dict """ flags = DIGCF_DEVICEINTERFACE @@ -221,6 +231,11 @@ def __str__(self): entry["bluetoothAddress"], entry["bluetoothName"] = getWidcommBluetoothPortInfo(port) except: pass + elif "USB" in hwID or "FTDIBUS" in hwID: + usbIDStart = hwID.find("VID_") + if usbIDStart==-1: + continue + usbID = entry['usbID'] = hwID[usbIDStart:usbIDStart+17] # VID_xxxx&PID_xxxx finally: ctypes.windll.advapi32.RegCloseKey(regKey) @@ -322,8 +337,8 @@ def listUsbDevices(onlyAvailable=True): """List USB devices on the system. @param onlyAvailable: Only return devices that are currently available. @type onlyAvailable: bool - @return: The USB vendor and product IDs in the form "VID_xxxx&PID_xxxx" - @rtype: generator of unicode + @return: Generates dicts including keys of usbID (VID and PID), devicePath and hardwareID. + @rtype: generator of dict """ flags = DIGCF_DEVICEINTERFACE if onlyAvailable: @@ -393,9 +408,13 @@ def __str__(self): else: # The string is of the form "usb\VID_xxxx&PID_xxxx&..." usbId = buf.value[4:21] # VID_xxxx&PID_xxxx + info = { + "hardwareID": buf.value, + "usbID": usbId, + "devicePath": idd.DevicePath} if _isDebug(): log.debug("%r" % usbId) - yield usbId + yield info finally: SetupDiDestroyDeviceInfoList(g_hdi) if _isDebug(): diff --git a/source/winKernel.py b/source/winKernel.py index 4e928152fb7..e3c77e3483a 100644 --- a/source/winKernel.py +++ b/source/winKernel.py @@ -36,6 +36,12 @@ LOCALE_NAME_USER_DEFAULT=None DATE_LONGDATE=0x00000002 TIME_NOSECONDS=0x00000002 +# Wait return types +WAIT_ABANDONED = 0x00000080L +WAIT_IO_COMPLETION = 0x000000c0L +WAIT_OBJECT_0 = 0x00000000L +WAIT_TIMEOUT = 0x00000102L +WAIT_FAILED = 0xffffffff def GetStdHandle(handleID): h=kernel32.GetStdHandle(handleID) @@ -56,6 +62,63 @@ def CreateFile(fileName,desiredAccess,shareMode,securityAttributes,creationDispo raise ctypes.WinError() return res +def createEvent(eventAttributes=None, manualReset=False, initialState=False, name=None): + res = kernel32.CreateEventW(eventAttributes, manualReset, initialState, name) + if res==0: + raise ctypes.WinError() + return res + +def createWaitableTimer(securityAttributes=None, manualReset=False, name=None): + """Wrapper to the kernel32 CreateWaitableTimer function. + Consult https://msdn.microsoft.com/en-us/library/windows/desktop/ms682492.aspx for Microsoft's documentation. + In contrast with the original function, this wrapper assumes the following defaults. + @param securityAttributes: Defaults to C{None}; + The timer object gets a default security descriptor and the handle cannot be inherited. + The ACLs in the default security descriptor for a timer come from the primary or impersonation token of the creator. + @type securityAttributes: pointer to L{SECURITY_ATTRIBUTES} + @param manualReset: Defaults to C{False} which means the timer is a synchronization timer. + If C{True}, the timer is a manual-reset notification timer. + @type manualReset: bool + @param name: Defaults to C{None}, the timer object is created without a name. + @type name: unicode + """ + res = kernel32.CreateWaitableTimerW(securityAttributes, manualReset, name) + if res==0: + raise ctypes.WinError() + return res + +def setWaitableTimer(handle, dueTime, period=0, completionRoutine=None, arg=None, resume=False): + """Wrapper to the kernel32 SETWaitableTimer function. + Consult https://msdn.microsoft.com/en-us/library/windows/desktop/ms686289.aspx for Microsoft's documentation. + @param handle: A handle to the timer object. + @type handle: int + @param dueTime: Relative time (in miliseconds). + Note that the original function requires relative time to be supplied as a negative nanoseconds value. + @type dueTime: int + @param period: Defaults to 0, timer is only executed once. + Value should be supplied in miliseconds. + @type period: int + @param completionRoutine: The function to be executed when the timer elapses. + @type completionRoutine: L{PAPCFUNC} + @param arg: Defaults to C{None}; a pointer to a structure that is passed to the completion routine. + @type arg: L{ctypes.c_void_p} + @param resume: Defaults to C{False}; the system is not restored. + If this parameter is TRUE, restores a system in suspended power conservation mode + when the timer state is set to signaled. + @type resume: bool + """ + res = kernel32.SetWaitableTimer( + handle, + # due time is in 100 nanosecond intervals, relative time should be negated. + byref(LARGE_INTEGER(dueTime*-10000)), + period, + completionRoutine, + arg, + resume + ) + if res==0: + raise ctypes.WinError() + return True def createWaitableTimer(securityAttributes=None, manualReset=False, name=None): """Wrapper to the kernel32 CreateWaitableTimer function. @@ -205,7 +268,16 @@ def writeProcessMemory(*args): return kernel32.WriteProcessMemory(*args) def waitForSingleObject(handle,timeout): - return kernel32.WaitForSingleObject(handle,timeout) + res = kernel32.WaitForSingleObject(handle,timeout) + if res==WAIT_FAILED: + raise ctypes.WinError() + return res + +def waitForSingleObjectEx(handle,timeout, alertable): + res = kernel32.WaitForSingleObjectEx(handle,timeout, alertable) + if res==WAIT_FAILED: + raise ctypes.WinError() + return res SHUTDOWN_NORETRY = 0x00000001 diff --git a/source/winUser.py b/source/winUser.py index 5893dd59a28..1686da53b58 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -314,6 +314,9 @@ class GUITHREADINFO(Structure): # RedrawWindow() flags RDW_INVALIDATE = 0x0001 RDW_UPDATENOW = 0x0100 +# MsgWaitForMultipleObjectsEx +QS_ALLINPUT = 0x04ff +MWMO_ALERTABLE = 0x0002 def setSystemScreenReaderFlag(val): user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 33a3879568d..2598a05983b 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -72,6 +72,8 @@ class AppArgs: appModuleHandler.initialize() # Anything which notifies of cursor updates requires braille to be initialized. import braille +# Disable auto detection of braille displays when unit testing. +config.conf['braille']['display'] = "noBraille" braille.initialize() # For braille unit tests, we need to construct a fake braille display as well as enable the braille handler # Give the display 40 cells diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 00b1eb5bedf..6b900dae92c 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -13,6 +13,10 @@ What's New in NVDA - custom roles via the aria-roledescription attribute are now supported in Firefox, Chrome and Internet Explorer. - New braille table: Swedish 8 dot computer braille. (#8227) - added czech eight dots, central kurdish, esperanto and hungarian braille tables. (#8446) +- Support has been added to automatically detect braille displays in the background. (#1271) + - ALVA, Baum/HumanWare/APH/Orbit, Eurobraille, Handy Tech, Hims, SuperBraille and HumanWare BrailleNote and Brailliant BI/B displays are currently supported. + - You can enable this feature by selecting the automatic option from the list of braille displays in NVDA's braille display selection dialog. + - Please consult the documentation for additional details. == Changes == @@ -26,11 +30,15 @@ What's New in NVDA == Bug Fixes == - Accessible labels for controls in Google Chrome are now more readily reported in browse mode when the label does not appear as content itself. (#4773) +- Switching braille context presentation when in browse mode no longer causes braille output to stop following. (#7741) == Changes for Developers == - Added scriptHandler.script, which can function as a decorator for scripts on scriptable objects. (#6266) - A system test framework has been introduced for NVDA. (#708) +- Some changes have been made to the hwPortUtils module: (#1271) + - listUsbDevices now yields dictionaries with device information including hardwareID and devicePath. + - Dictionaries yielded by listComPorts now also contain a usbID entry for COM ports with USB VID/PID information in their hardware ID. = 2018.2 = diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index b32814266d4..1e9b6209b49 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -22,7 +22,7 @@ Major highlights include: - Built-in speech synthesizer supporting over 80 languages - Reporting of textual formatting where available such as font name and size, style and spelling errors - Automatic announcement of text under the mouse and optional audible indication of the mouse position -- Support for many refreshable braille displays, including braille input on braille displays with a braille keyboard +- Support for many refreshable braille displays, including the ability to detect many of them automatically as well as braille input on braille displays with a braille keyboard - Ability to run entirely from a USB flash drive or other portable media without the need for installation - Easy to use talking installer - Translated into 54 languages @@ -47,6 +47,7 @@ Information about other speech synthesizers that NVDA supports can be found in t ++ Braille support ++[BrailleSupport] For users that own a refreshable braille display, NVDA can output its information in braille. Both uncontracted and contracted braille input via a braille keyboard is also supported. +Furthermore, NVDA will detect many braille displays automatically by default. Please see the [Supported Braille Displays #SupportedBrailleDisplays] section for information about the supported braille displays. NVDA supports braille codes for many languages, including contracted, uncontracted and computer braille codes. @@ -600,6 +601,7 @@ If you own a braille display, NVDA can display information in braille. If your braille display has a Perkins-style keyboard, you can also enter contracted or uncontracted braille. Please see the [Supported Braille Displays #SupportedBrailleDisplays] section for information about the supported braille displays. +This section also contains information about what displays support NVDA's automatic background braille display detection functionality. You can configure braille using the [Braille category #BrailleSettings] of the [NVDA Settings #NVDASettings] dialog. ++ Control Type, State and Landmark abbreviations ++[BrailleAbbreviations] @@ -1231,9 +1233,12 @@ If there is an error loading the display driver, NVDA will notify you with a mes This combo box presents you with several options depending on what braille display drivers are available on your system. Move between these options with the arrow keys. +The automatic option will allow NVDA to search for many supported braille displays in the background. +When this feature is enabled and you connect a supported display using USB or bluetooth, NVDA will automatically connect with this display. + No braille means that you are not using braille. -Please see the [Supported Braille Displays #SupportedBrailleDisplays] section for more information about supported braille displays. +Please see the [Supported Braille Displays #SupportedBrailleDisplays] section for more information about supported braille displays and which of these support automatic detection in the background. ==== Port ====[SelectBrailleDisplayPort] This option, if available, allows you to choose what port or type of connection will be used to communicate with the braille display you have selected. @@ -1891,6 +1896,22 @@ This synthesizer does not support [spelling functionality #SpeechSettingsUseSpel + Supported Braille Displays +[SupportedBrailleDisplays] This section contains information about the Braille displays supported by NVDA. +++ Displays supporting automatic detection in the background ++[AutomaticDetection] +NVDA has the ability to detect many braille displays in the background automatically, either via USB or bluetooth. +This behavior is achieved by selecting the Automatic option as the preferred braille display from NVDA's [Braille Settings dialog #BrailleSettings]. +This option is selected by default. + +The following displays support this automatic detection functionality. +- Handy Tech displays +- Baum/Humanware/APH/Orbit braille displays +- HumanWare Brailliant BI/B series +- HumanWare BrailleNote +- SuperBraille +- Optelec ALVA 6 series +- HIMS Braille Sense/Braille EDGE/Smart Beetle/Sync Braille Series +- Eurobraille Esys/Esytime/Iris displays +- + ++ Freedom Scientific Focus/PAC Mate Series ++[FreedomScientificFocus] All Focus and PAC Mate displays from [Freedom Scientific http://www.freedomscientific.com/] are supported when connected via USB or bluetooth. You will need the Freedom Scientific braille display drivers installed on your system. @@ -1902,6 +1923,8 @@ By default, NVDA can automatically detect and connect to these displays either v However, when configuring the display, you can explicitly select "USB" or "Bluetooth" ports to restrict the connection type to be used. This might be useful if you want to connect the focus display to NVDA using bluetooth, but still be able to charge it using USB power from your computer. +These displays do not yet support NVDA's automatic background braille display detection functionality. + Following are the key assignments for this display with NVDA. Please see the display's documentation for descriptions of where these keys can be found. %kc:beginInclude @@ -2044,6 +2067,8 @@ The Lilli braille display available from [MDV http://www.mdvbologna.it/] is supp You do not need any specific drivers to be installed to use this display. Just plug in the display and configure NVDA to use it. +This display does not support NVDA's automatic background braille display detection functionality. + Following are the key assignments for this display with NVDA. Please see the display's documentation for descriptions of where these keys can be found. %kc:beginInclude @@ -2099,6 +2124,8 @@ For displays which have a joystick: The hedo ProfiLine USB from [hedo Reha-Technik http://www.hedo.de/] is supported. You must first install the USB drivers provided by the manufacturer. +This display does not yet support NVDA's automatic background braille display detection functionality. + Following are the key assignments for this display with NVDA. Please see the display's documentation for descriptions of where these keys can be found. %kc:beginInclude @@ -2116,6 +2143,8 @@ Please see the display's documentation for descriptions of where these keys can The hedo MobilLine USB from [hedo Reha-Technik http://www.hedo.de/] is supported. You must first install the USB drivers provided by the manufacturer. +This display does not yet support NVDA's automatic background braille display detection functionality. + Following are the key assignments for this display with NVDA. Please see the display's documentation for descriptions of where these keys can be found. %kc:beginInclude @@ -2258,6 +2287,8 @@ The Seika Version 3, 4 and 5 (40 cells) and Seika80 (80 cells) braille displays You can find more information about these displays at http://www.seika-braille.com/. You must first install the USB drivers provided by the manufacturer. +These displays do not yet support NVDA's automatic background braille display detection functionality. + Following are the key assignments for this display with NVDA. Please see the display's documentation for descriptions of where these keys can be found. %kc:beginInclude @@ -2283,9 +2314,10 @@ The following Braille displays are supported: - BRAILLEX Live 20, BRAILLEX Live and BRAILLEX Live Plus (USB and bluetooth) - +These displays do not support NVDA's automatic background braille display detection functionality. + If BrxCom is installed, NVDA will use BrxCom. BrxCom is a tool that allows keyboard input from the braille display to function independently from a screen reader. -A new version of BrxCom which works with NVDA will be released by Papenmeier soon. Keyboard input is possible with the Trio and BRAILLEX Live models. Most devices have an Easy Access Bar (EAB) that allows intuitive and fast operation. @@ -2363,7 +2395,8 @@ The following Braille displays are supported: - Note that these displays can only be connected via a serial port. -Therefore, you should select the port to which the display is connected after you have chosen this driver in the [Select Braille Display #SelectBrailleDisplay] dialog. +Due to this, these displays do not support NVDA's automatic background braille display detection functionality. +You should select the port to which the display is connected after you have chosen this driver in the [Select Braille Display #SelectBrailleDisplay] dialog. Some of these devices have an Easy Access Bar (EAB) that allows intuitive and fast operation. The EAB can be moved in four directions where generally each direction has two switches. @@ -2541,6 +2574,7 @@ The following models are supported: - In NVDA, you can set the serial port to which the display is connected in the [Select Braille Display #SelectBrailleDisplay] dialog. +These displays do not support NVDA's automatic background braille display detection functionality. Following are the key assignments for EcoBraille displays. Please see the [EcoBraille documentation ftp://ftp.once.es/pub/utt/bibliotecnia/Lineas_Braille/ECO/] for descriptions of where these keys can be found. @@ -2669,6 +2703,8 @@ When configuring the display and port to use, be sure to pay close attention to For displays which have a braille keyboard, BRLTTY currently handles braille input itself. Therefore, NVDA's braille input table setting is not relevant. +BRLTYY is not involved in NVDA's automatic background braille display detection functionality. + Following are the BRLTTY command assignments for NVDA. Please see the [BRLTTY key binding lists http://mielke.cc/brltty/doc/KeyBindings/] for information about how BRLTTY commands are mapped to controls on braille displays. %kc:beginInclude