Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Native driver for Freedom Scientific displays (PR #8853)
Braille output and wizWheels are working. Tested on a Focus 40 blue

Freedom Scientific braille displays are now supported by braille display auto detection (#7727)
When an auto detected braille display is connected via Bluetooth, NVDA will keep searching for USB displays supported by the same driver and switch to a USB connection if it becomes available

The bumper keys now work correctly on Freedom Scientific braille displays (#8849)

Co-Authored-By: Leonard de Ruijter <leonardder@users.noreply.github.com>

fixes #7727
fixes #4464
fixes #8849
fixes #8729
  • Loading branch information
bramd authored and feerrenrut committed May 21, 2019
1 parent f6f9d04 commit 327b2dd
Show file tree
Hide file tree
Showing 5 changed files with 624 additions and 293 deletions.
73 changes: 60 additions & 13 deletions source/bdDetect.py
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
32 changes: 16 additions & 16 deletions source/braille.py
Expand Up @@ -1618,7 +1618,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()
Expand Down Expand Up @@ -1673,7 +1673,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:
Expand All @@ -1687,11 +1691,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)
Expand Down Expand Up @@ -1993,18 +1999,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):
Expand All @@ -2016,14 +2024,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.
"""
Expand Down

0 comments on commit 327b2dd

Please sign in to comment.