From a498a6fb17c1a05fa65f081f6f56665292e4239b Mon Sep 17 00:00:00 2001 From: Felix Gruetzmacher Date: Thu, 7 Feb 2019 15:14:43 +0100 Subject: [PATCH 1/9] Added support for Connect Braille and Basic Braille 84. --- source/bdDetect.py | 2 ++ source/brailleDisplayDrivers/handyTech.py | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/source/bdDetect.py b/source/bdDetect.py index fb2ba0d07ed..2536c5f9501 100644 --- a/source/bdDetect.py +++ b/source/bdDetect.py @@ -475,6 +475,7 @@ def driverSupportsAutoDetection(driver): # Newer Handy Tech displays have a native HID processor addUsbDevices("handyTech", KEY_HID, { "VID_1FE4&PID_0054", # Active Braille + "VID_1FE4&PID_0055", # Connect Braille "VID_1FE4&PID_0081", # Basic Braille 16 "VID_1FE4&PID_0082", # Basic Braille 20 "VID_1FE4&PID_0083", # Basic Braille 32 @@ -483,6 +484,7 @@ def driverSupportsAutoDetection(driver): "VID_1FE4&PID_0086", # Basic Braille 64 "VID_1FE4&PID_0087", # Basic Braille 80 "VID_1FE4&PID_008B", # Basic Braille 160 + "VID_1FE4&PID_008C", # Basic Braille 84 "VID_1FE4&PID_0061", # Actilino "VID_1FE4&PID_0064", # Active Star 40 }) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index dd9047d177d..970bdb64cf6 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -45,7 +45,7 @@ MODEL_MODULAR_CONNECT_88 = b"\x3A" MODEL_EASY_BRAILLE = b"\x44" MODEL_ACTIVE_BRAILLE = b"\x54" -MODEL_CONNECT_BRAILLE_40 = b"\x55" +MODEL_CONNECT_BRAILLE = b"\x55" MODEL_ACTILINO = b"\x61" MODEL_ACTIVE_STAR_40 = b"\x64" MODEL_BASIC_BRAILLE_16 = b"\x81" @@ -56,6 +56,7 @@ MODEL_BASIC_BRAILLE_64 = b"\x86" MODEL_BASIC_BRAILLE_80 = b"\x87" MODEL_BASIC_BRAILLE_160 = b"\x8B" +MODEL_BASIC_BRAILLE_84 = b"\x8C" MODEL_BRAILLINO = b"\x72" MODEL_BRAILLE_STAR_40 = b"\x74" MODEL_BRAILLE_STAR_80 = b"\x78" @@ -355,11 +356,11 @@ class ActiveBraille(TimeSyncMixin, AtcMixin, TripleActionKeysMixin, Model): genericName = name = 'Active Braille' -class ConnectBraille40(TimeSyncMixin, TripleActionKeysMixin, Model): - deviceId = MODEL_CONNECT_BRAILLE_40 +class ConnectBraille(TripleActionKeysMixin, Model): + deviceId = MODEL_CONNECT_BRAILLE numCells = 40 genericName = "Connect Braille" - name = "Connect Braille 40" + name = "Connect Braille" class Actilino(TimeSyncMixin, AtcMixin, JoystickMixin, TripleActionKeysMixin, Model): @@ -417,6 +418,7 @@ def basicBrailleFactory(numCells, deviceId): BasicBraille64 = basicBrailleFactory(64, MODEL_BASIC_BRAILLE_64) BasicBraille80 = basicBrailleFactory(80, MODEL_BASIC_BRAILLE_80) BasicBraille160 = basicBrailleFactory(160, MODEL_BASIC_BRAILLE_160) +BasicBraille84 = basicBrailleFactory(84, MODEL_BASIC_BRAILLE_84) class BrailleStar(TripleActionKeysMixin, Model): From 329e028f1f28c74cd55dffb18b88b8098f354f59 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Fri, 8 Feb 2019 10:19:52 +1000 Subject: [PATCH 2/9] Update what's new. --- user_docs/en/changes.t2t | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0446c7e2f92..e5b97362175 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -19,6 +19,7 @@ What's New in NVDA - Early support for apps such as Mozilla Firefox on computers with ARM64 (e.g. Qualcom Snapdragon) processors. (#9216) - A new Advanced Settings category has been added to NVDA's Settings dialog, including an option to try out NVDA's new support for Microsoft Word via the Microsoft UI Automation API. (#9200) - Added support for the graphical view in Windows Disk Management. (#1486) +- Added support for Handy Tech Connect Braille and Basic Braille 84. (#9249) == Changes == From 7a962f6da70bd6138f35f2a30ff9d344ad10a85e Mon Sep 17 00:00:00 2001 From: Felix Gruetzmacher Date: Fri, 7 Jun 2019 16:00:39 +0200 Subject: [PATCH 3/9] Added support for the Basic Braille Plus family of displays. Added sleep mode functionality so that applications can signal to the driver that it temporarily close its ports. --- source/bdDetect.py | 3 ++ source/brailleDisplayDrivers/handyTech.py | 49 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/source/bdDetect.py b/source/bdDetect.py index 2536c5f9501..be8c6222841 100644 --- a/source/bdDetect.py +++ b/source/bdDetect.py @@ -485,6 +485,8 @@ def driverSupportsAutoDetection(driver): "VID_1FE4&PID_0087", # Basic Braille 80 "VID_1FE4&PID_008B", # Basic Braille 160 "VID_1FE4&PID_008C", # Basic Braille 84 + "VID_1FE4&PID_0093", # Basic Braille Plus 32 + "VID_1FE4&PID_0094", # Basic Braille Plus 40 "VID_1FE4&PID_0061", # Actilino "VID_1FE4&PID_0064", # Active Star 40 }) @@ -501,6 +503,7 @@ def driverSupportsAutoDetection(driver): "Active Braille AB", "Active Star AS", "Basic Braille BB", + "Basic Braille Plus BP", "Braille Star 40 BS", "Braillino BL", "Braille Wave BW", diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index 970bdb64cf6..7fdd423caa4 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -24,6 +24,23 @@ import bdDetect import time import datetime +from ctypes import windll +import windowUtils + +class InvisibleDriverWindow(windowUtils.CustomWindow): + className = u"Handy_Tech_Server" + def __init__(self, driver): + super(InvisibleDriverWindow, self).__init__(u"Handy Tech Server") + self.driver = driver + def windowProc(self, hwnd, msg, wParam, lParam): + if msg == window_message: + if wParam == 100 and lParam == 1: + self.driver.go_to_sleep() + elif wParam == 100 and lParam == 0: + self.driver.wake_up() + return 0 + +window_message=windll.user32.RegisterWindowMessageW(u"Handy_Tech_Server") BAUD_RATE = 19200 PARITY = serial.PARITY_ODD @@ -528,6 +545,7 @@ def getManualPorts(cls): def __init__(self, port="auto"): super(BrailleDisplayDriver, self).__init__() + self._messageWindow=InvisibleDriverWindow(self) self.numCells = 0 self._model = None self._ignoreKeyReleases = False @@ -535,12 +553,14 @@ def __init__(self, port="auto"): self.brailleInput = False self._hidSerialBuffer = b"" self._atc = False + self._sleepcounter = 0 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 == bdDetect.KEY_HID self.isHidSerial = portId in USB_IDS_HID_CONVERTER + self.port = port try: if self.isHidSerial: # This is either the standalone HID adapter cable for older displays, @@ -575,8 +595,33 @@ def __init__(self, port="auto"): else: raise RuntimeError("No Handy Tech display found") + def go_to_sleep(self): + self._sleepcounter += 1 + if self._dev is not None: + self._dev.close() + self._dev = None + time.sleep(self.timeout) + def wake_up(self): + if self._sleepcounter > 0: + self._sleepcounter -= 1 + if self._sleepcounter > 0: # Still not zero after decrementing + return + # Might throw if device no longer exists. + # We leave it to autodetection to grab it when it reappears. + if self.isHidSerial: + # This is either the standalone HID adapter cable for older displays, + # or an older display with a HID - serial adapter built in + self._dev = hwIo.Hid(self.port, onReceive=self._hidSerialOnReceive) + # Send a flush to open the serial channel + self._dev.write(HT_HID_RPT_InCommand + HT_HID_CMD_FlushBuffers) + elif self.isHid: + self._dev = hwIo.Hid(self.port, onReceive=self._hidOnReceive) + else: + self._dev = hwIo.Serial(self.port, baudrate=BAUD_RATE, parity=PARITY, + timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._serialOnReceive) def terminate(self): try: + self._messageWindow.destroy() super(BrailleDisplayDriver, self).terminate() finally: # We must sleep before closing the connection as not doing this can leave the display in a bad state where it can not be re-initialized. @@ -602,6 +647,8 @@ def _set_atc(self, state): self._atc = state def sendPacket(self, packetType, data=""): + if self._dev is None: + return if type(data) == bool or type(data) == int: data = chr(data) if self.isHid: @@ -610,6 +657,8 @@ def sendPacket(self, packetType, data=""): self._dev.write(packetType + data) def sendExtendedPacket(self, packetType, data=""): + if self._dev is None: + return if type(data) == bool or type(data) == int: data = chr(data) packet = b"{length}{extType}{data}\x16".format( From 99d698d42bd9cc8c97f86204e8007d511d782184 Mon Sep 17 00:00:00 2001 From: FelixGruetzmacher Date: Sat, 8 Jun 2019 14:21:57 +0200 Subject: [PATCH 4/9] Hidden message window for Handy Tech driver is now always created and destroyed on the main ui thread. --- source/brailleDisplayDrivers/handyTech.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index d91ed432621..5e8a9595d0e 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -27,6 +27,7 @@ from ctypes import windll import windowUtils from driverHandler import BooleanDriverSetting +import wx class InvisibleDriverWindow(windowUtils.CustomWindow): className = u"Handy_Tech_Server" @@ -552,7 +553,7 @@ def getManualPorts(cls): def __init__(self, port="auto"): super(BrailleDisplayDriver, self).__init__() - self._messageWindow=InvisibleDriverWindow(self) + wx.CallAfter(self.create_message_window) self.numCells = 0 self._model = None self._ignoreKeyReleases = False @@ -601,8 +602,20 @@ def __init__(self, port="auto"): self._dev.close() else: + wx.CallAfter(self.destroy_message_window) raise RuntimeError("No Handy Tech display found") + def create_message_window(self): + try: + self._messageWindow = InvisibleDriverWindow(self) + except: + log.debugWarning("", exc_info=True) + def destroy_message_window(self): + try: + self._messageWindow.destroy() + except: + log.debugWarning("", exc_info=True) + def go_to_sleep(self): self._sleepcounter += 1 if self._dev is not None: @@ -629,7 +642,7 @@ def wake_up(self): timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._serialOnReceive) def terminate(self): try: - self._messageWindow.destroy() + wx.CallAfter(self.destroy_message_window) super(BrailleDisplayDriver, self).terminate() finally: # We must sleep before closing the connection as not doing this can leave the display in a bad state where it can not be re-initialized. From 6f2924d13da84537d745d63a36cba612a70f032a Mon Sep 17 00:00:00 2001 From: FelixGruetzmacher Date: Sat, 8 Jun 2019 23:41:35 +0200 Subject: [PATCH 5/9] Fixed line endings for modified files. --- source/bdDetect.py | 1164 ++++++------ source/brailleDisplayDrivers/handyTech.py | 1958 ++++++++++----------- 2 files changed, 1561 insertions(+), 1561 deletions(-) diff --git a/source/bdDetect.py b/source/bdDetect.py index 9d0ae563cf7..f71a55bbff7 100644 --- a/source/bdDetect.py +++ b/source/bdDetect.py @@ -1,582 +1,582 @@ -#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): - """Detector class used to automatically detect braille displays. - This should only be used by the L{braille} module. - """ - - 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 - 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 = usb - self._detectBluetooth = bluetooth - self._limitToDevices = limitToDevices - self._runningApcLock = threading.Lock() - # Perform initial scan. - self._startBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices) - - @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, 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(): - # 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 - limitToDevices = self._limitToDevices - if detectUsb: - if self._stopEvent.isSet(): - continue - for driver, match in getDriversForConnectedUsbDevices(): - if self._stopEvent.isSet() or (self._limitToDevices and driver not in self._limitToDevices): - 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() or (self._limitToDevices and driver not in self._limitToDevices): - 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, 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=usb, bluetooth=bluetooth, limitToDevices=limitToDevices) - - def handleWindowMessage(self, msg=None, wParam=None): - if msg == WM_DEVICECHANGE and wParam == DBT_DEVNODES_CHANGED: - 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=self._detectBluetooth, limitToDevices=self._limitToDevices) - - 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")) - -# 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 - "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_0055", # Connect 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_008C", # Basic Braille 84 - "VID_1FE4&PID_0093", # Basic Braille Plus 32 - "VID_1FE4&PID_0094", # Basic Braille Plus 40 - "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", - "Basic Braille Plus BP", - "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 -}) - +#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): + """Detector class used to automatically detect braille displays. + This should only be used by the L{braille} module. + """ + + 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 + 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 = usb + self._detectBluetooth = bluetooth + self._limitToDevices = limitToDevices + self._runningApcLock = threading.Lock() + # Perform initial scan. + self._startBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices) + + @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, 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(): + # 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 + limitToDevices = self._limitToDevices + if detectUsb: + if self._stopEvent.isSet(): + continue + for driver, match in getDriversForConnectedUsbDevices(): + if self._stopEvent.isSet() or (self._limitToDevices and driver not in self._limitToDevices): + 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() or (self._limitToDevices and driver not in self._limitToDevices): + 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, 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=usb, bluetooth=bluetooth, limitToDevices=limitToDevices) + + def handleWindowMessage(self, msg=None, wParam=None): + if msg == WM_DEVICECHANGE and wParam == DBT_DEVNODES_CHANGED: + 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=self._detectBluetooth, limitToDevices=self._limitToDevices) + + 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")) + +# 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 + "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_0055", # Connect 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_008C", # Basic Braille 84 + "VID_1FE4&PID_0093", # Basic Braille Plus 32 + "VID_1FE4&PID_0094", # Basic Braille Plus 40 + "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", + "Basic Braille Plus BP", + "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/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index 5e8a9595d0e..98746581739 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -1,979 +1,979 @@ -# -*- coding: UTF-8 -*- -#brailleDisplayDrivers/handyTech.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-2018 NV Access Limited, Bram Duvigneau, Leonard de Ruijter (Babbage B.V.), Felix Grützmacher (Handy Tech Elektronik GmbH) - -""" -Braille display driver for Handy Tech braille displays. -""" - -from collections import OrderedDict -from cStringIO import StringIO -import serial # pylint: disable=E0401 -import weakref -import hwIo -import braille -import brailleInput -import inputCore -import ui -from baseObject import ScriptableObject, AutoPropertyObject -from globalCommands import SCRCAT_BRAILLE -from logHandler import log -import bdDetect -import time -import datetime -from ctypes import windll -import windowUtils -from driverHandler import BooleanDriverSetting -import wx - -class InvisibleDriverWindow(windowUtils.CustomWindow): - className = u"Handy_Tech_Server" - def __init__(self, driver): - super(InvisibleDriverWindow, self).__init__(u"Handy Tech Server") - self.driver = driver - def windowProc(self, hwnd, msg, wParam, lParam): - if msg == window_message: - if wParam == 100 and lParam == 1: - self.driver.go_to_sleep() - elif wParam == 100 and lParam == 0: - self.driver.wake_up() - return 0 - -window_message=windll.user32.RegisterWindowMessageW(u"Handy_Tech_Server") - -BAUD_RATE = 19200 -PARITY = serial.PARITY_ODD - -# pylint: disable=C0330 -# 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 -} - -# Model identifiers -# pylint: disable=C0103 -MODEL_BRAILLE_WAVE = b"\x05" -MODEL_MODULAR_EVOLUTION_64 = b"\x36" -MODEL_MODULAR_EVOLUTION_88 = b"\x38" -MODEL_MODULAR_CONNECT_88 = b"\x3A" -MODEL_EASY_BRAILLE = b"\x44" -MODEL_ACTIVE_BRAILLE = b"\x54" -MODEL_CONNECT_BRAILLE = b"\x55" -MODEL_ACTILINO = b"\x61" -MODEL_ACTIVE_STAR_40 = b"\x64" -MODEL_BASIC_BRAILLE_16 = b"\x81" -MODEL_BASIC_BRAILLE_20 = b"\x82" -MODEL_BASIC_BRAILLE_32 = b"\x83" -MODEL_BASIC_BRAILLE_40 = b"\x84" -MODEL_BASIC_BRAILLE_48 = b"\x8A" -MODEL_BASIC_BRAILLE_64 = b"\x86" -MODEL_BASIC_BRAILLE_80 = b"\x87" -MODEL_BASIC_BRAILLE_160 = b"\x8B" -MODEL_BASIC_BRAILLE_84 = b"\x8C" -MODEL_BRAILLINO = b"\x72" -MODEL_BRAILLE_STAR_40 = b"\x74" -MODEL_BRAILLE_STAR_80 = b"\x78" -MODEL_MODULAR_20 = b"\x80" -MODEL_MODULAR_80 = b"\x88" -MODEL_MODULAR_40 = b"\x89" - -# Key constants -KEY_B1 = 0x03 -KEY_B2 = 0x07 -KEY_B3 = 0x0B -KEY_B4 = 0x0F -KEY_B5 = 0x13 -KEY_B6 = 0x17 -KEY_B7 = 0x1B -KEY_B8 = 0x1F -KEY_LEFT = 0x04 -KEY_RIGHT = 0x08 -KEY_LEFT_SPACE = 0x10 -KEY_RIGHT_SPACE = 0x18 -KEY_ROUTING = 0x20 -KEY_RELEASE_MASK = 0x80 - -# Braille dot mapping -KEY_DOTS = { - KEY_B4: 1, - KEY_B3: 2, - KEY_B2: 3, - KEY_B5: 4, - KEY_B6: 5, - KEY_B7: 6, - KEY_B1: 7, - KEY_B8: 8, -} - -# Considered spaces in braille input mode -KEY_SPACES = (KEY_LEFT_SPACE, KEY_RIGHT_SPACE,) - - -class Model(AutoPropertyObject): - """Extend from this base class to define model specific behavior.""" - #: Device identifier, used in the protocol to identify the device - #: @type: string - deviceId = None - - #: A generic name that identifies the model/series, used in gesture identifiers - #: @type: string - genericName = None - - #: Specific name of this model - #: @type: string - name = None - - #: Number of braille cells - #: @type: int - numCells = 0 - - def __init__(self, display): - super(Model, self).__init__() - # A weak reference to the driver instance, used due to a circular reference between Model and Display - self._displayRef = weakref.ref(display) - - def postInit(self): - """Executed after model initialisation. - - Subclasses may extend this method to perform actions on initialization - of the display. Don't use __init__ for this, since the model ID has - not been set, which is needed for sending packets to the display. - """ - - def _get__display(self): - """The L{BrailleDisplayDriver} which initialized this Model instance""" - # self._displayRef is a weakref, call it to get the object - return self._displayRef() - - # pylint: disable=R0201 - def _get_keys(self): - """Basic keymap - - This returns a basic keymap with sensible defaults for all devices. - Subclasses should override this method to add model specific keys, - or relabel keys. Even if a key isn't available on all devices, add it here - if it would make sense for most devices. - """ - return OrderedDict({ - # Braille input keys - # Numbered from left to right, might be used for braille input on some models - KEY_B1: "b1", - KEY_B2: "b2", - KEY_B3: "b3", - KEY_B4: "b4", - KEY_B5: "b5", - KEY_B6: "b6", - KEY_B7: "b7", - KEY_B8: "b8", - - KEY_LEFT_SPACE: "leftSpace", - KEY_RIGHT_SPACE: "rightSpace", - # Left and right keys, found on Easy Braille and Braille Wave - KEY_LEFT: "left", - KEY_RIGHT: "right", - - # Modular/BS80 keypad - 0x01: "b12", - 0x09: "b13", - 0x05: "n0", - 0x0D: "b14", - - 0x11: "b11", - 0x15: "n1", - 0x19: "n2", - 0x1D: "n3", - - 0x02: "b10", - 0x06: "n4", - 0x0A: "n5", - 0x0E: "n6", - - 0x12: "b9", - 0x16: "n7", - 0x1A: "n8", - 0x1E: "n9", - }) - - def display(self, cells): - """Display cells on the braille display - - This is the modern protocol, which uses an extended packet to send braille - cells. Some displays use an older, simpler protocol. See OldProtocolMixin. - """ - self._display.sendExtendedPacket(HT_EXTPKT_BRAILLE, - "".join(chr(cell) for cell in cells)) - -class OldProtocolMixin(object): - "Mixin for displays using an older protocol to send braille cells and handle input" - - def display(self, cells): - """Write cells to the display according to the old protocol - - This older protocol sends a simple packet starting with HT_PKT_BRAILLE, - followed by the cells. No model ID or lenghth are included. - """ - self._display.sendPacket(HT_PKT_BRAILLE, b"".join(chr(cell) for cell in cells)) - - -class AtcMixin(object): - """Support for displays with Active Tactile Control (ATC)""" - - def postInit(self): - super(AtcMixin, self).postInit() - log.debug("Enabling ATC") - self._display.atc = True - - -class TimeSyncFirmnessMixin(object): - """Functionality for displays that support time synchronization and dot firmness adjustments.""" - - supportedSettings=( - braille.BrailleDisplayDriver.DotFirmnessSetting(defaultVal=1, minVal=0, maxVal=2, useConfig=False), - ) - - def postInit(self): - super(TimeSyncFirmnessMixin, self).postInit() - log.debug("Request current display time") - self._display.sendExtendedPacket(HT_EXTPKT_GET_RTC) - log.debug("Request current dot firmness") - self._display.sendExtendedPacket(HT_EXTPKT_GET_FIRMNESS) - - def handleTime(self, timeStr): - ords = map(ord, timeStr) - try: - displayDateTime = datetime.datetime( - year=ords[0] << 8 | ords[1], - month=ords[2], - day=ords[3], - hour=ords[4], - minute=ords[5], - second=ords[6] - ) - except ValueError: - log.debugWarning("Invalid time/date of Handy Tech display: %r"%timeStr) - return - localDateTime = datetime.datetime.today() - if abs((displayDateTime - localDateTime).total_seconds()) >= 5: - log.debugWarning("Display time out of sync: %s"%displayDateTime.isoformat()) - self.syncTime(localDateTime) - else: - log.debug("Time in sync. Display time %s"%displayDateTime.isoformat()) - - def syncTime(self, dt): - log.debug("Synchronizing braille display date and time...") - # Setting the time uses a swapped byte order for the year. - timeList = [ - dt.year & 0xFF, dt.year >> 8, - dt.month, dt.day, - dt.hour, dt.minute, dt.second - ] - timeStr = b"".join(map(chr, timeList)) - self._display.sendExtendedPacket(HT_EXTPKT_SET_RTC, timeStr) - - -class TripleActionKeysMixin(AutoPropertyObject): - """Triple action keys - - Most Handy Tech models have so called triple action keys. This keys are - on the left and right side of the cells and can be pressed at the top, - at the bottom and in the middle. - """ - def _get_keys(self): - """Add the triple action keys to the keys property""" - keys = super(TripleActionKeysMixin, self).keys - keys.update({ - 0x0C: "leftTakTop", - 0x14: "leftTakBottom", - 0x04: "rightTakTop", - 0x08: "rightTakBottom", - }) - return keys - - -class JoystickMixin(AutoPropertyObject): - """Joystick - - Some Handy Tech models have a joystick, which can be moved left, right, up, - down or clicked on the center. - """ - - def _get_keys(self): - """Add the joystick keys to the keys property""" - keys = super(JoystickMixin, self).keys - keys.update({ - 0x74: "joystickLeft", - 0x75: "joystickRight", - 0x76: "joystickUp", - 0x77: "joystickDown", - 0x78: "joystickAction", - }) - return keys - - -class StatusCellMixin(AutoPropertyObject): - """Status cells and routing keys - - Some Handy Tech models have four status cells with corresponding routing keys. - """ - - def _get_keys(self): - """Add the status routing keys to the keys property""" - keys = super(StatusCellMixin, self).keys - keys.update({ - 0x70: "statusRouting1", - 0x71: "statusRouting2", - 0x72: "statusRouting3", - 0x73: "statusRouting4", - }) - return keys - - def display(self, cells): - """Display braille on the display with empty status cells - - Some displays (e.g. Modular series) have 4 status cells. - These cells need to be included in the braille data, but since NVDA doesn't - support status cells, we just send empty cells. - """ - cells = [0] * 4 + cells - super(StatusCellMixin, self).display(cells) - - -class ModularConnect88(TripleActionKeysMixin, Model): - deviceId = MODEL_MODULAR_CONNECT_88 - genericName = "Modular Connect" - name = "Modular Connect 88" - numCells = 88 - - -# pylint: disable=C0111 -class ModularEvolution(AtcMixin, TripleActionKeysMixin, Model): - genericName = "Modular Evolution" - - def _get_name(self): - return '{name} {cells}'.format(name=self.genericName, cells=self.numCells) - - -class ModularEvolution88(ModularEvolution): - deviceId = MODEL_MODULAR_EVOLUTION_88 - numCells = 88 - - -class ModularEvolution64(ModularEvolution): - deviceId = MODEL_MODULAR_EVOLUTION_64 - numCells = 64 - - -class EasyBraille(OldProtocolMixin, Model): - deviceId = MODEL_EASY_BRAILLE - numCells = 40 - genericName = name = "Easy Braille" - - -class ActiveBraille(TimeSyncFirmnessMixin, AtcMixin, TripleActionKeysMixin, Model): - deviceId = MODEL_ACTIVE_BRAILLE - numCells = 40 - genericName = name = 'Active Braille' - - -class ConnectBraille(TripleActionKeysMixin, Model): - deviceId = MODEL_CONNECT_BRAILLE - numCells = 40 - genericName = "Connect Braille" - name = "Connect Braille" - - -class Actilino(TimeSyncFirmnessMixin, AtcMixin, JoystickMixin, TripleActionKeysMixin, Model): - deviceId = MODEL_ACTILINO - numCells = 16 - genericName = name = "Actilino" - - -class ActiveStar40(TimeSyncFirmnessMixin, AtcMixin, TripleActionKeysMixin, Model): - deviceId = MODEL_ACTIVE_STAR_40 - numCells = 40 - name = "Active Star 40" - genericName = "Active Star" - - -class Braillino(TripleActionKeysMixin, OldProtocolMixin, Model): - deviceId = MODEL_BRAILLINO - numCells = 20 - genericName = name = 'Braillino' - - -class BrailleWave(OldProtocolMixin, Model): - deviceId = MODEL_BRAILLE_WAVE - numCells = 40 - genericName = name = "Braille Wave" - - def _get_keys(self): - keys = super(BrailleWave, self).keys - keys.update({ - 0x0C: "escape", - 0x14: "return", - KEY_LEFT_SPACE: "space", - }) - return keys - - -class BasicBraille(Model): - genericName = "Basic Braille" - - def _get_name(self): - return '{name} {cells}'.format(name=self.genericName, cells=self.numCells) - - -def basicBrailleFactory(numCells, deviceId): - return type("BasicBraille{cells}".format(cells=numCells), (BasicBraille,), { - "deviceId": deviceId, - "numCells": numCells, - }) - -BasicBraille16 = basicBrailleFactory(16, MODEL_BASIC_BRAILLE_16) -BasicBraille20 = basicBrailleFactory(20, MODEL_BASIC_BRAILLE_20) -BasicBraille32 = basicBrailleFactory(32, MODEL_BASIC_BRAILLE_32) -BasicBraille40 = basicBrailleFactory(40, MODEL_BASIC_BRAILLE_40) -BasicBraille48 = basicBrailleFactory(48, MODEL_BASIC_BRAILLE_48) -BasicBraille64 = basicBrailleFactory(64, MODEL_BASIC_BRAILLE_64) -BasicBraille80 = basicBrailleFactory(80, MODEL_BASIC_BRAILLE_80) -BasicBraille160 = basicBrailleFactory(160, MODEL_BASIC_BRAILLE_160) -BasicBraille84 = basicBrailleFactory(84, MODEL_BASIC_BRAILLE_84) - - -class BrailleStar(TripleActionKeysMixin, Model): - genericName = "Braille Star" - - def _get_name(self): - return '{name} {cells}'.format(name=self.genericName, cells=self.numCells) - - -class BrailleStar40(BrailleStar): - deviceId = MODEL_BRAILLE_STAR_40 - numCells = 40 - - -class BrailleStar80(BrailleStar): - deviceId = MODEL_BRAILLE_STAR_80 - numCells = 80 - - -class Modular(StatusCellMixin, TripleActionKeysMixin, OldProtocolMixin, Model): - genericName = "Modular" - - def _get_name(self): - return '{name} {cells}'.format(name=self.genericName, cells=self.numCells) - - -class Modular20(Modular): - deviceId = MODEL_MODULAR_20 - numCells = 20 - - -class Modular40(Modular): - deviceId = MODEL_MODULAR_40 - numCells = 40 - - -class Modular80(Modular): - deviceId = MODEL_MODULAR_80 - numCells = 80 - - -def _allSubclasses(cls): - """List all direct and indirect subclasses of cls - - This function calls itself recursively to return all subclasses of cls. - - @param cls: the base class to list subclasses of - @type cls: class - @rtype: [class] - """ - return cls.__subclasses__() + [g for s in cls.__subclasses__() - for g in _allSubclasses(s)] - -# Model dict for easy lookup -MODELS = { - m.deviceId: m for m in _allSubclasses(Model) if hasattr(m, 'deviceId') -} - - -# Packet types -HT_PKT_BRAILLE = b"\x01" -HT_PKT_EXTENDED = b"\x79" -HT_PKT_NAK = b"\x7D" -HT_PKT_ACK = b"\x7E" -HT_PKT_OK = b"\xFE" -HT_PKT_RESET = b"\xFF" -HT_EXTPKT_BRAILLE = HT_PKT_BRAILLE -HT_EXTPKT_KEY = b"\x04" -HT_EXTPKT_CONFIRMATION = b"\x07" -HT_EXTPKT_SCANCODE = b"\x09" -HT_EXTPKT_PING = b"\x19" -HT_EXTPKT_SERIAL_NUMBER = b"\x41" -HT_EXTPKT_SET_RTC = b"\x44" -HT_EXTPKT_GET_RTC = b"\x45" -HT_EXTPKT_BLUETOOTH_PIN = b"\x47" -HT_EXTPKT_SET_ATC_MODE = b"\x50" -HT_EXTPKT_SET_ATC_SENSITIVITY = b"\x51" -HT_EXTPKT_ATC_INFO = b"\x52" -HT_EXTPKT_SET_ATC_SENSITIVITY_2 = b"\x53" -HT_EXTPKT_GET_ATC_SENSITIVITY_2 = b"\x54" -HT_EXTPKT_READING_POSITION = b"\x55" -HT_EXTPKT_SET_FIRMNESS = b"\x60" -HT_EXTPKT_GET_FIRMNESS = b"\x61" -HT_EXTPKT_GET_PROTOCOL_PROPERTIES = b"\xC1" -HT_EXTPKT_GET_FIRMWARE_VERSION = b"\xC2" - -# HID specific constants -HT_HID_RPT_OutData = b"\x01" # receive data from device -HT_HID_RPT_InData = b"\x02" # send data to device -HT_HID_RPT_InCommand = b"\xFB" # run USB-HID firmware command -HT_HID_RPT_OutVersion = b"\xFC" # get version of USB-HID firmware -HT_HID_RPT_OutBaud = b"\xFD" # get baud rate of serial connection -HT_HID_RPT_InBaud = b"\xFE" # set baud rate of serial connection -HT_HID_CMD_FlushBuffers = b"\x01" # flush input and output buffers - -class BrailleDisplayDriver(braille.BrailleDisplayDriver, ScriptableObject): - name = "handyTech" - # Translators: The name of a series of braille displays. - description = _("Handy Tech braille displays") - isThreadSafe = True - receivesAckPackets = True - timeout = 0.2 - - @classmethod - def getManualPorts(cls): - return braille.getSerialPorts() - - def __init__(self, port="auto"): - super(BrailleDisplayDriver, self).__init__() - wx.CallAfter(self.create_message_window) - self.numCells = 0 - self._model = None - self._ignoreKeyReleases = False - self._keysDown = set() - self.brailleInput = False - self._dotFirmness = 1 - self._hidSerialBuffer = b"" - self._atc = False - self._sleepcounter = 0 - - 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 == bdDetect.KEY_HID - self.isHidSerial = portId in USB_IDS_HID_CONVERTER - self.port = port - try: - if self.isHidSerial: - # This is either the standalone HID adapter cable for older displays, - # or an older display with a HID - serial adapter built in - self._dev = hwIo.Hid(port, onReceive=self._hidSerialOnReceive) - # Send a flush to open the serial channel - self._dev.write(HT_HID_RPT_InCommand + HT_HID_CMD_FlushBuffers) - elif self.isHid: - self._dev = hwIo.Hid(port, onReceive=self._hidOnReceive) - else: - self._dev = hwIo.Serial(port, baudrate=BAUD_RATE, parity=PARITY, - timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._serialOnReceive) - except EnvironmentError: - log.debugWarning("", exc_info=True) - continue - - self.sendPacket(HT_PKT_RESET) - for _i in xrange(3): - # An expected response hasn't arrived yet, so wait for it. - self._dev.waitForRead(self.timeout) - if self.numCells and self._model: - break - - if self.numCells: - # A display responded. - self._model.postInit() - log.info("Found {device} connected via {type} ({port})".format( - device=self._model.name, type=portType, port=port)) - break - self._dev.close() - - else: - wx.CallAfter(self.destroy_message_window) - raise RuntimeError("No Handy Tech display found") - - def create_message_window(self): - try: - self._messageWindow = InvisibleDriverWindow(self) - except: - log.debugWarning("", exc_info=True) - def destroy_message_window(self): - try: - self._messageWindow.destroy() - except: - log.debugWarning("", exc_info=True) - - def go_to_sleep(self): - self._sleepcounter += 1 - if self._dev is not None: - self._dev.close() - self._dev = None - time.sleep(self.timeout) - def wake_up(self): - if self._sleepcounter > 0: - self._sleepcounter -= 1 - if self._sleepcounter > 0: # Still not zero after decrementing - return - # Might throw if device no longer exists. - # We leave it to autodetection to grab it when it reappears. - if self.isHidSerial: - # This is either the standalone HID adapter cable for older displays, - # or an older display with a HID - serial adapter built in - self._dev = hwIo.Hid(self.port, onReceive=self._hidSerialOnReceive) - # Send a flush to open the serial channel - self._dev.write(HT_HID_RPT_InCommand + HT_HID_CMD_FlushBuffers) - elif self.isHid: - self._dev = hwIo.Hid(self.port, onReceive=self._hidOnReceive) - else: - self._dev = hwIo.Serial(self.port, baudrate=BAUD_RATE, parity=PARITY, - timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._serialOnReceive) - def terminate(self): - try: - wx.CallAfter(self.destroy_message_window) - super(BrailleDisplayDriver, self).terminate() - finally: - # We must sleep before closing the connection as not doing this can leave the display in a bad state where it can not be re-initialized. - # This has been observed for Easy Braille displays. - time.sleep(self.timeout) - # Make sure the device gets closed. - self._dev.close() - # We also must sleep after closing, as it sometimes takes some time for the device to disconnect. - # This has been observed for Active Braille displays. - time.sleep(self.timeout) - - def _get_supportedSettings(self): - settings = [ - braille.BrailleDisplayDriver.BrailleInputSetting(), - ] - if self._model: - # Add the per model supported settings to the list. - for cls in self._model.__class__.__mro__: - if hasattr(cls, "supportedSettings"): - settings.extend(cls.supportedSettings) - return settings - - def _get_atc(self): - return self._atc - - def _set_atc(self, state): - if self._atc is state: - return - if isinstance(self._model,AtcMixin): - self.sendExtendedPacket(HT_EXTPKT_SET_ATC_MODE, state) - else: - log.debugWarning("Changing ATC setting for unsupported device %s"%self._model.name) - # Regardless whether this setting is supported or not, we want to safe its state. - self._atc = state - - def _get_dotFirmness(self): - return self._dotFirmness - - def _set_dotFirmness(self, value): - if self._dotFirmness is value: - return - if isinstance(self._model,TimeSyncFirmnessMixin): - self.sendExtendedPacket(HT_EXTPKT_SET_FIRMNESS, value) - else: - log.debugWarning("Changing dot firmness setting for unsupported device %s"%self._model.name) - # Regardless whether this setting is supported or not, we want to safe its state. - self._dotFirmness = value - - def sendPacket(self, packetType, data=""): - if self._dev is None: - return - if type(data) == bool or type(data) == int: - data = chr(data) - if self.isHid: - self._sendHidPacket(packetType+data) - else: - self._dev.write(packetType + data) - - def sendExtendedPacket(self, packetType, data=""): - if self._dev is None: - return - if type(data) == bool or type(data) == int: - data = chr(data) - packet = b"{length}{extType}{data}\x16".format( - extType=packetType, data=data, - length=chr(len(data) + len(packetType)) - ) - if self._model: - packet = self._model.deviceId + packet - self.sendPacket(HT_PKT_EXTENDED, packet) - - def _sendHidPacket(self, packet): - assert self.isHid - maxBlockSize = self._dev._writeSize-3 - # When the packet length exceeds C{writeSize}, the packet is split up into several packets. - # They contain C{HT_HID_RPT_InData}, the length of the data block, - # the data block itself and a terminating null character. - for offset in xrange(0, len(packet), maxBlockSize): - block = packet[offset:offset+maxBlockSize] - hidPacket = HT_HID_RPT_InData + chr(len(block)) + block + b"\x00" - self._dev.write(hidPacket) - - def _handleKeyRelease(self): - if self._ignoreKeyReleases or not self._keysDown: - return - # The first key released executes the key combination. - try: - inputCore.manager.executeGesture( - InputGesture(self._model, self._keysDown, self.brailleInput)) - except inputCore.NoInputGestureAction: - pass - # Any further releases are just the rest of the keys in the combination - # being released, so they should be ignored. - self._ignoreKeyReleases = True - - - # pylint: disable=R0912 - # Pylint complains about many branches, might be worth refactoring - def _hidOnReceive(self, data): - # data contains the entire packet. - stream = StringIO(data) - htPacketType = data[2] - # Skip the header, so reading the stream will only give the rest of the data - stream.seek(3) - self._handleInputStream(htPacketType, stream) - - def _hidSerialOnReceive(self, data): - # The HID serial converter wraps one or two bytes into a single HID packet - hidLength = ord(data[1]) - self._hidSerialBuffer+=data[2:(2+hidLength)] - self._processHidSerialBuffer() - - def _processHidSerialBuffer(self): - while self._hidSerialBuffer: - currentBufferLength=len(self._hidSerialBuffer) - htPacketType = self._hidSerialBuffer[0] - if htPacketType!=HT_PKT_EXTENDED: - packetLength = 2 if htPacketType==HT_PKT_OK else 1 - if currentBufferLength>=packetLength: - stream = StringIO(self._hidSerialBuffer[:packetLength]) - self._hidSerialBuffer = self._hidSerialBuffer[packetLength:] - else: - # The packet is not yet complete - return - elif htPacketType==HT_PKT_EXTENDED and currentBufferLength>=5: - # Check whether our packet is complete - # Extended packets are at least 5 bytes in size. - # The second byte is the model, the third byte is the data length, excluding the terminator - packet_length = ord(self._hidSerialBuffer[2])+4 - if len(self._hidSerialBuffer)= 5: + log.debugWarning("Display time out of sync: %s"%displayDateTime.isoformat()) + self.syncTime(localDateTime) + else: + log.debug("Time in sync. Display time %s"%displayDateTime.isoformat()) + + def syncTime(self, dt): + log.debug("Synchronizing braille display date and time...") + # Setting the time uses a swapped byte order for the year. + timeList = [ + dt.year & 0xFF, dt.year >> 8, + dt.month, dt.day, + dt.hour, dt.minute, dt.second + ] + timeStr = b"".join(map(chr, timeList)) + self._display.sendExtendedPacket(HT_EXTPKT_SET_RTC, timeStr) + + +class TripleActionKeysMixin(AutoPropertyObject): + """Triple action keys + + Most Handy Tech models have so called triple action keys. This keys are + on the left and right side of the cells and can be pressed at the top, + at the bottom and in the middle. + """ + def _get_keys(self): + """Add the triple action keys to the keys property""" + keys = super(TripleActionKeysMixin, self).keys + keys.update({ + 0x0C: "leftTakTop", + 0x14: "leftTakBottom", + 0x04: "rightTakTop", + 0x08: "rightTakBottom", + }) + return keys + + +class JoystickMixin(AutoPropertyObject): + """Joystick + + Some Handy Tech models have a joystick, which can be moved left, right, up, + down or clicked on the center. + """ + + def _get_keys(self): + """Add the joystick keys to the keys property""" + keys = super(JoystickMixin, self).keys + keys.update({ + 0x74: "joystickLeft", + 0x75: "joystickRight", + 0x76: "joystickUp", + 0x77: "joystickDown", + 0x78: "joystickAction", + }) + return keys + + +class StatusCellMixin(AutoPropertyObject): + """Status cells and routing keys + + Some Handy Tech models have four status cells with corresponding routing keys. + """ + + def _get_keys(self): + """Add the status routing keys to the keys property""" + keys = super(StatusCellMixin, self).keys + keys.update({ + 0x70: "statusRouting1", + 0x71: "statusRouting2", + 0x72: "statusRouting3", + 0x73: "statusRouting4", + }) + return keys + + def display(self, cells): + """Display braille on the display with empty status cells + + Some displays (e.g. Modular series) have 4 status cells. + These cells need to be included in the braille data, but since NVDA doesn't + support status cells, we just send empty cells. + """ + cells = [0] * 4 + cells + super(StatusCellMixin, self).display(cells) + + +class ModularConnect88(TripleActionKeysMixin, Model): + deviceId = MODEL_MODULAR_CONNECT_88 + genericName = "Modular Connect" + name = "Modular Connect 88" + numCells = 88 + + +# pylint: disable=C0111 +class ModularEvolution(AtcMixin, TripleActionKeysMixin, Model): + genericName = "Modular Evolution" + + def _get_name(self): + return '{name} {cells}'.format(name=self.genericName, cells=self.numCells) + + +class ModularEvolution88(ModularEvolution): + deviceId = MODEL_MODULAR_EVOLUTION_88 + numCells = 88 + + +class ModularEvolution64(ModularEvolution): + deviceId = MODEL_MODULAR_EVOLUTION_64 + numCells = 64 + + +class EasyBraille(OldProtocolMixin, Model): + deviceId = MODEL_EASY_BRAILLE + numCells = 40 + genericName = name = "Easy Braille" + + +class ActiveBraille(TimeSyncFirmnessMixin, AtcMixin, TripleActionKeysMixin, Model): + deviceId = MODEL_ACTIVE_BRAILLE + numCells = 40 + genericName = name = 'Active Braille' + + +class ConnectBraille(TripleActionKeysMixin, Model): + deviceId = MODEL_CONNECT_BRAILLE + numCells = 40 + genericName = "Connect Braille" + name = "Connect Braille" + + +class Actilino(TimeSyncFirmnessMixin, AtcMixin, JoystickMixin, TripleActionKeysMixin, Model): + deviceId = MODEL_ACTILINO + numCells = 16 + genericName = name = "Actilino" + + +class ActiveStar40(TimeSyncFirmnessMixin, AtcMixin, TripleActionKeysMixin, Model): + deviceId = MODEL_ACTIVE_STAR_40 + numCells = 40 + name = "Active Star 40" + genericName = "Active Star" + + +class Braillino(TripleActionKeysMixin, OldProtocolMixin, Model): + deviceId = MODEL_BRAILLINO + numCells = 20 + genericName = name = 'Braillino' + + +class BrailleWave(OldProtocolMixin, Model): + deviceId = MODEL_BRAILLE_WAVE + numCells = 40 + genericName = name = "Braille Wave" + + def _get_keys(self): + keys = super(BrailleWave, self).keys + keys.update({ + 0x0C: "escape", + 0x14: "return", + KEY_LEFT_SPACE: "space", + }) + return keys + + +class BasicBraille(Model): + genericName = "Basic Braille" + + def _get_name(self): + return '{name} {cells}'.format(name=self.genericName, cells=self.numCells) + + +def basicBrailleFactory(numCells, deviceId): + return type("BasicBraille{cells}".format(cells=numCells), (BasicBraille,), { + "deviceId": deviceId, + "numCells": numCells, + }) + +BasicBraille16 = basicBrailleFactory(16, MODEL_BASIC_BRAILLE_16) +BasicBraille20 = basicBrailleFactory(20, MODEL_BASIC_BRAILLE_20) +BasicBraille32 = basicBrailleFactory(32, MODEL_BASIC_BRAILLE_32) +BasicBraille40 = basicBrailleFactory(40, MODEL_BASIC_BRAILLE_40) +BasicBraille48 = basicBrailleFactory(48, MODEL_BASIC_BRAILLE_48) +BasicBraille64 = basicBrailleFactory(64, MODEL_BASIC_BRAILLE_64) +BasicBraille80 = basicBrailleFactory(80, MODEL_BASIC_BRAILLE_80) +BasicBraille160 = basicBrailleFactory(160, MODEL_BASIC_BRAILLE_160) +BasicBraille84 = basicBrailleFactory(84, MODEL_BASIC_BRAILLE_84) + + +class BrailleStar(TripleActionKeysMixin, Model): + genericName = "Braille Star" + + def _get_name(self): + return '{name} {cells}'.format(name=self.genericName, cells=self.numCells) + + +class BrailleStar40(BrailleStar): + deviceId = MODEL_BRAILLE_STAR_40 + numCells = 40 + + +class BrailleStar80(BrailleStar): + deviceId = MODEL_BRAILLE_STAR_80 + numCells = 80 + + +class Modular(StatusCellMixin, TripleActionKeysMixin, OldProtocolMixin, Model): + genericName = "Modular" + + def _get_name(self): + return '{name} {cells}'.format(name=self.genericName, cells=self.numCells) + + +class Modular20(Modular): + deviceId = MODEL_MODULAR_20 + numCells = 20 + + +class Modular40(Modular): + deviceId = MODEL_MODULAR_40 + numCells = 40 + + +class Modular80(Modular): + deviceId = MODEL_MODULAR_80 + numCells = 80 + + +def _allSubclasses(cls): + """List all direct and indirect subclasses of cls + + This function calls itself recursively to return all subclasses of cls. + + @param cls: the base class to list subclasses of + @type cls: class + @rtype: [class] + """ + return cls.__subclasses__() + [g for s in cls.__subclasses__() + for g in _allSubclasses(s)] + +# Model dict for easy lookup +MODELS = { + m.deviceId: m for m in _allSubclasses(Model) if hasattr(m, 'deviceId') +} + + +# Packet types +HT_PKT_BRAILLE = b"\x01" +HT_PKT_EXTENDED = b"\x79" +HT_PKT_NAK = b"\x7D" +HT_PKT_ACK = b"\x7E" +HT_PKT_OK = b"\xFE" +HT_PKT_RESET = b"\xFF" +HT_EXTPKT_BRAILLE = HT_PKT_BRAILLE +HT_EXTPKT_KEY = b"\x04" +HT_EXTPKT_CONFIRMATION = b"\x07" +HT_EXTPKT_SCANCODE = b"\x09" +HT_EXTPKT_PING = b"\x19" +HT_EXTPKT_SERIAL_NUMBER = b"\x41" +HT_EXTPKT_SET_RTC = b"\x44" +HT_EXTPKT_GET_RTC = b"\x45" +HT_EXTPKT_BLUETOOTH_PIN = b"\x47" +HT_EXTPKT_SET_ATC_MODE = b"\x50" +HT_EXTPKT_SET_ATC_SENSITIVITY = b"\x51" +HT_EXTPKT_ATC_INFO = b"\x52" +HT_EXTPKT_SET_ATC_SENSITIVITY_2 = b"\x53" +HT_EXTPKT_GET_ATC_SENSITIVITY_2 = b"\x54" +HT_EXTPKT_READING_POSITION = b"\x55" +HT_EXTPKT_SET_FIRMNESS = b"\x60" +HT_EXTPKT_GET_FIRMNESS = b"\x61" +HT_EXTPKT_GET_PROTOCOL_PROPERTIES = b"\xC1" +HT_EXTPKT_GET_FIRMWARE_VERSION = b"\xC2" + +# HID specific constants +HT_HID_RPT_OutData = b"\x01" # receive data from device +HT_HID_RPT_InData = b"\x02" # send data to device +HT_HID_RPT_InCommand = b"\xFB" # run USB-HID firmware command +HT_HID_RPT_OutVersion = b"\xFC" # get version of USB-HID firmware +HT_HID_RPT_OutBaud = b"\xFD" # get baud rate of serial connection +HT_HID_RPT_InBaud = b"\xFE" # set baud rate of serial connection +HT_HID_CMD_FlushBuffers = b"\x01" # flush input and output buffers + +class BrailleDisplayDriver(braille.BrailleDisplayDriver, ScriptableObject): + name = "handyTech" + # Translators: The name of a series of braille displays. + description = _("Handy Tech braille displays") + isThreadSafe = True + receivesAckPackets = True + timeout = 0.2 + + @classmethod + def getManualPorts(cls): + return braille.getSerialPorts() + + def __init__(self, port="auto"): + super(BrailleDisplayDriver, self).__init__() + wx.CallAfter(self.create_message_window) + self.numCells = 0 + self._model = None + self._ignoreKeyReleases = False + self._keysDown = set() + self.brailleInput = False + self._dotFirmness = 1 + self._hidSerialBuffer = b"" + self._atc = False + self._sleepcounter = 0 + + 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 == bdDetect.KEY_HID + self.isHidSerial = portId in USB_IDS_HID_CONVERTER + self.port = port + try: + if self.isHidSerial: + # This is either the standalone HID adapter cable for older displays, + # or an older display with a HID - serial adapter built in + self._dev = hwIo.Hid(port, onReceive=self._hidSerialOnReceive) + # Send a flush to open the serial channel + self._dev.write(HT_HID_RPT_InCommand + HT_HID_CMD_FlushBuffers) + elif self.isHid: + self._dev = hwIo.Hid(port, onReceive=self._hidOnReceive) + else: + self._dev = hwIo.Serial(port, baudrate=BAUD_RATE, parity=PARITY, + timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._serialOnReceive) + except EnvironmentError: + log.debugWarning("", exc_info=True) + continue + + self.sendPacket(HT_PKT_RESET) + for _i in xrange(3): + # An expected response hasn't arrived yet, so wait for it. + self._dev.waitForRead(self.timeout) + if self.numCells and self._model: + break + + if self.numCells: + # A display responded. + self._model.postInit() + log.info("Found {device} connected via {type} ({port})".format( + device=self._model.name, type=portType, port=port)) + break + self._dev.close() + + else: + wx.CallAfter(self.destroy_message_window) + raise RuntimeError("No Handy Tech display found") + + def create_message_window(self): + try: + self._messageWindow = InvisibleDriverWindow(self) + except: + log.debugWarning("", exc_info=True) + def destroy_message_window(self): + try: + self._messageWindow.destroy() + except: + log.debugWarning("", exc_info=True) + + def go_to_sleep(self): + self._sleepcounter += 1 + if self._dev is not None: + self._dev.close() + self._dev = None + time.sleep(self.timeout) + def wake_up(self): + if self._sleepcounter > 0: + self._sleepcounter -= 1 + if self._sleepcounter > 0: # Still not zero after decrementing + return + # Might throw if device no longer exists. + # We leave it to autodetection to grab it when it reappears. + if self.isHidSerial: + # This is either the standalone HID adapter cable for older displays, + # or an older display with a HID - serial adapter built in + self._dev = hwIo.Hid(self.port, onReceive=self._hidSerialOnReceive) + # Send a flush to open the serial channel + self._dev.write(HT_HID_RPT_InCommand + HT_HID_CMD_FlushBuffers) + elif self.isHid: + self._dev = hwIo.Hid(self.port, onReceive=self._hidOnReceive) + else: + self._dev = hwIo.Serial(self.port, baudrate=BAUD_RATE, parity=PARITY, + timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._serialOnReceive) + def terminate(self): + try: + wx.CallAfter(self.destroy_message_window) + super(BrailleDisplayDriver, self).terminate() + finally: + # We must sleep before closing the connection as not doing this can leave the display in a bad state where it can not be re-initialized. + # This has been observed for Easy Braille displays. + time.sleep(self.timeout) + # Make sure the device gets closed. + self._dev.close() + # We also must sleep after closing, as it sometimes takes some time for the device to disconnect. + # This has been observed for Active Braille displays. + time.sleep(self.timeout) + + def _get_supportedSettings(self): + settings = [ + braille.BrailleDisplayDriver.BrailleInputSetting(), + ] + if self._model: + # Add the per model supported settings to the list. + for cls in self._model.__class__.__mro__: + if hasattr(cls, "supportedSettings"): + settings.extend(cls.supportedSettings) + return settings + + def _get_atc(self): + return self._atc + + def _set_atc(self, state): + if self._atc is state: + return + if isinstance(self._model,AtcMixin): + self.sendExtendedPacket(HT_EXTPKT_SET_ATC_MODE, state) + else: + log.debugWarning("Changing ATC setting for unsupported device %s"%self._model.name) + # Regardless whether this setting is supported or not, we want to safe its state. + self._atc = state + + def _get_dotFirmness(self): + return self._dotFirmness + + def _set_dotFirmness(self, value): + if self._dotFirmness is value: + return + if isinstance(self._model,TimeSyncFirmnessMixin): + self.sendExtendedPacket(HT_EXTPKT_SET_FIRMNESS, value) + else: + log.debugWarning("Changing dot firmness setting for unsupported device %s"%self._model.name) + # Regardless whether this setting is supported or not, we want to safe its state. + self._dotFirmness = value + + def sendPacket(self, packetType, data=""): + if self._dev is None: + return + if type(data) == bool or type(data) == int: + data = chr(data) + if self.isHid: + self._sendHidPacket(packetType+data) + else: + self._dev.write(packetType + data) + + def sendExtendedPacket(self, packetType, data=""): + if self._dev is None: + return + if type(data) == bool or type(data) == int: + data = chr(data) + packet = b"{length}{extType}{data}\x16".format( + extType=packetType, data=data, + length=chr(len(data) + len(packetType)) + ) + if self._model: + packet = self._model.deviceId + packet + self.sendPacket(HT_PKT_EXTENDED, packet) + + def _sendHidPacket(self, packet): + assert self.isHid + maxBlockSize = self._dev._writeSize-3 + # When the packet length exceeds C{writeSize}, the packet is split up into several packets. + # They contain C{HT_HID_RPT_InData}, the length of the data block, + # the data block itself and a terminating null character. + for offset in xrange(0, len(packet), maxBlockSize): + block = packet[offset:offset+maxBlockSize] + hidPacket = HT_HID_RPT_InData + chr(len(block)) + block + b"\x00" + self._dev.write(hidPacket) + + def _handleKeyRelease(self): + if self._ignoreKeyReleases or not self._keysDown: + return + # The first key released executes the key combination. + try: + inputCore.manager.executeGesture( + InputGesture(self._model, self._keysDown, self.brailleInput)) + except inputCore.NoInputGestureAction: + pass + # Any further releases are just the rest of the keys in the combination + # being released, so they should be ignored. + self._ignoreKeyReleases = True + + + # pylint: disable=R0912 + # Pylint complains about many branches, might be worth refactoring + def _hidOnReceive(self, data): + # data contains the entire packet. + stream = StringIO(data) + htPacketType = data[2] + # Skip the header, so reading the stream will only give the rest of the data + stream.seek(3) + self._handleInputStream(htPacketType, stream) + + def _hidSerialOnReceive(self, data): + # The HID serial converter wraps one or two bytes into a single HID packet + hidLength = ord(data[1]) + self._hidSerialBuffer+=data[2:(2+hidLength)] + self._processHidSerialBuffer() + + def _processHidSerialBuffer(self): + while self._hidSerialBuffer: + currentBufferLength=len(self._hidSerialBuffer) + htPacketType = self._hidSerialBuffer[0] + if htPacketType!=HT_PKT_EXTENDED: + packetLength = 2 if htPacketType==HT_PKT_OK else 1 + if currentBufferLength>=packetLength: + stream = StringIO(self._hidSerialBuffer[:packetLength]) + self._hidSerialBuffer = self._hidSerialBuffer[packetLength:] + else: + # The packet is not yet complete + return + elif htPacketType==HT_PKT_EXTENDED and currentBufferLength>=5: + # Check whether our packet is complete + # Extended packets are at least 5 bytes in size. + # The second byte is the model, the third byte is the data length, excluding the terminator + packet_length = ord(self._hidSerialBuffer[2])+4 + if len(self._hidSerialBuffer) Date: Mon, 10 Jun 2019 22:40:17 +0200 Subject: [PATCH 6/9] Cleaned up Handy Tech driver code. --- source/brailleDisplayDrivers/handyTech.py | 45 +++++++++++++++++------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index 98746581739..48af2934495 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -9,6 +9,7 @@ Braille display driver for Handy Tech braille displays. """ +import ui from collections import OrderedDict from cStringIO import StringIO import serial # pylint: disable=E0401 @@ -31,18 +32,29 @@ class InvisibleDriverWindow(windowUtils.CustomWindow): className = u"Handy_Tech_Server" + HT_SLEEP = 100 + HT_INCREMENT = 1 + HT_DECREMENT = 0 def __init__(self, driver): super(InvisibleDriverWindow, self).__init__(u"Handy Tech Server") - self.driver = driver + # Register shared window message. + # Note: There is no corresponding unregister function. + # Still this does no harm if done repeatedly. + self.window_message=windll.user32.RegisterWindowMessageW(u"Handy_Tech_Server") + self.driver = weakref.ref(driver, lambda(r): self.destroy()) + def windowProc(self, hwnd, msg, wParam, lParam): - if msg == window_message: - if wParam == 100 and lParam == 1: - self.driver.go_to_sleep() - elif wParam == 100 and lParam == 0: - self.driver.wake_up() - return 0 + if msg == self.window_message: + if wParam == self.HT_SLEEP and lParam == self.HT_INCREMENT: + d = self.driver() + if d is not None: + d.go_to_sleep() + elif wParam == self.HT_SLEEP and lParam == self.HT_DECREMENT: + d = self.driver() + if d is not None: + d.wake_up() + return 0 # success, bypass default window procedure -window_message=windll.user32.RegisterWindowMessageW(u"Handy_Tech_Server") BAUD_RATE = 19200 PARITY = serial.PARITY_ODD @@ -553,6 +565,7 @@ def getManualPorts(cls): def __init__(self, port="auto"): super(BrailleDisplayDriver, self).__init__() + # Create the message window on the ui thread. wx.CallAfter(self.create_message_window) self.numCells = 0 self._model = None @@ -602,26 +615,31 @@ def __init__(self, port="auto"): self._dev.close() else: + # Make sure this is called on the ui thread wx.CallAfter(self.destroy_message_window) raise RuntimeError("No Handy Tech display found") def create_message_window(self): try: + self._sleepcounter = 0 self._messageWindow = InvisibleDriverWindow(self) - except: + except WindowsError: log.debugWarning("", exc_info=True) def destroy_message_window(self): try: self._messageWindow.destroy() - except: + except WindowsError: log.debugWarning("", exc_info=True) def go_to_sleep(self): self._sleepcounter += 1 if self._dev is not None: + # Must sleep before and after closing to ensure the device can be reconnected. + time.sleep(self.timeout) self._dev.close() self._dev = None - time.sleep(self.timeout) + time.sleep(self.timeout) # + def wake_up(self): if self._sleepcounter > 0: self._sleepcounter -= 1 @@ -640,8 +658,10 @@ def wake_up(self): else: self._dev = hwIo.Serial(self.port, baudrate=BAUD_RATE, parity=PARITY, timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._serialOnReceive) + def terminate(self): try: + # Make sure this is called on the ui thread. wx.CallAfter(self.destroy_message_window) super(BrailleDisplayDriver, self).terminate() finally: @@ -702,7 +722,8 @@ def sendPacket(self, packetType, data=""): self._dev.write(packetType + data) def sendExtendedPacket(self, packetType, data=""): - if self._dev is None: + if self._sleepcounter > 0: + log.debug("Packet discarded as driver was requested to sleep") return if type(data) == bool or type(data) == int: data = chr(data) From 995020617085b1ed432d457c9d3862ad0f5c563e Mon Sep 17 00:00:00 2001 From: FelixGruetzmacher Date: Mon, 10 Jun 2019 23:26:59 +0200 Subject: [PATCH 7/9] Follow-up, getting rid of an inconsistency. --- source/brailleDisplayDrivers/handyTech.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index 48af2934495..f02cc7d4599 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -712,7 +712,7 @@ def _set_dotFirmness(self, value): self._dotFirmness = value def sendPacket(self, packetType, data=""): - if self._dev is None: + if self._sleepcounter > 0: return if type(data) == bool or type(data) == int: data = chr(data) From f0c1f06a44e5ffa32c7a58d7fc6a7426efdd36cf Mon Sep 17 00:00:00 2001 From: FelixGruetzmacher Date: Tue, 11 Jun 2019 08:36:11 +0200 Subject: [PATCH 8/9] Final cosmetic corrections to Handy Tech driver. --- source/brailleDisplayDrivers/handyTech.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index f02cc7d4599..96d23c2111f 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -625,6 +625,7 @@ def create_message_window(self): self._messageWindow = InvisibleDriverWindow(self) except WindowsError: log.debugWarning("", exc_info=True) + def destroy_message_window(self): try: self._messageWindow.destroy() @@ -638,7 +639,7 @@ def go_to_sleep(self): time.sleep(self.timeout) self._dev.close() self._dev = None - time.sleep(self.timeout) # + time.sleep(self.timeout) def wake_up(self): if self._sleepcounter > 0: From 7766a772d26eba49bd4f9eadc30527c225bdfbbe Mon Sep 17 00:00:00 2001 From: Felix Gruetzmacher Date: Tue, 11 Jun 2019 09:41:14 +0200 Subject: [PATCH 9/9] Removed unnecessary import. --- source/brailleDisplayDrivers/handyTech.py | 1 - 1 file changed, 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index 96d23c2111f..269bbe3cddc 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -9,7 +9,6 @@ Braille display driver for Handy Tech braille displays. """ -import ui from collections import OrderedDict from cStringIO import StringIO import serial # pylint: disable=E0401