diff --git a/source/bdDetect.py b/source/bdDetect.py index 2536c5f9501..dc823700d85 100644 --- a/source/bdDetect.py +++ b/source/bdDetect.py @@ -159,11 +159,21 @@ def _get_hidDevices(self): deviceInfoFetcher = _DeviceInfoFetcher() class Detector(object): - """Automatically detect braille displays. + """Detector class used to automatically detect braille displays. This should only be used by the L{braille} module. """ - def __init__(self): + def __init__(self, usb=True, bluetooth=True, limitToDevices=None): + """Constructor. + The keyword arguments initialize the detector in a particular state. + On an initialized instance, these initial arguments can be overridden by calling L{_startBgScan} or L{rescan}. + @param usb: Whether this instance should detect USB devices initially. + @type usb: bool + @param bluetooth: Whether this instance should detect Bluetooth devices initially. + @type bluetooth: bool + @param limitToDevices: Drivers to which detection should be limited initially. + C{None} if no driver filtering should occur. + """ self._BgScanApc = winKernel.PAPCFUNC(self._bgScan) self._btDevsLock = threading.Lock() self._btDevs = None @@ -172,11 +182,12 @@ def __init__(self): self._stopEvent = threading.Event() self._queuedScanLock = threading.Lock() self._scanQueued = False - self._detectUsb = False - self._detectBluetooth = False + self._detectUsb = usb + self._detectBluetooth = bluetooth + self._limitToDevices = limitToDevices self._runningApcLock = threading.Lock() # Perform initial scan. - self._startBgScan(usb=True, bluetooth=True) + self._startBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices) @property def _scanQueuedSafe(self): @@ -190,10 +201,21 @@ def _scanQueuedSafe(self, state): with self._queuedScanLock: self._scanQueued = state - def _startBgScan(self, usb=False, bluetooth=False): + def _startBgScan(self, usb=False, bluetooth=False, limitToDevices=None): + """Starts a scan for devices. + If a scan is already in progress, a new scan will be queued after the current scan. + To explicitely cancel a scan in progress, use L{rescan}. + @param usb: Whether USB devices should be detected for this and subsequent scans. + @type usb: bool + @param bluetooth: Whether Bluetooth devices should be detected for this and subsequent scans. + @type bluetooth: bool + @param limitToDevices: Drivers to which detection should be limited for this and subsequent scans. + C{None} if no driver filtering should occur. + """ with self._queuedScanLock: self._detectUsb = usb self._detectBluetooth = bluetooth + self._limitToDevices = limitToDevices if not self._scanQueued: self._scanQueued = True if self._runningApcLock.locked(): @@ -224,11 +246,12 @@ def _bgScan(self, param): self._scanQueued = False detectUsb = self._detectUsb detectBluetooth = self._detectBluetooth + limitToDevices = self._limitToDevices if detectUsb: if self._stopEvent.isSet(): continue for driver, match in getDriversForConnectedUsbDevices(): - if self._stopEvent.isSet(): + if self._stopEvent.isSet() or (self._limitToDevices and driver not in self._limitToDevices): continue if braille.handler.setDisplayByName(driver, detected=match): return @@ -244,7 +267,7 @@ def _bgScan(self, param): btDevs = self._btDevs btDevsCache = btDevs for driver, match in btDevs: - if self._stopEvent.isSet(): + if self._stopEvent.isSet() or (self._limitToDevices and driver not in self._limitToDevices): continue if btDevsCache is not btDevs: btDevsCache.append((driver, match)) @@ -256,25 +279,35 @@ def _bgScan(self, param): with self._btDevsLock: self._btDevs = btDevsCache - def rescan(self): - """Stop a current scan when in progress, and start scanning from scratch.""" + def rescan(self, usb=True, bluetooth=True, limitToDevices=None): + """Stop a current scan when in progress, and start scanning from scratch. + @param usb: Whether USB devices should be detected for this and subsequent scans. + @type usb: bool + @param bluetooth: Whether Bluetooth devices should be detected for this and subsequent scans. + @type bluetooth: bool + @param limitToDevices: Drivers to which detection should be limited for this and subsequent scans. + C{None} if no driver filtering should occur. + """ 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) + self._startBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices) def handleWindowMessage(self, msg=None, wParam=None): if msg == WM_DEVICECHANGE and wParam == DBT_DEVNODES_CHANGED: - self.rescan() + self.rescan(bluetooth=self._detectBluetooth, limitToDevices=self._limitToDevices) def pollBluetoothDevices(self): """Poll bluetooth devices that might be in range. This does not cancel the current scan.""" + if not self._detectBluetooth: + # Do not poll bluetooth devices at all when bluetooth is disabled. + return with self._btDevsLock: if not self._btDevs: return - self._startBgScan(bluetooth=True) + self._startBgScan(bluetooth=self._detectBluetooth, limitToDevices=self._limitToDevices) def terminate(self): appModuleHandler.post_appSwitch.unregister(self.pollBluetoothDevices) @@ -466,6 +499,20 @@ def driverSupportsAutoDetection(driver): addBluetoothDevices("eurobraille", lambda m: m.id.startswith("Esys")) +# freedomScientific +addUsbDevices("freedomScientific", KEY_CUSTOM, { + "VID_0F4E&PID_0100", # Focus 1 + "VID_0F4E&PID_0111", # PAC Mate + "VID_0F4E&PID_0112", # Focus 2 + "VID_0F4E&PID_0114", # Focus Blue +}) + +addBluetoothDevices("freedomScientific", lambda m: any(m.id.startswith(prefix) for prefix in ( + "F14", "Focus 14 BT", + "Focus 40 BT", + "Focus 80 BT", +))) + # handyTech addUsbDevices("handyTech", KEY_SERIAL, { "VID_0403&PID_6001", # FTDI chip diff --git a/source/braille.py b/source/braille.py index 6e846ca9cc8..8d3f9b932d1 100644 --- a/source/braille.py +++ b/source/braille.py @@ -1616,7 +1616,7 @@ def setDisplayByName(self, name, isFallback=False, detected=None): # #8032: Take note of the display requested, even if it is going to fail. self._lastRequestedDisplayName=name if name == AUTO_DISPLAY_NAME: - self._enableDetection() + self._enableDetection(keepCurrentDisplay=False) return True elif not isFallback and not detected: self._disableDetection() @@ -1670,7 +1670,11 @@ def setDisplayByName(self, name, isFallback=False, detected=None): self.displaySize = newDisplay.numCells self.enabled = bool(self.displaySize) if isFallback: - self._resumeDetection() + if self._detectionEnabled and not self._detector: + # As this is the fallback display, which is usually noBraille, + # we can keep the current display when enabling detection. + # Note that in this case, L{_detectionEnabled} is set by L{handleDisplayUnavailable} + self._enableDetection(keepCurrentDisplay=True) elif not detected: config.conf["braille"]["display"] = name else: # detected: @@ -1684,11 +1688,13 @@ def setDisplayByName(self, name, isFallback=False, detected=None): # We should handle this more gracefully, since this is no reason # to stop a display from loading successfully. log.debugWarning("Error in initial display after display load", exc_info=True) + if detected and 'bluetoothName' in detected.deviceInfo: + self._enableDetection(bluetooth=False, keepCurrentDisplay=True, limitToDevices=[name]) return True except: # 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) + log.error("Error initializing display driver %s for kwargs %r"%(name,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) @@ -1990,18 +1996,20 @@ def handleDisplayUnavailable(self): 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): + + def _enableDetection(self, usb=True, bluetooth=True, keepCurrentDisplay=False, limitToDevices=None): """Enables automatic detection of braille displays. When auto detection is already active, this will force a rescan for devices. + This should also be executed when auto detection should be resumed due to loss of display connectivity. """ if self._detectionEnabled and self._detector: - self._detector.rescan() + self._detector.rescan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices) return _BgThread.start() config.conf["braille"]["display"] = AUTO_DISPLAY_NAME - self.setDisplayByName("noBraille", isFallback=True) - self._detector = bdDetect.Detector() + if not keepCurrentDisplay: + self.setDisplayByName("noBraille", isFallback=True) + self._detector = bdDetect.Detector(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices) self._detectionEnabled = True def _disableDetection(self): @@ -2013,14 +2021,6 @@ def _disableDetection(self): 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. """ diff --git a/source/brailleDisplayDrivers/freedomScientific.py b/source/brailleDisplayDrivers/freedomScientific.py old mode 100644 new mode 100755 index 8b925185aad..49eb851bd1a --- a/source/brailleDisplayDrivers/freedomScientific.py +++ b/source/brailleDisplayDrivers/freedomScientific.py @@ -1,292 +1,579 @@ +# -*- coding: UTF-8 -*- #brailleDisplayDrivers/freedomScientific.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) 2008-2017 NV Access Limited +#Copyright (C) 2008-2018 NV Access Limited, Bram Duvigneau, Leonard de Ruijter -from ctypes import * -from ctypes.wintypes import * -from collections import OrderedDict +""" +Braille display driver for Freedom Scientific braille displays. +A c(lang) reference implementation is available in brltty. +""" + +from six import BytesIO, int2byte import itertools -import hwPortUtils import braille import inputCore from baseObject import ScriptableObject -from winUser import WNDCLASSEXW, WNDPROC, LRESULT, HCURSOR from logHandler import log +import bdDetect import brailleInput - -#Try to load the fs braille dll -try: - fsbLib=windll.fsbrldspapi -except: - fsbLib=None - -#Map the needed functions in the fs braille dll -if fsbLib: - fbOpen=getattr(fsbLib,'_fbOpen@12') - fbGetCellCount=getattr(fsbLib,'_fbGetCellCount@4') - fbWrite=getattr(fsbLib,'_fbWrite@16') - fbClose=getattr(fsbLib,'_fbClose@4') - fbConfigure=getattr(fsbLib, '_fbConfigure@8') - fbGetDisplayName=getattr(fsbLib, "_fbGetDisplayName@12") - fbGetFirmwareVersion=getattr(fsbLib, "_fbGetFirmwareVersion@12") - fbBeep=getattr(fsbLib, "_fbBeep@4") - -FB_INPUT=1 -FB_DISCONNECT=2 -FB_EXT_KEY=3 - -LRESULT=c_long -HCURSOR=c_long - -appInstance=windll.kernel32.GetModuleHandleW(None) - -nvdaFsBrlWm=windll.user32.RegisterWindowMessageW(u"nvdaFsBrlWm") - -inputType_keys=3 -inputType_routing=4 -inputType_wizWheel=5 - -# Names of freedom scientific bluetooth devices -bluetoothNames = ( - "F14", "Focus 14 BT", - "Focus 40 BT", - "Focus 80 BT", -) - -keysPressed=0 -extendedKeysPressed=0 -@WNDPROC -def nvdaFsBrlWndProc(hwnd,msg,wParam,lParam): - global keysPressed, extendedKeysPressed - keysDown=0 - extendedKeysDown=0 - if msg==nvdaFsBrlWm and wParam in (FB_INPUT, FB_EXT_KEY): - if wParam==FB_INPUT: - inputType=lParam&0xff - if inputType==inputType_keys: - keyBits=lParam>>8 - keysDown=keyBits - keysPressed |= keyBits - elif inputType==inputType_routing: - routingIndex=(lParam>>8)&0xff - isRoutingPressed=bool((lParam>>16)&0xff) - isTopRoutingRow=bool((lParam>>24)&0xff) - if isRoutingPressed: - gesture=RoutingGesture(routingIndex,isTopRoutingRow) - try: - inputCore.manager.executeGesture(gesture) - except inputCore.NoInputGestureAction: - pass - elif inputType==inputType_wizWheel: - numUnits=(lParam>>8)&0x7 - isRight=bool((lParam>>12)&1) - isDown=bool((lParam>>11)&1) - #Right's up and down are rversed, but NVDA does not want this - if isRight: isDown=not isDown - for unit in xrange(numUnits): - gesture=WizWheelGesture(isDown,isRight) - try: - inputCore.manager.executeGesture(gesture) - except inputCore.NoInputGestureAction: - pass - elif wParam==FB_EXT_KEY: - keyBits=lParam>>4 - extendedKeysDown=keyBits - extendedKeysPressed|=keyBits - if keysDown==0 and extendedKeysDown==0 and (keysPressed!=0 or extendedKeysPressed!=0): - gesture=KeyGesture(keysPressed,extendedKeysPressed) - keysPressed=extendedKeysPressed=0 - try: - inputCore.manager.executeGesture(gesture) - except inputCore.NoInputGestureAction: - pass - return 0 - else: - return windll.user32.DefWindowProcW(hwnd,msg,wParam,lParam) - -nvdaFsBrlWndCls=WNDCLASSEXW() -nvdaFsBrlWndCls.cbSize=sizeof(nvdaFsBrlWndCls) -nvdaFsBrlWndCls.lpfnWndProc=nvdaFsBrlWndProc -nvdaFsBrlWndCls.hInstance=appInstance -nvdaFsBrlWndCls.lpszClassName=u"nvdaFsBrlWndCls" - -class BrailleDisplayDriver(braille.BrailleDisplayDriver,ScriptableObject): - - name="freedomScientific" +import hwIo +import serial + + +BAUD_RATE = 57600 +PARITY = serial.PARITY_NONE + +#: Model names and number of cells +MODELS = { + "Focus 14": 14, + "Focus 40": 40, + "Focus 44": 44, + "Focus 70": 70, + "Focus 80": 80, + "Focus 84": 84, + "pm display 20": 20, + "pm display 40": 40, +} + +#: Number of cells of Focus first generation displays +# The assumption is that any displays with the following cell counts are due to three cells at the +# beginning/end of the display are used as status cells, and an extra blank cell to separate status +# from normal cells. These devices require a special translation table: L{FOCUS_1_TRANSLATION_TABLE} +# This line of displays is known as the first generation Focus displays. +FOCUS_1_CELL_COUNTS = (44, 70, 84,) + +# Packet types +#: Query the display for information such as manufacturer, model and firmware version +FS_PKT_QUERY = b"\x00" +#: Response from the display that acknowledges a packet has been received +FS_PKT_ACK = b"\x01" +#: Negative response from the display indicating a problem +FS_PKT_NAK = b"\x02" +#: The display indicates that one ore more keys on the display are pressed/released. This includes normal buttons and the braille keyboard +FS_PKT_KEY = b"\x03" +#: A routing button on the display is pressed/released +FS_PKT_BUTTON = b"\x04" +#: Indicates a whiz wheel has turned. Please note that on newer models the wheels have been replaced by buttons, but there is no difference in the protocol. +FS_PKT_WHEEL = b"\x05" +#: Set braille dot firmness. Not yet used in this driver. +FS_PKT_HVADJ = b"\x08" +#: Lets the display beep. Not yet used in this driver. +FS_PKT_BEEP = b"\x09" +#: Sends a configuration request to the display. Mainly used to enable extended key mode on newer displays to use all the buttons. +FS_PKT_CONFIG = b"\x0F" +#: Indicates a response to FS_PKT_QUERY from the display +FS_PKT_INFO = b"\x80" +#: Sends braille cells to the display. +FS_PKT_WRITE = b"\x81" +#: Indicates extended keys have been pressed. Newer displays use this for some of their keys, see also the list in KeyGesture.extendedKeyLabels +FS_PKT_EXT_KEY = b"\x82" + +# Parts of packets +#: An empty packet argument or null byte +FS_BYTE_NULL = b"\x00" +#: Empty data in the packet payload +FS_DATA_EMPTY = b"" +#: Send extended key events, to be used with the FS_PKT_CONFIG +FS_CFG_EXTKEY = b"\x02" + +# FS_PKT_INFO payload offsets +#: Start position of manufacturer in FS_PKT_INFO payload +INFO_MANU_START = 0 +#: End position of manufacturer in FS_PKT_INFO payload +INFO_MANU_END = 24 +#: Start position of model in FS_PKT_INFO payload +INFO_MODEL_START = INFO_MANU_END +#: End position of model in FS_PKT_INFO payload +INFO_MODEL_END = INFO_MODEL_START + 16 +#: Start position of firmware version in FS_PKT_INFO payload +INFO_VERSION_START = INFO_MODEL_END +#: End position of firmware version in FS_PKT_INFO payload +INFO_VERSION_END = INFO_MODEL_END + 8 + +# Braille translation +#: The number of dots in a braille character/cell +DOTS_TABLE_SIZE = 8 +#: THe size of a full braille translation table including all possible dot combinations +TRANSLATION_TABLE_SIZE = 2 ** DOTS_TABLE_SIZE + +def _makeTranslationTable(dotsTable): + """Create a translation table for braille dot combinations + + @param dotsTable: The list of 8 bitmasks to use for each dot (dot 1 - 8) + """ + def isoDot(number): + """ + Returns the ISO 11548 formatted braille dot for the given number. + + From least- to most-significant octal digit: + + * the first contains dots 1-3 + * the second contains dots 4-6 + * the third contains dots 7-8 + + Based on: https://github.com/brltty/brltty/blob/master/Headers/brl_dots.h + + @param number: The dot to encode (1-8) + @type number: int + """ + return 1 << (number - 1) + + outputTable = [0] * TRANSLATION_TABLE_SIZE + for byte in xrange(TRANSLATION_TABLE_SIZE): + cell = 0 + for dot in xrange(DOTS_TABLE_SIZE): + if byte & isoDot(dot + 1): + cell |= dotsTable[dot] + outputTable[byte] = cell + return outputTable + +def _translate(cells, translationTable): + """Translate cells according to a translation table + + The translation table contains the bytes to encode all the possible dot combinations. + See L{_makeTranslationTable} as well. + + @param cells: The cells to translate, given in ISO 11548 format (used by most braille displays) + @type cells: [int] + @param translationTable: A list of all possible braille dot combinations + @type translationTable: [int] + """ + outCells = [0] * len(cells) + for i, cell in enumerate(cells): + outCells[i] = translationTable[cell] + return outCells + +#: Dots table used by first generation Focus displays +FOCUS_1_DOTS_TABLE = [ + 0X01, 0X02, 0X04, 0X10, 0X20, 0X40, 0X08, 0X80 +] + +#: Braille translation table used by first generation Focus displays +FOCUS_1_TRANSLATION_TABLE = _makeTranslationTable(FOCUS_1_DOTS_TABLE) + + +class BrailleDisplayDriver(braille.BrailleDisplayDriver, ScriptableObject): + """ + Driver for Freedom Scientific braille displays + """ + name = "freedomScientific" # Translators: Names of braille displays. - description=_("Freedom Scientific Focus/PAC Mate series") - - @classmethod - def check(cls): - return bool(fsbLib) - - @classmethod - def getPossiblePorts(cls): - ports = OrderedDict([cls.AUTOMATIC_PORT, ("USB", "USB",)]) - try: - cls._getBluetoothPorts().next() - ports["bluetooth"] = "Bluetooth" - except StopIteration: - pass - return ports - - @classmethod - def _getBluetoothPorts(cls): - for p in hwPortUtils.listComPorts(): - try: - btName = p["bluetoothName"] - except KeyError: - continue - if not any(btName == prefix or btName.startswith(prefix + " ") for prefix in bluetoothNames): - continue - yield p["port"].encode("mbcs") - - wizWheelActions=[ - # Translators: The name of a key on a braille display, that scrolls the display to show previous/next part of a long line. - (_("display scroll"),("globalCommands","GlobalCommands","braille_scrollBack"),("globalCommands","GlobalCommands","braille_scrollForward")), + description = _("Freedom Scientific Focus/PAC Mate series") + isThreadSafe = True + receivesAckPackets = True + timeout = 0.2 + + wizWheelActions = [ + # Translators: The name of a key on a braille display, that scrolls the display + # to show previous/next part of a long line. + (_("display scroll"), ("globalCommands", "GlobalCommands", "braille_scrollBack"), + ("globalCommands", "GlobalCommands", "braille_scrollForward")), # Translators: The name of a key on a braille display, that scrolls the display to show the next/previous line. - (_("line scroll"),("globalCommands","GlobalCommands","braille_previousLine"),("globalCommands","GlobalCommands","braille_nextLine")), + (_("line scroll"), ("globalCommands", "GlobalCommands", "braille_previousLine"), + ("globalCommands", "GlobalCommands", "braille_nextLine")), ] def __init__(self, port="auto"): - self.leftWizWheelActionCycle=itertools.cycle(self.wizWheelActions) - action=self.leftWizWheelActionCycle.next() - self.gestureMap.add("br(freedomScientific):leftWizWheelUp",*action[1]) - self.gestureMap.add("br(freedomScientific):leftWizWheelDown",*action[2]) - self.rightWizWheelActionCycle=itertools.cycle(self.wizWheelActions) - action=self.rightWizWheelActionCycle.next() - self.gestureMap.add("br(freedomScientific):rightWizWheelUp",*action[1]) - self.gestureMap.add("br(freedomScientific):rightWizWheelDown",*action[2]) - super(BrailleDisplayDriver,self).__init__() - self._messageWindowClassAtom=windll.user32.RegisterClassExW(byref(nvdaFsBrlWndCls)) - self._messageWindow=windll.user32.CreateWindowExW(0,self._messageWindowClassAtom,u"nvdaFsBrlWndCls window",0,0,0,0,0,None,None,appInstance,None) - if port == "auto": - portsToTry = itertools.chain(["USB"], self._getBluetoothPorts()) - elif port == "bluetooth": - portsToTry = self._getBluetoothPorts() - else: # USB - portsToTry = ["USB"] - fbHandle=-1 - for port in portsToTry: - fbHandle=fbOpen(port,self._messageWindow,nvdaFsBrlWm) - if fbHandle!=-1: + self.numCells = 0 + self._ackPending = False + self._pendingCells = [] + self._keyBits = 0 + self._extendedKeyBits = 0 + self._ignoreKeyReleases = False + self._model = None + self._manufacturer = None + self._firmwareVersion = None + self.translationTable = None + self.leftWizWheelActionCycle = itertools.cycle(self.wizWheelActions) + action = self.leftWizWheelActionCycle.next() + self.gestureMap.add("br(freedomScientific):leftWizWheelUp", *action[1]) + self.gestureMap.add("br(freedomScientific):leftWizWheelDown", *action[2]) + self.rightWizWheelActionCycle = itertools.cycle(self.wizWheelActions) + action = self.rightWizWheelActionCycle.next() + self.gestureMap.add("br(freedomScientific):rightWizWheelUp", *action[1]) + self.gestureMap.add("br(freedomScientific):rightWizWheelDown", *action[2]) + super(BrailleDisplayDriver, self).__init__() + for portType, portId, port, portInfo in self._getTryPorts(port): + self.isUsb = portType == bdDetect.KEY_CUSTOM + # Try talking to the display. + try: + if self.isUsb: + self._dev = hwIo.Bulk( + port, + epIn=1, + epOut=0, + onReceive=self._onReceive, + writeSize=0, + onReceiveSize=56 + ) + else: + self._dev = hwIo.Serial( + port, + baudrate=BAUD_RATE, + parity=PARITY, + timeout=self.timeout, + writeTimeout=self.timeout, + onReceive=self._onReceive + ) + except EnvironmentError: + log.debugWarning("", exc_info=True) + continue + + # Send an identification request + self._sendPacket(FS_PKT_QUERY) + for _i in xrange(3): + self._dev.waitForRead(self.timeout) + if self.numCells and self._model: + break + + if self.numCells and self._model: + # A display responded. + log.info("Found {device} connected via {type} ({port})".format( + device=self._model, type=portType, port=port)) break - if fbHandle==-1: - windll.user32.DestroyWindow(self._messageWindow) - windll.user32.UnregisterClassW(self._messageWindowClassAtom,appInstance) - raise RuntimeError("No display found") - self.fbHandle=fbHandle - self._configureDisplay() - numCells=self.numCells - self.gestureMap.add("br(freedomScientific):topRouting1","globalCommands","GlobalCommands","braille_scrollBack") - self.gestureMap.add("br(freedomScientific):topRouting%d"%numCells,"globalCommands","GlobalCommands","braille_scrollForward") + self._dev.close() - def terminate(self): - super(BrailleDisplayDriver,self).terminate() - fbClose(self.fbHandle) - windll.user32.DestroyWindow(self._messageWindow) - windll.user32.UnregisterClassW(self._messageWindowClassAtom,appInstance) + else: + raise RuntimeError("No Freedom Scientific display found") - def _get_numCells(self): - return fbGetCellCount(self.fbHandle) + self._configureDisplay() + self.gestureMap.add("br(freedomScientific):topRouting1", + "globalCommands", "GlobalCommands", "braille_scrollBack") + self.gestureMap.add("br(freedomScientific):topRouting%d" % self.numCells, + "globalCommands", "GlobalCommands", "braille_scrollForward") - def display(self,cells): - cells="".join([chr(x) for x in cells]) - fbWrite(self.fbHandle,0,len(cells),cells) + def terminate(self): + try: + super(BrailleDisplayDriver, self).terminate() + finally: + # Make sure the device gets closed. + # If it doesn't, we may not be able to re-open it later. + self._dev.close() + + def _sendPacket(self, packetType, arg1=FS_BYTE_NULL, arg2=FS_BYTE_NULL, arg3=FS_BYTE_NULL, data=FS_DATA_EMPTY): + """Send a packet to the display + + @param packetType: Type of packet (first byte), use one of the FS_PKT constants + @type packetType: str + @param arg1: First argument (second byte of packet) + @type arg1: str + @param arg2: Second argument (third byte of packet) + @type arg2: str + @param arg3: Third argument (fourth byte of packet) + @type arg3: str + @param data: Data to send if this is an extended packet, required checksum will be added automatically + @type data: str + """ + def handleArg(arg): + if type(arg) == int: + return int2byte(arg) + return arg + arg1 = handleArg(arg1) + arg2 = handleArg(arg2) + arg3 = handleArg(arg3) + packet = [packetType, arg1, arg2, arg3, data] + if data: + packet.append(int2byte(BrailleDisplayDriver._calculateChecksum("".join(packet)))) + self._dev.write("".join(packet)) + + def _onReceive(self, data): + """Event handler when data from the display is received + + Formats a packet of four bytes in a packet type and three arguments. + If the packet is known to have a payload, this is also fetched and the checksum is verified. + The constructed packet is handed off to L{_handlePacket}. + """ + if self.isUsb: + data = BytesIO(data) + packetType = data.read(1) + else: + packetType = data + data = self._dev + + arg1 = data.read(1) + arg2 = data.read(1) + arg3 = data.read(1) + log.debug("Got packet of type %r with args: %r %r %r", packetType, arg1, arg2, arg3) + # Info and extended key responses are the only packets with payload and checksum + if packetType in (FS_PKT_INFO, FS_PKT_EXT_KEY): + length = ord(arg1) + payload = data.read(length) + checksum = ord(data.read(1)) + calculatedChecksum = BrailleDisplayDriver._calculateChecksum(packetType + arg1 + arg2 + arg3 + payload) + assert calculatedChecksum == checksum, "Checksum mismatch, expected %s but got %s" % (checksum, payload[-1]) + else: + payload = FS_DATA_EMPTY + + self._handlePacket(packetType, arg1, arg2, arg3, payload) + + def _handlePacket(self, packetType, arg1, arg2, arg3, payload): + """Handle a packet from the device" + + The following packet types are handled: + + * FS_PKT_ACK: See L{_handleAck} + * FS_PKT_NAK: Logged and handled as an ACK + * FS_PKT_INFO: Manufacturer, model and firmware version are extracted and set as + properties on the object. Cell count is determined based on L{MODELS}. + * arg1: length of payload + * payload: manufacturer, model, firmware version in a fixed width field string + * FS_PKT_WHEEL: The corresponding L{WheelGesture}s are sent for the wheel events. + * arg1: movement direction (up/down) and number of clicks moved + Bits: BBBAAA (least significant) + * A: (bits 1-3) number of clicks the wheel has moved + * B: (bits 4-6) which wheel (left/right) and what direction (up/down) + * FS_PKT_BUTTON: the corresponding L{RoutingGesture} is sent + * arg1: number of routing button + * arg2: key press/release + * arg3: if this is a button on the second row of routing buttons + * FS_PKT_KEY: a key or button on the display is pressed/released (including the braille keyboard) + * arg 1, 2, 3, 4: + These bytes form the value indicating which of the 8 keys are pressed on the device. + Key releases can be detected by comparing to the previous state, this work is done in L{_handleKeys}. + * FS_PKT_EXT_KEY: ?? + * payload: The 4 most significant bits from a single byte are used. + More investigation is required. + """ + if packetType == FS_PKT_ACK: + self._handleAck() + elif packetType == FS_PKT_NAK: + log.debugWarning("NAK received!") + self._handleAck() + elif packetType == FS_PKT_INFO: + self._manufacturer = payload[INFO_MANU_START:INFO_MANU_END].replace(FS_BYTE_NULL, "") + self._model = payload[INFO_MODEL_START:INFO_MODEL_END].replace(FS_BYTE_NULL, "") + self._firmwareVersion = payload[INFO_VERSION_START:INFO_VERSION_END].replace(FS_BYTE_NULL, "") + self.numCells = MODELS.get(self._model, 0) + if self.numCells in FOCUS_1_CELL_COUNTS: + # Focus first gen: apply custom translation table + self.translationTable = FOCUS_1_TRANSLATION_TABLE + log.debug("Device info: manufacturer: %s model: %s, version: %s", + self._manufacturer, self._model, self._firmwareVersion) + elif packetType == FS_PKT_WHEEL: + threeLeastSigBitsMask = 0x7 + count = ord(arg1) & threeLeastSigBitsMask + wheelNumber = ((ord(arg1) >> 3) & threeLeastSigBitsMask) + try: + # There are only two wheels, one on the left, one on the right. + # Either wheel could have moved up or down. + isDown, isRight = [ + (False, False), + (True, False), + (True, True), + (False, True) + ][wheelNumber] + except IndexError: + log.debugWarning("wheelNumber unknown") + return + for _i in xrange(count): + gesture = WizWheelGesture(self._model, isDown, isRight) + try: + inputCore.manager.executeGesture(gesture) + except inputCore.NoInputGestureAction: + pass + elif packetType == FS_PKT_BUTTON: + key = ord(arg1) + # the least significant bit is set when the key is pressed + leastSigBitMask = 0x01 + isPress = bool(ord(arg2) & leastSigBitMask) + isTopRow = bool(ord(arg3)) + if isPress: + # Ignore keypresses + return + gesture = RoutingGesture(self._model, key, isTopRow) + try: + inputCore.manager.executeGesture(gesture) + except inputCore.NoInputGestureAction: + pass + elif packetType == FS_PKT_KEY: + keyBits = ord(arg1) | (ord(arg2) << 8) | (ord(arg3) << 16) + self._handleKeys(keyBits) + elif packetType == FS_PKT_EXT_KEY: + keyBits = ord(payload[0]) >> 4 + self._handleExtendedKeys(keyBits) + else: + log.debugWarning("Unknown packet of type: %r", packetType) + + def _handleAck(self): + "Displays any queued cells after receiving an ACK" + super(BrailleDisplayDriver, self)._handleAck() + if self._pendingCells: + self.display(self._pendingCells) + + @staticmethod + def _updateKeyBits(keyBits, oldKeyBits, keyCount): + """Helper function that reports if keys have been pressed and which keys have been released + based on old and new keybits. + """ + isRelease = False + keyBitsBeforeRelease = 0 + newKeysPressed = False + keyBit = 0X1 + keyBits |= oldKeyBits & ~((0X1 << keyCount) - 1) + while oldKeyBits != keyBits: + oldKey = oldKeyBits & keyBit + newKey = keyBits & keyBit + + if oldKey and not newKey: + # A key has been released + isRelease = True + if not keyBitsBeforeRelease: + keyBitsBeforeRelease = oldKeyBits + oldKeyBits &= ~keyBit + elif newKey and not oldKey: + oldKeyBits |= keyBit + newKeysPressed = True + + keyBit <<= 1 + return oldKeyBits, isRelease, keyBitsBeforeRelease, newKeysPressed + + def _handleKeys(self, keyBits): + """Send gestures if keys are released and update self._keyBits""" + keyBits, isRelease, keyBitsBeforeRelease, newKeysPressed = self._updateKeyBits(keyBits, self._keyBits, 24) + if newKeysPressed: + self._ignoreKeyReleases = False + self._keyBits = keyBits + if isRelease and not self._ignoreKeyReleases: + gesture = KeyGesture(self._model, keyBitsBeforeRelease, self._extendedKeyBits) + try: + inputCore.manager.executeGesture(gesture) + except inputCore.NoInputGestureAction: + pass + self._ignoreKeyReleases = True + + def _handleExtendedKeys(self, keyBits): + """Send gestures if keys are released and update self._extendedKeyBits""" + keyBits, isRelease, keyBitsBeforeRelease, newKeysPressed = self._updateKeyBits(keyBits, self._extendedKeyBits, 24) + if newKeysPressed: + self._ignoreKeyReleases = False + self._extendedKeyBits = keyBits + if isRelease and not self._ignoreKeyReleases: + gesture = KeyGesture(self._model, self._keyBits, keyBitsBeforeRelease) + try: + inputCore.manager.executeGesture(gesture) + except inputCore.NoInputGestureAction: + pass + self._ignoreKeyReleases = True + + @staticmethod + def _calculateChecksum(data): + """Calculate the checksum for extended packets""" + checksum = 0 + for byte in data: + checksum -= ord(byte) + checksum = checksum & 0xFF + return checksum + + def display(self, cells): + if self.translationTable: + cells = _translate(cells, FOCUS_1_TRANSLATION_TABLE) + if not self._awaitingAck: + cells = b"".join([int2byte(x) for x in cells]) + self._sendPacket(FS_PKT_WRITE, int2byte(self.numCells), FS_BYTE_NULL, FS_BYTE_NULL, cells) + self._pendingCells = [] + else: + self._pendingCells = cells def _configureDisplay(self): - # See what display we are connected to - displayName= firmwareVersion="" - buf = create_string_buffer(16) - if fbGetDisplayName(self.fbHandle, buf, 16): - displayName=buf.value - if fbGetFirmwareVersion(self.fbHandle, buf, 16): - firmwareVersion=buf.value - if displayName and firmwareVersion and displayName=="Focus" and ord(firmwareVersion[0])>=ord('3'): + """Enable extended keys on Focus firmware 3 and up""" + if not self._model or not self._firmwareVersion: + return + if self._model.startswith("Focus") and ord(self._firmwareVersion[0]) >= ord("3"): # Focus 2 or later. Make sure extended keys support is enabled. - log.debug("Activating extended keys on freedom Scientific display. Display name: %s, firmware version: %s.", displayName, firmwareVersion) - fbConfigure(self.fbHandle, 0x02) - - def script_toggleLeftWizWheelAction(self,gesture): - action=self.leftWizWheelActionCycle.next() - self.gestureMap.add("br(freedomScientific):leftWizWheelUp",*action[1],replace=True) - self.gestureMap.add("br(freedomScientific):leftWizWheelDown",*action[2],replace=True) + log.debug("Activating extended keys on freedom Scientific display. Display name: %s, firmware version: %s.", + self._model, self._firmwareVersion) + self._sendPacket(FS_PKT_CONFIG, FS_CFG_EXTKEY) + + def script_toggleLeftWizWheelAction(self, _gesture): + action = self.leftWizWheelActionCycle.next() + self.gestureMap.add("br(freedomScientific):leftWizWheelUp", *action[1], replace=True) + self.gestureMap.add("br(freedomScientific):leftWizWheelDown", *action[2], replace=True) braille.handler.message(action[0]) - def script_toggleRightWizWheelAction(self,gesture): - action=self.rightWizWheelActionCycle.next() - self.gestureMap.add("br(freedomScientific):rightWizWheelUp",*action[1],replace=True) - self.gestureMap.add("br(freedomScientific):rightWizWheelDown",*action[2],replace=True) + def script_toggleRightWizWheelAction(self, _gesture): + action = self.rightWizWheelActionCycle.next() + self.gestureMap.add("br(freedomScientific):rightWizWheelUp", *action[1], replace=True) + self.gestureMap.add("br(freedomScientific):rightWizWheelDown", *action[2], replace=True) braille.handler.message(action[0]) - __gestures={ - "br(freedomScientific):leftWizWheelPress":"toggleLeftWizWheelAction", - "br(freedomScientific):rightWizWheelPress":"toggleRightWizWheelAction", + __gestures = { + "br(freedomScientific):leftWizWheelPress": "toggleLeftWizWheelAction", + "br(freedomScientific):rightWizWheelPress": "toggleRightWizWheelAction", } - gestureMap=inputCore.GlobalGestureMap({ - "globalCommands.GlobalCommands" : { - "braille_routeTo":("br(freedomScientific):routing",), - "braille_scrollBack" : ("br(freedomScientific):leftAdvanceBar", "br(freedomScientific):leftBumperBarUp","br(freedomScientific):rightBumperBarUp",), - "braille_scrollForward" : ("br(freedomScientific):rightAdvanceBar","br(freedomScientific):leftBumperBarDown","br(freedomScientific):rightBumperBarDown",), - "braille_previousLine" : ("br(freedomScientific):leftRockerBarUp", "br(freedomScientific):rightRockerBarUp",), - "braille_nextLine" : ("br(freedomScientific):leftRockerBarDown", "br(freedomScientific):rightRockerBarDown",), + gestureMap = inputCore.GlobalGestureMap({ + "globalCommands.GlobalCommands": { + "braille_routeTo": ("br(freedomScientific):routing",), + "braille_scrollBack": ("br(freedomScientific):leftAdvanceBar", + "br(freedomScientific):leftBumperBarUp", "br(freedomScientific):rightBumperBarUp",), + "braille_scrollForward": ("br(freedomScientific):rightAdvanceBar", + "br(freedomScientific):leftBumperBarDown", "br(freedomScientific):rightBumperBarDown",), + "braille_previousLine": + ("br(freedomScientific):leftRockerBarUp", "br(freedomScientific):rightRockerBarUp",), + "braille_nextLine": ("br(freedomScientific):leftRockerBarDown", "br(freedomScientific):rightRockerBarDown",), "kb:shift+tab": ("br(freedomScientific):dot1+dot2+brailleSpaceBar",), - "kb:tab" : ("br(freedomScientific):dot4+dot5+brailleSpaceBar",), - "kb:upArrow" : ("br(freedomScientific):dot1+brailleSpaceBar",), - "kb:downArrow" : ("br(freedomScientific):dot4+brailleSpaceBar",), - "kb:leftArrow" : ("br(freedomScientific):dot3+brailleSpaceBar",), - "kb:rightArrow" : ("br(freedomScientific):dot6+brailleSpaceBar",), - "kb:control+leftArrow" : ("br(freedomScientific):dot2+brailleSpaceBar",), - "kb:control+rightArrow" : ("br(freedomScientific):dot5+brailleSpaceBar",), - "kb:home" : ("br(freedomScientific):dot1+dot3+brailleSpaceBar",), - "kb:control+home" : ("br(freedomScientific):dot1+dot2+dot3+brailleSpaceBar",), - "kb:end" : ("br(freedomScientific):dot4+dot6+brailleSpaceBar",), - "kb:control+end" : ("br(freedomScientific):dot4+dot5+dot6+brailleSpaceBar",), - "kb:alt" : ("br(freedomScientific):dot1+dot3+dot4+brailleSpaceBar",), - "kb:alt+tab" : ("br(freedomScientific):dot2+dot3+dot4+dot5+brailleSpaceBar",), - "kb:alt+shift+tab" : ("br(freedomScientific):dot1+dot2+dot5+dot6+brailleSpaceBar",), - "kb:windows+tab" : ("br(freedomScientific):dot2+dot3+dot4+brailleSpaceBar",), - "kb:escape" : ("br(freedomScientific):dot1+dot5+brailleSpaceBar",), - "kb:windows" : ("br(freedomScientific):dot2+dot4+dot5+dot6+brailleSpaceBar",), - "kb:windows+d" : ("br(freedomScientific):dot1+dot2+dot3+dot4+dot5+dot6+brailleSpaceBar",), - "reportCurrentLine" : ("br(freedomScientific):dot1+dot4+brailleSpaceBar",), - "showGui" :("br(freedomScientific):dot1+dot3+dot4+dot5+brailleSpaceBar",), - "braille_toggleTether" : ("br(freedomScientific):leftGDFButton+rightGDFButton",), + "kb:tab": ("br(freedomScientific):dot4+dot5+brailleSpaceBar",), + "kb:upArrow": ("br(freedomScientific):dot1+brailleSpaceBar",), + "kb:downArrow": ("br(freedomScientific):dot4+brailleSpaceBar",), + "kb:leftArrow": ("br(freedomScientific):dot3+brailleSpaceBar",), + "kb:rightArrow": ("br(freedomScientific):dot6+brailleSpaceBar",), + "kb:control+leftArrow": ("br(freedomScientific):dot2+brailleSpaceBar",), + "kb:control+rightArrow": ("br(freedomScientific):dot5+brailleSpaceBar",), + "kb:home": ("br(freedomScientific):dot1+dot3+brailleSpaceBar",), + "kb:control+home": ("br(freedomScientific):dot1+dot2+dot3+brailleSpaceBar",), + "kb:end": ("br(freedomScientific):dot4+dot6+brailleSpaceBar",), + "kb:control+end": ("br(freedomScientific):dot4+dot5+dot6+brailleSpaceBar",), + "kb:alt": ("br(freedomScientific):dot1+dot3+dot4+brailleSpaceBar",), + "kb:alt+tab": ("br(freedomScientific):dot2+dot3+dot4+dot5+brailleSpaceBar",), + "kb:alt+shift+tab": ("br(freedomScientific):dot1+dot2+dot5+dot6+brailleSpaceBar",), + "kb:windows+tab": ("br(freedomScientific):dot2+dot3+dot4+brailleSpaceBar",), + "kb:escape": ("br(freedomScientific):dot1+dot5+brailleSpaceBar",), + "kb:windows": ("br(freedomScientific):dot2+dot4+dot5+dot6+brailleSpaceBar",), + "kb:windows+d": ("br(freedomScientific):dot1+dot2+dot3+dot4+dot5+dot6+brailleSpaceBar",), + "reportCurrentLine": ("br(freedomScientific):dot1+dot4+brailleSpaceBar",), + "showGui": ("br(freedomScientific):dot1+dot3+dot4+dot5+brailleSpaceBar",), + "braille_toggleTether": ("br(freedomScientific):leftGDFButton+rightGDFButton",), } }) +# pylint: disable=abstract-method class InputGesture(braille.BrailleDisplayGesture): + """Base gesture for this braille display""" source = BrailleDisplayDriver.name -class KeyGesture(InputGesture, brailleInput.BrailleInputGesture): + def __init__(self, model): + self.model = model.replace(" ", "") + super(InputGesture, self).__init__() - keyLabels=[ - #Braille keys (byte 1) - 'dot1','dot2','dot3','dot4','dot5','dot6','dot7','dot8', - #Assorted keys (byte 2) - 'leftWizWheelPress','rightWizWheelPress', - 'leftShiftKey','rightShiftKey', - 'leftAdvanceBar','rightAdvanceBar', - None, - 'brailleSpaceBar', - #GDF keys (byte 3) - 'leftGDFButton','rightGDFButton', +class KeyGesture(InputGesture, brailleInput.BrailleInputGesture): + """Handle keys and braille input for Freedom Scientific braille displays""" + keyLabels = [ + # Braille keys (byte 1) + "dot1", "dot2", "dot3", "dot4", "dot5", "dot6", "dot7", "dot8", + # Assorted keys (byte 2) + "leftWizWheelPress", "rightWizWheelPress", + "leftShiftKey", "rightShiftKey", + "leftAdvanceBar", "rightAdvanceBar", None, - 'leftBumperBarUp','leftBumperBarDown','rightBumperBarUp','rightBumperBarDown', + "brailleSpaceBar", + # GDF keys (byte 3) + "leftGDFButton", "rightGDFButton", + None, None, + "leftBumperBarUp", "leftBumperBarDown", "rightBumperBarUp", "rightBumperBarDown", ] extendedKeyLabels = [ # Rocker bar keys. "leftRockerBarUp", "leftRockerBarDown", "rightRockerBarUp", "rightRockerBarDown", ] - def __init__(self,keyBits, extendedKeyBits): - super(KeyGesture,self).__init__() - keys=[self.keyLabels[num] for num in xrange(24) if (keyBits>>num)&1] - extendedKeys=[self.extendedKeyLabels[num] for num in xrange(4) if (extendedKeyBits>>num)&1] - self.id="+".join(keys+extendedKeys) + def __init__(self, model, keyBits, extendedKeyBits): + super(KeyGesture, self).__init__(model) + keys = [self.keyLabels[num] for num in xrange(24) if (keyBits>>num) & 1] + extendedKeys = [self.extendedKeyLabels[num] for num in xrange(4) if (extendedKeyBits>>num) & 1] + # pylint: disable=invalid-name + self.id = "+".join(keys+extendedKeys) # Don't say is this a dots gesture if some keys either from dots and space are pressed. if not extendedKeyBits and not keyBits & ~(0xff | (1 << 0xf)): self.dots = keyBits & 0xff @@ -295,19 +582,22 @@ def __init__(self,keyBits, extendedKeyBits): self.space = True class RoutingGesture(InputGesture): - - def __init__(self,routingIndex,topRow=False): + """Gesture to handle cursor routing and second row of routing keys on older models""" + def __init__(self, model, routingIndex, topRow=False): if topRow: - self.id="topRouting%d"%(routingIndex+1) + # pylint: disable=invalid-name + self.id = "topRouting%d"%(routingIndex+1) else: - self.id="routing" - self.routingIndex=routingIndex - super(RoutingGesture,self).__init__() + # pylint: disable=invalid-name + self.id = "routing" + self.routingIndex = routingIndex + super(RoutingGesture, self).__init__(model) class WizWheelGesture(InputGesture): - - def __init__(self,isDown,isRight): - which="right" if isRight else "left" - direction="Down" if isDown else "Up" - self.id="%sWizWheel%s"%(which,direction) - super(WizWheelGesture,self).__init__() + """Gesture to handle wiz wheels movements""" + def __init__(self, model, isDown, isRight): + which = "right" if isRight else "left" + direction = "Down" if isDown else "Up" + # pylint: disable=invalid-name + self.id = "%sWizWheel%s" % (which, direction) + super(WizWheelGesture, self).__init__(model) diff --git a/source/hwIo.py b/source/hwIo.py index db267b2e244..d323d278ce3 100644 --- a/source/hwIo.py +++ b/source/hwIo.py @@ -269,9 +269,7 @@ def __init__(self, path, onReceive, exclusive=True): if handle == INVALID_HANDLE_VALUE: if _isDebug(): log.debug("Open failed: %s" % ctypes.WinError()) - self._file = None raise ctypes.WinError() - self._file = handle pd = ctypes.c_void_p() if not ctypes.windll.hid.HidD_GetPreparsedData(handle, byref(pd)): raise ctypes.WinError() @@ -340,8 +338,6 @@ def setOutputReport(self,report): raise ctypes.WinError() def close(self): - if not self._file: - return super(Hid, self).close() winKernel.closeHandle(self._file) self._file = None diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 5cfbb3336c3..9fe7fa0d094 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -2034,13 +2034,11 @@ All Focus and PAC Mate displays from [Freedom Scientific http://www.freedomscien You will need the Freedom Scientific braille display drivers installed on your system. If you do not have them already, you can obtain them from http://www2.freedomscientific.com/downloads/focus-40-blue/focus-40-14-blue-downloads.asp. Although this page only mentions the Focus Blue display, the drivers support all Freedom Scientific Focus and Pacmate displays. -If your system is running 64 bit Windows and the drivers were already installed by another screen reader, you will probably still need to install the drivers from this link, as the files required by NVDA were probably not installed by the other screen reader. By default, NVDA can automatically detect and connect to these displays either via USB or bluetooth. 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. +NVDA's automatic braille display detection will also recognize the display on USB or Bluetooth. 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.