• R/O
  • HTTP
  • SSH
  • HTTPS

nvdajp: Commit

NVDA with Japanese branch


Commit MetaInfo

Revision284e76d1191c9ad15b0e8588a35f7b4c1300766e (tree)
Time2016-01-06 14:34:26
AuthorTakuya Nishimoto <nishimotz@gmai...>
CommiterTakuya Nishimoto

Log Message

Merge commit 'fetch_head' into jpbeta

Change Summary

Incremental Difference

--- a/source/IAccessibleHandler.py
+++ b/source/IAccessibleHandler.py
@@ -785,7 +785,7 @@ def processDestroyWinEvent(window,objectID,childID):
785785 focus=api.getFocusObject()
786786 from NVDAObjects.IAccessible.mscandui import BaseCandidateItem
787787 if objectID==0 and childID==0 and isinstance(focus,BaseCandidateItem) and window==focus.windowHandle and not eventHandler.isPendingEvents("gainFocus"):
788- obj=focus.parent
788+ obj=focus.container
789789 if obj:
790790 eventHandler.queueEvent("gainFocus",obj)
791791
--- a/source/braille.py
+++ b/source/braille.py
@@ -2,13 +2,17 @@
22 #A part of NonVisual Desktop Access (NVDA)
33 #This file is covered by the GNU General Public License.
44 #See the file COPYING for more details.
5-#Copyright (C) 2008-2014 NV Access Limited
5+#Copyright (C) 2008-2015 NV Access Limited
66
7+import sys
78 import itertools
89 import os
910 import pkgutil
11+import ctypes.wintypes
12+import threading
1013 import wx
1114 import louis
15+import winKernel
1216 import keyboardHandler
1317 import baseObject
1418 import config
@@ -1489,6 +1493,7 @@ class BrailleHandler(baseObject.AutoPropertyObject):
14891493 if self.display:
14901494 self.display.terminate()
14911495 self.display = None
1496+ _BgThread.stop()
14921497
14931498 def _get_tether(self):
14941499 return config.conf["braille"]["tetherTo"]
@@ -1526,6 +1531,9 @@ class BrailleHandler(baseObject.AutoPropertyObject):
15261531 newDisplay = self.display
15271532 newDisplay.__init__(**kwargs)
15281533 else:
1534+ if newDisplay.isThreadSafe:
1535+ # Start the thread if it wasn't already.
1536+ _BgThread.start()
15291537 newDisplay = newDisplay(**kwargs)
15301538 if self.display:
15311539 try:
@@ -1557,13 +1565,28 @@ class BrailleHandler(baseObject.AutoPropertyObject):
15571565 self._cursorBlinkTimer = wx.PyTimer(self._blink)
15581566 self._cursorBlinkTimer.Start(blinkRate)
15591567
1568+ def _writeCells(self, cells):
1569+ if not self.display.isThreadSafe:
1570+ self.display.display(cells)
1571+ return
1572+ with _BgThread.queuedWriteLock:
1573+ alreadyQueued = _BgThread.queuedWrite
1574+ _BgThread.queuedWrite = cells
1575+ # If a write was already queued, we don't need to queue another;
1576+ # we just replace the data.
1577+ # This means that if multiple writes occur while an earlier write is still in progress,
1578+ # we skip all but the last.
1579+ if not alreadyQueued:
1580+ # Queue a call to the background thread.
1581+ _BgThread.queueApc(_BgThread.executor)
1582+
15601583 def _displayWithCursor(self):
15611584 if not self._cells:
15621585 return
15631586 cells = list(self._cells)
15641587 if self._cursorPos is not None and self._cursorBlinkUp:
15651588 cells[self._cursorPos] |= config.conf["braille"]["cursorShape"]
1566- self.display.display(cells)
1589+ self._writeCells(cells)
15671590
15681591 def _blink(self):
15691592 self._cursorBlinkUp = not self._cursorBlinkUp
@@ -1737,6 +1760,60 @@ class BrailleHandler(baseObject.AutoPropertyObject):
17371760 if display != self.display.name:
17381761 self.setDisplayByName(display)
17391762
1763+class _BgThread:
1764+ """A singleton background thread used for background writes and raw braille display I/O.
1765+ """
1766+
1767+ thread = None
1768+ exit = False
1769+ queuedWrite = None
1770+
1771+ @classmethod
1772+ def start(cls):
1773+ if cls.thread:
1774+ return
1775+ cls.queuedWriteLock = threading.Lock()
1776+ thread = cls.thread = threading.Thread(target=cls.func)
1777+ thread.daemon = True
1778+ thread.start()
1779+ cls.handle = ctypes.windll.kernel32.OpenThread(winKernel.THREAD_SET_CONTEXT, False, thread.ident)
1780+
1781+ @classmethod
1782+ def queueApc(cls, func):
1783+ ctypes.windll.kernel32.QueueUserAPC(func, cls.handle, 0)
1784+
1785+ @classmethod
1786+ def stop(cls):
1787+ if not cls.thread:
1788+ return
1789+ cls.exit = True
1790+ # Wake up the thread. It will exit when it sees exit is True.
1791+ cls.queueApc(cls.executor)
1792+ cls.thread.join()
1793+ cls.exit = False
1794+ winKernel.closeHandle(cls.handle)
1795+ cls.handle = None
1796+ cls.thread = None
1797+
1798+ @winKernel.PAPCFUNC
1799+ def executor(param):
1800+ if _BgThread.exit:
1801+ # func will see this and exit.
1802+ return
1803+ with _BgThread.queuedWriteLock:
1804+ data = _BgThread.queuedWrite
1805+ _BgThread.queuedWrite = None
1806+ if not data:
1807+ return
1808+ handler.display.display(data)
1809+
1810+ @classmethod
1811+ def func(cls):
1812+ while True:
1813+ ctypes.windll.kernel32.SleepEx(winKernel.INFINITE, True)
1814+ if cls.exit:
1815+ break
1816+
17401817 def initialize():
17411818 global handler
17421819 config.addConfigDirsToPythonPackagePath(brailleDisplayDrivers)
@@ -1773,6 +1850,8 @@ class BrailleDisplayDriver(baseObject.AutoPropertyObject):
17731850 They should subclass L{BrailleDisplayGesture} and execute instances of those gestures using L{inputCore.manager.executeGesture}.
17741851 These gestures can be mapped in L{gestureMap}.
17751852 A driver can also inherit L{baseObject.ScriptableObject} to provide display specific scripts.
1853+
1854+ @see: L{hwIo} for raw serial and HID I/O.
17761855 """
17771856 #: The name of the braille display; must be the original module file name.
17781857 #: @type: str
@@ -1780,6 +1859,14 @@ class BrailleDisplayDriver(baseObject.AutoPropertyObject):
17801859 #: A description of the braille display.
17811860 #: @type: str
17821861 description = ""
1862+ #: Whether this driver is thread-safe.
1863+ #: If it is, NVDA may initialize, terminate or call this driver on any thread.
1864+ #: This allows NVDA to read from and write to the display in the background,
1865+ #: which means the rest of NVDA is not blocked while this occurs,
1866+ #: thus resulting in better performance.
1867+ #: This is also required to use the L{hwIo} module.
1868+ #: @type: bool
1869+ isThreadSafe = False
17831870
17841871 @classmethod
17851872 def check(cls):
--- a/source/brailleDisplayDrivers/baum.py
+++ b/source/brailleDisplayDrivers/baum.py
@@ -7,22 +7,22 @@
77
88 import time
99 from collections import OrderedDict
10-import wx
11-import serial
10+from cStringIO import StringIO
1211 import hwPortUtils
1312 import braille
1413 import inputCore
1514 from logHandler import log
1615 import brailleInput
16+import hwIo
1717
1818 TIMEOUT = 0.2
1919 BAUD_RATE = 19200
20-READ_INTERVAL = 50
2120
2221 ESCAPE = "\x1b"
2322
2423 BAUM_DISPLAY_DATA = "\x01"
2524 BAUM_CELL_COUNT = "\x01"
25+BAUM_REQUEST_INFO = "\x02"
2626 BAUM_PROTOCOL_ONOFF = "\x15"
2727 BAUM_COMMUNICATION_CHANNEL = "\x16"
2828 BAUM_POWERDOWN = "\x17"
@@ -55,7 +55,7 @@ KEY_NAMES = {
5555 BAUM_JOYSTICK_KEYS: ("up", "left", "down", "right", "select"),
5656 }
5757
58-USB_IDS = frozenset((
58+USB_IDS_SER = {
5959 "VID_0403&PID_FE70", # Vario 40
6060 "VID_0403&PID_FE71", # PocketVario
6161 "VID_0403&PID_FE72", # SuperVario/Brailliant 40
@@ -76,7 +76,18 @@ USB_IDS = frozenset((
7676 "VID_0904&PID_2015", # EcoVario 64
7777 "VID_0904&PID_2016", # EcoVario 80
7878 "VID_0904&PID_3000", # RefreshaBraille 18
79-))
79+}
80+
81+USB_IDS_HID = {
82+ "VID_0904&PID_3001", # RefreshaBraille 18
83+ "VID_0904&PID_6101", # VarioUltra 20
84+ "VID_0904&PID_6103", # VarioUltra 32
85+ "VID_0904&PID_6102", # VarioUltra 40
86+ "VID_0904&PID_4004", # Pronto! 18 V3
87+ "VID_0904&PID_4005", # Pronto! 40 V3
88+ "VID_0904&PID_4007", # Pronto! 18 V4
89+ "VID_0904&PID_4008", # Pronto! 40 V4
90+}
8091
8192 BLUETOOTH_NAMES = (
8293 "Baum SuperVario",
@@ -94,6 +105,7 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
94105 name = "baum"
95106 # Translators: Names of braille displays.
96107 description = _("Baum/HumanWare/APH braille displays")
108+ isThreadSafe = True
97109
98110 @classmethod
99111 def check(cls):
@@ -115,18 +127,21 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
115127
116128 @classmethod
117129 def _getAutoPorts(cls, comPorts):
130+ for portInfo in hwPortUtils.listHidDevices():
131+ if portInfo.get("usbID") in USB_IDS_HID:
132+ yield portInfo["devicePath"], "USB HID"
118133 # Try bluetooth ports last.
119134 for portInfo in sorted(comPorts, key=lambda item: "bluetoothName" in item):
120135 port = portInfo["port"]
121136 hwID = portInfo["hardwareID"]
122137 if hwID.startswith(r"FTDIBUS\COMPORT"):
123138 # USB.
124- portType = "USB"
139+ portType = "USB serial"
125140 try:
126141 usbID = hwID.split("&", 1)[1]
127142 except IndexError:
128143 continue
129- if usbID not in USB_IDS:
144+ if usbID not in USB_IDS_SER:
130145 continue
131146 elif "bluetoothName" in portInfo:
132147 # Bluetooth.
@@ -150,92 +165,90 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
150165 for port, portType in tryPorts:
151166 # At this point, a port bound to this display has been found.
152167 # Try talking to the display.
168+ self.isHid = portType == "USB HID"
153169 try:
154- self._ser = serial.Serial(port, baudrate=BAUD_RATE, timeout=TIMEOUT, writeTimeout=TIMEOUT)
155- except serial.SerialException:
170+ if self.isHid:
171+ self._dev = hwIo.Hid(port, onReceive=self._onReceive)
172+ else:
173+ self._dev = hwIo.Serial(port, baudrate=BAUD_RATE, timeout=TIMEOUT, writeTimeout=TIMEOUT, onReceive=self._onReceive)
174+ except EnvironmentError:
156175 continue
157- # If the protocol is already on, sending protocol on won't return anything.
158- # First ensure it's off.
159- self._sendRequest(BAUM_PROTOCOL_ONOFF, False)
160- # This will cause the device id, serial number and number of cells to be returned.
161- self._sendRequest(BAUM_PROTOCOL_ONOFF, True)
162- # Send again in case the display misses the first one.
163- self._sendRequest(BAUM_PROTOCOL_ONOFF, True)
164- self._handleResponses(wait=True)
165- if not self.numCells or not self._deviceID:
176+ if self.isHid:
177+ # Some displays don't support BAUM_PROTOCOL_ONOFF.
178+ self._sendRequest(BAUM_REQUEST_INFO, 0)
179+ else:
180+ # If the protocol is already on, sending protocol on won't return anything.
181+ # First ensure it's off.
182+ self._sendRequest(BAUM_PROTOCOL_ONOFF, False)
183+ # This will cause the device id, serial number and number of cells to be returned.
184+ self._sendRequest(BAUM_PROTOCOL_ONOFF, True)
185+ # Send again in case the display misses the first one.
186+ self._sendRequest(BAUM_PROTOCOL_ONOFF, True)
187+ for i in xrange(3):
166188 # An expected response hasn't arrived yet, so wait for it.
167- self._handleResponses(wait=True)
189+ self._dev.waitForRead(TIMEOUT)
190+ if self.numCells and self._deviceID:
191+ break
168192 if self.numCells:
169193 # A display responded.
170194 log.info("Found {device} connected via {type} ({port})".format(
171195 device=self._deviceID, type=portType, port=port))
172196 break
197+ self._dev.close()
173198
174199 else:
175200 raise RuntimeError("No Baum display found")
176201
177- self._readTimer = wx.PyTimer(self._handleResponses)
178- self._readTimer.Start(READ_INTERVAL)
179202 self._keysDown = {}
180203 self._ignoreKeyReleases = False
181204
182205 def terminate(self):
183206 try:
184207 super(BrailleDisplayDriver, self).terminate()
185- self._readTimer.Stop()
186- self._readTimer = None
187- self._sendRequest(BAUM_PROTOCOL_ONOFF, False)
208+ try:
209+ self._sendRequest(BAUM_PROTOCOL_ONOFF, False)
210+ except EnvironmentError:
211+ # Some displays don't support BAUM_PROTOCOL_ONOFF.
212+ pass
188213 finally:
189- # We absolutely must close the Serial object, as it does not have a destructor.
190- # If we don't, we won't be able to re-open it later.
191- self._ser.close()
214+ # Make sure the device gets closed.
215+ # If it doesn't, we may not be able to re-open it later.
216+ self._dev.close()
192217
193218 def _sendRequest(self, command, arg=""):
194219 if isinstance(arg, (int, bool)):
195220 arg = chr(arg)
196- self._ser.write("\x1b{command}{arg}".format(command=command,
197- arg=arg.replace(ESCAPE, ESCAPE * 2)))
198-
199- def _handleResponses(self, wait=False):
200- while wait or self._ser.inWaiting():
201- command, arg = self._readPacket()
202- if command:
203- self._handleResponse(command, arg)
204- wait = False
205-
206- def _readPacket(self):
207- # Find the escape.
208- chars = []
209- escapeFound = False
210- while True:
211- char = self._ser.read(1)
212- if char == ESCAPE:
213- escapeFound = True
214- break
215- else:
216- chars.append(char)
217- if not self._ser.inWaiting():
218- break
219- if chars:
220- log.debugWarning("Ignoring data before escape: %r" % "".join(chars))
221- if not escapeFound:
222- return None, None
221+ if self.isHid:
222+ self._dev.write(command + arg)
223+ else:
224+ self._dev.write("\x1b{command}{arg}".format(command=command,
225+ arg=arg.replace(ESCAPE, ESCAPE * 2)))
223226
224- command = self._ser.read(1)
227+ def _onReceive(self, data):
228+ if self.isHid:
229+ # data contains the entire packet.
230+ stream = StringIO(data)
231+ else:
232+ if data != ESCAPE:
233+ log.debugWarning("Ignoring byte before escape: %r" % data)
234+ return
235+ # data only contained the escape. Read the rest from the device.
236+ stream = self._dev
237+ command = stream.read(1)
225238 length = BAUM_RSP_LENGTHS.get(command, 0)
226239 if command == BAUM_ROUTING_KEYS:
227240 length = 10 if self.numCells > 40 else 5
228- arg = self._ser.read(length)
229- return command, arg
241+ arg = stream.read(length)
242+ if command == BAUM_DEVICE_ID and arg == "Refreshabraille ":
243+ # For most Baum devices, the argument is 16 bytes,
244+ # but it is 18 bytes for the Refreshabraille.
245+ arg += stream.read(2)
246+ self._handleResponse(command, arg)
230247
231248 def _handleResponse(self, command, arg):
232249 if command == BAUM_CELL_COUNT:
233250 self.numCells = ord(arg)
234251 elif command == BAUM_DEVICE_ID:
235- if arg == "Refreshabraille ":
236- # For most Baum devices, the argument is 16 bytes,
237- # but it is 18 bytes for the Refreshabraille.
238- arg += self._ser.read(2)
239252 # Short ids can be padded with either nulls or spaces.
240253 self._deviceID = arg.rstrip("\0 ")
241254 elif command in KEY_NAMES:
--- a/source/brailleDisplayDrivers/brailliantB.py
+++ b/source/brailleDisplayDrivers/brailliantB.py
@@ -5,22 +5,21 @@
55 #Copyright (C) 2012-2015 NV Access Limited
66
77 import os
8-import time
98 import _winreg
109 import itertools
11-import wx
1210 import serial
1311 import hwPortUtils
1412 import braille
1513 import inputCore
1614 from logHandler import log
1715 import brailleInput
16+import hwIo
1817
1918 TIMEOUT = 0.2
2019 BAUD_RATE = 115200
2120 PARITY = serial.PARITY_EVEN
22-READ_INTERVAL = 50
2321
22+# Serial
2423 HEADER = "\x1b"
2524 MSG_INIT = "\x00"
2625 MSG_INIT_RESP = "\x01"
@@ -28,6 +27,12 @@ MSG_DISPLAY = "\x02"
2827 MSG_KEY_DOWN = "\x05"
2928 MSG_KEY_UP = "\x06"
3029
30+# HID
31+HR_CAPS = "\x01"
32+HR_KEYS = "\x04"
33+HR_BRAILLE = "\x05"
34+HR_POWEROFF = "\x07"
35+
3136 KEY_NAMES = {
3237 # Braille keyboard.
3338 2: "dot1",
@@ -58,7 +63,12 @@ DOT8_KEY = 9
5863 SPACE_KEY = 10
5964
6065 def _getPorts():
61- # USB.
66+ # USB HID.
67+ for portInfo in hwPortUtils.listHidDevices():
68+ if portInfo.get("usbID") == "VID_1C71&PID_C006":
69+ yield "USB HID", portInfo["devicePath"]
70+
71+ # USB serial.
6272 try:
6373 rootKey = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Enum\USB\Vid_1c71&Pid_c005")
6474 except WindowsError:
@@ -73,7 +83,7 @@ def _getPorts():
7383 break
7484 try:
7585 with _winreg.OpenKey(rootKey, os.path.join(keyName, "Device Parameters")) as paramsKey:
76- yield "USB", _winreg.QueryValueEx(paramsKey, "PortName")[0]
86+ yield "USB serial", _winreg.QueryValueEx(paramsKey, "PortName")[0]
7787 except WindowsError:
7888 continue
7989
@@ -90,6 +100,7 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
90100 name = "brailliantB"
91101 # Translators: The name of a series of braille displays.
92102 description = _("HumanWare Brailliant BI/B series")
103+ isThreadSafe = True
93104
94105 @classmethod
95106 def check(cls):
@@ -105,77 +116,71 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
105116 self.numCells = 0
106117
107118 for portType, port in _getPorts():
119+ self.isHid = portType == "USB HID"
108120 # Try talking to the display.
109121 try:
110- self._ser = serial.Serial(port, baudrate=BAUD_RATE, parity=PARITY, timeout=TIMEOUT, writeTimeout=TIMEOUT)
111- except serial.SerialException:
122+ if self.isHid:
123+ self._dev = hwIo.Hid(port, onReceive=self._hidOnReceive)
124+ else:
125+ self._dev = hwIo.Serial(port, baudrate=BAUD_RATE, parity=PARITY, timeout=TIMEOUT, writeTimeout=TIMEOUT, onReceive=self._serOnReceive)
126+ except EnvironmentError:
112127 continue
113- # This will cause the number of cells to be returned.
114- self._sendMessage(MSG_INIT)
115- # #5406: With the new USB driver, the first command is ignored after a reconnection.
116- # Worse, if we don't receive a reply,
117- # _handleResponses freezes for some reason despite the timeout.
118- # Send the init message again just in case.
119- self._sendMessage(MSG_INIT)
120- self._handleResponses(wait=True)
121- if not self.numCells:
122- # HACK: When connected via bluetooth, the display sometimes reports communication not allowed on the first attempt.
123- self._sendMessage(MSG_INIT)
124- self._handleResponses(wait=True)
128+ if self.isHid:
129+ data = self._dev.getFeature(HR_CAPS)
130+ self.numCells = ord(data[24])
131+ else:
132+ # This will cause the number of cells to be returned.
133+ self._serSendMessage(MSG_INIT)
134+ # #5406: With the new USB driver, the first command is ignored after a reconnection.
135+ # Send the init message again just in case.
136+ self._serSendMessage(MSG_INIT)
137+ self._dev.waitForRead(TIMEOUT)
138+ if not self.numCells:
139+ # HACK: When connected via bluetooth, the display sometimes reports communication not allowed on the first attempt.
140+ self._serSendMessage(MSG_INIT)
141+ self._dev.waitForRead(TIMEOUT)
125142 if self.numCells:
126143 # A display responded.
127144 log.info("Found display with {cells} cells connected via {type} ({port})".format(
128145 cells=self.numCells, type=portType, port=port))
129146 break
147+ self._dev.close()
130148
131149 else:
132150 raise RuntimeError("No display found")
133151
134- self._readTimer = wx.PyTimer(self._handleResponses)
135- self._readTimer.Start(READ_INTERVAL)
136152 self._keysDown = set()
137153 self._ignoreKeyReleases = False
138154
139155 def terminate(self):
140156 try:
141157 super(BrailleDisplayDriver, self).terminate()
142- self._readTimer.Stop()
143- self._readTimer = None
144158 finally:
145- # We absolutely must close the Serial object, as it does not have a destructor.
146- # If we don't, we won't be able to re-open it later.
147- self._ser.close()
159+ # Make sure the device gets closed.
160+ # If it doesn't, we may not be able to re-open it later.
161+ self._dev.close()
148162
149- def _sendMessage(self, msgId, payload=""):
163+ def _serSendMessage(self, msgId, payload=""):
150164 if isinstance(payload, (int, bool)):
151165 payload = chr(payload)
152- self._ser.write("{header}{id}{length}{payload}".format(
166+ self._dev.write("{header}{id}{length}{payload}".format(
153167 header=HEADER, id=msgId,
154168 length=chr(len(payload)), payload=payload))
155169
156- def _handleResponses(self, wait=False):
157- while wait or self._ser.inWaiting():
158- msgId, payload = self._readPacket()
159- if msgId:
160- self._handleResponse(msgId, payload)
161- wait = False
170+ def _serOnReceive(self, data):
171+ if data != HEADER:
172+ log.debugWarning("Ignoring byte before header: %r" % data)
173+ return
174+ msgId = self._dev.read(1)
175+ length = ord(self._dev.read(1))
176+ payload = self._dev.read(length)
177+ self._serHandleResponse(msgId, payload)
162178
163- def _readPacket(self):
164- # Wait for the header.
165- while True:
166- char = self._ser.read(1)
167- if char == HEADER:
168- break
169- msgId = self._ser.read(1)
170- length = ord(self._ser.read(1))
171- payload = self._ser.read(length)
172- return msgId, payload
173-
174- def _handleResponse(self, msgId, payload):
179+ def _serHandleResponse(self, msgId, payload):
175180 if msgId == MSG_INIT_RESP:
176181 if ord(payload[0]) != 0:
177182 # Communication not allowed.
178- log.debugWarning("Display at %r reports communication not allowed" % self._ser.port)
183+ log.debugWarning("Display at %r reports communication not allowed" % self._dev.port)
179184 return
180185 self.numCells = ord(payload[2])
181186
@@ -187,22 +192,50 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
187192
188193 elif msgId == MSG_KEY_UP:
189194 payload = ord(payload)
190- if not self._ignoreKeyReleases and self._keysDown:
191- try:
192- inputCore.manager.executeGesture(InputGesture(self._keysDown))
193- except inputCore.NoInputGestureAction:
194- pass
195- # Any further releases are just the rest of the keys in the combination being released,
196- # so they should be ignored.
197- self._ignoreKeyReleases = True
195+ self._handleKeyRelease()
198196 self._keysDown.discard(payload)
199197
200198 else:
201199 log.debugWarning("Unknown message: id {id!r}, payload {payload!r}".format(id=msgId, payload=payload))
202200
201+ def _hidOnReceive(self, data):
202+ rId = data[0]
203+ if rId == HR_KEYS:
204+ keys = data[1:].split("\0", 1)[0]
205+ keys = {ord(key) for key in keys}
206+ if len(keys) > len(self._keysDown):
207+ # Press. This begins a new key combination.
208+ self._ignoreKeyReleases = False
209+ elif len(keys) < len(self._keysDown):
210+ self._handleKeyRelease()
211+ self._keysDown = keys
212+
213+ elif rId == HR_POWEROFF:
214+ log.debug("Powering off")
215+ else:
216+ log.debugWarning("Unknown report: %r" % data)
217+
218+ def _handleKeyRelease(self):
219+ if self._ignoreKeyReleases or not self._keysDown:
220+ return
221+ try:
222+ inputCore.manager.executeGesture(InputGesture(self._keysDown))
223+ except inputCore.NoInputGestureAction:
224+ pass
225+ # Any further releases are just the rest of the keys in the combination being released,
226+ # so they should be ignored.
227+ self._ignoreKeyReleases = True
228+
203229 def display(self, cells):
204230 # cells will already be padded up to numCells.
205- self._sendMessage(MSG_DISPLAY, "".join(chr(cell) for cell in cells))
231+ cells = "".join(chr(cell) for cell in cells)
232+ if self.isHid:
233+ self._dev.write("{id}"
234+ "\x01\x00" # Module 1, offset 0
235+ "{length}{cells}"
236+ .format(id=HR_BRAILLE, length=chr(self.numCells), cells=cells))
237+ else:
238+ self._serSendMessage(MSG_DISPLAY, cells)
206239
207240 gestureMap = inputCore.GlobalGestureMap({
208241 "globalCommands.GlobalCommands": {
--- a/source/config/__init__.py
+++ b/source/config/__init__.py
@@ -232,6 +232,9 @@ confspec = ConfigObj(StringIO(
232232 reportReadingStringChanges = boolean(default=True)
233233 reportCompositionStringChanges = boolean(default=True)
234234
235+[debugLog]
236+ hwIo = boolean(default=false)
237+
235238 [upgrade]
236239 newLaptopKeyboardLayout = boolean(default=false)
237240 """
--- /dev/null
+++ b/source/hwIo.py
@@ -0,0 +1,277 @@
1+#hwIo.py
2+#A part of NonVisual Desktop Access (NVDA)
3+#This file is covered by the GNU General Public License.
4+#See the file COPYING for more details.
5+#Copyright (C) 2015 NV Access Limited
6+
7+"""Raw input/output for braille displays via serial and HID.
8+See the L{Serial} and L{Hid} classes.
9+Braille display drivers must be thread-safe to use this, as it utilises a background thread.
10+See L{braille.BrailleDisplayDriver.isThreadSafe}.
11+"""
12+
13+import threading
14+import ctypes
15+from ctypes import byref
16+from ctypes.wintypes import DWORD, USHORT
17+import serial
18+from serial.win32 import OVERLAPPED, FILE_FLAG_OVERLAPPED, INVALID_HANDLE_VALUE, ERROR_IO_PENDING, CreateFile
19+import winKernel
20+import braille
21+from logHandler import log
22+import config
23+
24+LPOVERLAPPED_COMPLETION_ROUTINE = ctypes.WINFUNCTYPE(None, DWORD, DWORD, serial.win32.LPOVERLAPPED)
25+ERROR_OPERATION_ABORTED = 995
26+
27+def _isDebug():
28+ return config.conf["debugLog"]["hwIo"]
29+
30+class IoBase(object):
31+ """Base class for raw I/O.
32+ This watches for data of a specified size and calls a callback when it is received.
33+ """
34+
35+ def __init__(self, fileHandle, onReceive, onReceiveSize=1, writeSize=None):
36+ """Constructr.
37+ @param fileHandle: A handle to an open I/O device opened for overlapped I/O.
38+ @param onReceive: A callable taking the received data as its only argument.
39+ @type onReceive: callable(str)
40+ @param onReceiveSize: The size (in bytes) of the data with which to call C{onReceive}.
41+ @type onReceiveSize: int
42+ @param writeSize: The size of the buffer for writes,
43+ C{None} to use the length of the data written.
44+ @param writeSize: int or None
45+ """
46+ self._file = fileHandle
47+ self._onReceive = onReceive
48+ self._readSize = onReceiveSize
49+ self._writeSize = writeSize
50+ self._readBuf = ctypes.create_string_buffer(onReceiveSize)
51+ self._readOl = OVERLAPPED()
52+ self._recvEvt = threading.Event()
53+ self._ioDoneInst = LPOVERLAPPED_COMPLETION_ROUTINE(self._ioDone)
54+ self._writeOl = OVERLAPPED()
55+ # Do the initial read.
56+ @winKernel.PAPCFUNC
57+ def init(param):
58+ self._initApc = None
59+ self._asyncRead()
60+ # Ensure the APC stays alive until it runs.
61+ self._initApc = init
62+ braille._BgThread.queueApc(init)
63+
64+ def waitForRead(self, timeout):
65+ """Wait for a chunk of data to be received and processed.
66+ This will return after L{onReceive} has been called or when the timeout elapses.
67+ @param timeout: The maximum time to wait in seconds.
68+ @type timeout: int or float
69+ @return: C{True} if received data was processed before the timeout,
70+ C{False} if not.
71+ @rtype: bool
72+ """
73+ if not self._recvEvt.wait(timeout):
74+ if _isDebug():
75+ log.debug("Wait timed out")
76+ return False
77+ self._recvEvt.clear()
78+ return True
79+
80+ def write(self, data):
81+ if _isDebug():
82+ log.debug("Write: %r" % data)
83+ size = self._writeSize or len(data)
84+ buf = ctypes.create_string_buffer(size)
85+ buf.raw = data
86+ if not ctypes.windll.kernel32.WriteFile(self._file, data, size, None, byref(self._writeOl)):
87+ if ctypes.GetLastError() != ERROR_IO_PENDING:
88+ if _isDebug():
89+ log.debug("Write failed: %s" % ctypes.WinError())
90+ raise ctypes.WinError()
91+ bytes = DWORD()
92+ ctypes.windll.kernel32.GetOverlappedResult(self._file, byref(self._writeOl), byref(bytes), True)
93+
94+ def close(self):
95+ if _isDebug():
96+ log.debug("Closing")
97+ self._onReceive = None
98+ ctypes.windll.kernel32.CancelIoEx(self._file, byref(self._readOl))
99+
100+ def __del__(self):
101+ self.close()
102+
103+ def _asyncRead(self):
104+ # Wait for _readSize bytes of data.
105+ # _ioDone will call onReceive once it is received.
106+ # onReceive can then optionally read additional bytes if it knows these are coming.
107+ ctypes.windll.kernel32.ReadFileEx(self._file, self._readBuf, self._readSize, byref(self._readOl), self._ioDoneInst)
108+
109+ def _ioDone(self, error, bytes, overlapped):
110+ if not self._onReceive:
111+ # close has been called.
112+ self._ioDone = None
113+ return
114+ elif error != 0:
115+ raise ctypes.WinError(error)
116+ self._notifyReceive(self._readBuf[:bytes])
117+ self._recvEvt.set()
118+ self._asyncRead()
119+
120+ def _notifyReceive(self, data):
121+ """Called when data is received.
122+ The base implementation just calls the onReceive callback provided to the constructor.
123+ This can be extended to perform tasks before/after the callback.
124+ """
125+ if _isDebug():
126+ log.debug("Read: %r" % data)
127+ try:
128+ self._onReceive(data)
129+ except:
130+ log.error("", exc_info=True)
131+
132+class Serial(IoBase):
133+ """Raw I/O for serial devices.
134+ This extends pyserial to call a callback when data is received.
135+ """
136+
137+ def __init__(self, *args, **kwargs):
138+ """Constructor.
139+ Pass the arguments you would normally pass to L{serial.Serial}.
140+ There is also one additional keyword argument.
141+ @param onReceive: A callable taking a byte of received data as its only argument.
142+ This callable can then call C{read} to get additional data if desired.
143+ @type onReceive: callable(str)
144+ """
145+ onReceive = kwargs.pop("onReceive")
146+ self._ser = None
147+ if _isDebug():
148+ port = args[0] if len(args) >= 1 else kwargs["port"]
149+ log.debug("Opening port %s" % port)
150+ try:
151+ self._ser = serial.Serial(*args, **kwargs)
152+ except Exception as e:
153+ if _isDebug():
154+ log.debug("Open failed: %s" % e)
155+ raise
156+ self._origTimeout = self._ser.timeout
157+ # We don't want a timeout while we're waiting for data.
158+ self._ser.timeout = None
159+ self.inWaiting = self._ser.inWaiting
160+ super(Serial, self).__init__(self._ser.hComPort, onReceive)
161+
162+ def read(self, size=1):
163+ data = self._ser.read(size)
164+ if _isDebug():
165+ log.debug("Read: %r" % data)
166+ return data
167+
168+ def write(self, data):
169+ if _isDebug():
170+ log.debug("Write: %r" % data)
171+ self._ser.write(data)
172+
173+ def close(self):
174+ if not self._ser:
175+ return
176+ super(Serial, self).close()
177+ self._ser.close()
178+
179+ def _notifyReceive(self, data):
180+ # Set the timeout for onReceive in case it does a sync read.
181+ self._ser.timeout = self._origTimeout
182+ super(Serial, self)._notifyReceive(data)
183+ self._ser.timeout = None
184+
185+class HIDP_CAPS (ctypes.Structure):
186+ _fields_ = (
187+ ("Usage", USHORT),
188+ ("UsagePage", USHORT),
189+ ("InputReportByteLength", USHORT),
190+ ("OutputReportByteLength", USHORT),
191+ ("FeatureReportByteLength", USHORT),
192+ ("Reserved", USHORT * 17),
193+ ("NumberLinkCollectionNodes", USHORT),
194+ ("NumberInputButtonCaps", USHORT),
195+ ("NumberInputValueCaps", USHORT),
196+ ("NumberInputDataIndices", USHORT),
197+ ("NumberOutputButtonCaps", USHORT),
198+ ("NumberOutputValueCaps", USHORT),
199+ ("NumberOutputDataIndices", USHORT),
200+ ("NumberFeatureButtonCaps", USHORT),
201+ ("NumberFeatureValueCaps", USHORT),
202+ ("NumberFeatureDataIndices", USHORT)
203+ )
204+
205+class Hid(IoBase):
206+ """Raw I/O for HID devices.
207+ """
208+
209+ def __init__(self, path, onReceive):
210+ """Constructor.
211+ @param path: The device path.
212+ This can be retrieved using L{hwPortUtils.listHidDevices}.
213+ @type path: unicode
214+ @param onReceive: A callable taking a received input report as its only argument.
215+ @type onReceive: callable(str)
216+ """
217+ if _isDebug():
218+ log.debug("Opening device %s" % path)
219+ handle = CreateFile(path, winKernel.GENERIC_READ | winKernel.GENERIC_WRITE,
220+ 0, None, winKernel.OPEN_EXISTING, FILE_FLAG_OVERLAPPED, None)
221+ if handle == INVALID_HANDLE_VALUE:
222+ if _isDebug():
223+ log.debug("Open failed: %s" % ctypes.WinError())
224+ raise ctypes.WinError()
225+ pd = ctypes.c_void_p()
226+ if not ctypes.windll.hid.HidD_GetPreparsedData(handle, byref(pd)):
227+ raise ctypes.WinError()
228+ caps = HIDP_CAPS()
229+ ctypes.windll.hid.HidP_GetCaps(pd, byref(caps))
230+ ctypes.windll.hid.HidD_FreePreparsedData(pd)
231+ if _isDebug():
232+ log.debug("Report byte lengths: input %d, output %d, feature %d"
233+ % (caps.InputReportByteLength, caps.OutputReportByteLength,
234+ caps.FeatureReportByteLength))
235+ self._featureSize = caps.FeatureReportByteLength
236+ # Reading any less than caps.InputReportByteLength is an error.
237+ # On Windows 7, writing any less than caps.OutputReportByteLength is also an error.
238+ super(Hid, self).__init__(handle, onReceive,
239+ onReceiveSize=caps.InputReportByteLength,
240+ writeSize=caps.OutputReportByteLength)
241+
242+ def getFeature(self, reportId):
243+ """Get a feature report from this device.
244+ @param reportId: The report id.
245+ @type reportId: str
246+ @return: The report, including the report id.
247+ @rtype: str
248+ """
249+ buf = ctypes.create_string_buffer(self._featureSize)
250+ buf[0] = reportId
251+ if not ctypes.windll.hid.HidD_GetFeature(self._file, buf, self._featureSize):
252+ if _isDebug():
253+ log.debug("Get feature %r failed: %s"
254+ % (reportId, ctypes.WinError()))
255+ raise ctypes.WinError()
256+ if _isDebug():
257+ log.debug("Get feature: %r" % buf.raw)
258+ return buf.raw
259+
260+ def setFeature(self, report):
261+ """Send a feature report to this device.
262+ @param report: The report, including its id.
263+ @type report: str
264+ """
265+ length = len(report)
266+ buf = ctypes.create_string_buffer(length)
267+ buf.raw = report
268+ if _isDebug():
269+ log.debug("Set feature: %r" % report)
270+ if not ctypes.windll.hid.HidD_SetFeature(self._file, buf, length):
271+ if _isDebug():
272+ log.debug("Set feature failed: %s" % ctypes.WinError())
273+ raise ctypes.WinError()
274+
275+ def close(self):
276+ super(Hid, self).close()
277+ winKernel.closeHandle(self._file)
--- a/source/hwPortUtils.py
+++ b/source/hwPortUtils.py
@@ -1,7 +1,7 @@
11 #hwPortUtils.py
22 #A part of NonVisual Desktop Access (NVDA)
3-# Original serial scanner code from http://pyserial.svn.sourceforge.net/viewvc/*checkout*/pyserial/trunk/pyserial/examples/scanwin32.py
4-# Modifications and enhancements by James Teh
3+#Copyright (C) 2001-2015 Chris Liechti, NV Access Limited
4+# Based on serial scanner code by Chris Liechti from https://raw.githubusercontent.com/pyserial/pyserial/81167536e796cc2e13aa16abd17a14634dc3aed1/pyserial/examples/scanwin32.py
55
66 """Utilities for working with hardware connection ports.
77 """
@@ -11,6 +11,8 @@ import ctypes
1111 from ctypes.wintypes import BOOL, WCHAR, HWND, DWORD, ULONG, WORD
1212 import _winreg as winreg
1313 from winKernel import SYSTEMTIME
14+import config
15+from logHandler import log
1416
1517 def ValidHandle(value):
1618 if value == 0:
@@ -93,6 +95,8 @@ SetupDiGetDeviceRegistryProperty.restype = BOOL
9395
9496 GUID_CLASS_COMPORT = GUID(0x86e0d1e0L, 0x8089, 0x11d0,
9597 (ctypes.c_ubyte*8)(0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73))
98+GUID_DEVINTERFACE_USB_DEVICE = GUID(0xA5DCBF10, 0x6530, 0x11D2,
99+ (0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED))
96100
97101 DIGCF_PRESENT = 2
98102 DIGCF_DEVICEINTERFACE = 16
@@ -105,12 +109,15 @@ ERROR_NO_MORE_ITEMS = 259
105109 DICS_FLAG_GLOBAL = 0x00000001
106110 DIREG_DEV = 0x00000001
107111
112+def _isDebug():
113+ return config.conf["debugLog"]["hwIo"]
114+
108115 def listComPorts(onlyAvailable=True):
109116 """List com ports on the system.
110117 @param onlyAvailable: Only return ports that are currently available.
111118 @type onlyAvailable: bool
112119 @return: Generates dicts including keys of port, friendlyName and hardwareID.
113- @rtype: generator of (str, str, str)
120+ @rtype: generator of dict
114121 """
115122 flags = DIGCF_DEVICEINTERFACE
116123 if onlyAvailable:
@@ -221,10 +228,14 @@ def listComPorts(onlyAvailable=True):
221228 else:
222229 entry["friendlyName"] = buf.value
223230
231+ if _isDebug():
232+ log.debug("%r" % entry)
224233 yield entry
225234
226235 finally:
227236 SetupDiDestroyDeviceInfoList(g_hdi)
237+ if _isDebug():
238+ log.debug("Finished listing com ports")
228239
229240 BLUETOOTH_MAX_NAME_SIZE = 248
230241 BTH_ADDR = BLUETOOTH_ADDRESS = ULONGLONG
@@ -295,3 +306,182 @@ def getWidcommBluetoothPortInfo(port):
295306 name = winreg.QueryValueEx(itemKey, "BDName")[0]
296307 return addr, name
297308 raise LookupError
309+
310+def listUsbDevices(onlyAvailable=True):
311+ """List USB devices on the system.
312+ @param onlyAvailable: Only return devices that are currently available.
313+ @type onlyAvailable: bool
314+ @return: The USB vendor and product IDs in the form "VID_xxxx&PID_xxxx"
315+ @rtype: generator of unicode
316+ """
317+ flags = DIGCF_DEVICEINTERFACE
318+ if onlyAvailable:
319+ flags |= DIGCF_PRESENT
320+
321+ buf = ctypes.create_unicode_buffer(1024)
322+ g_hdi = SetupDiGetClassDevs(GUID_DEVINTERFACE_USB_DEVICE, None, NULL, flags)
323+ try:
324+ for dwIndex in xrange(256):
325+ did = SP_DEVICE_INTERFACE_DATA()
326+ did.cbSize = ctypes.sizeof(did)
327+
328+ if not SetupDiEnumDeviceInterfaces(
329+ g_hdi,
330+ None,
331+ GUID_DEVINTERFACE_USB_DEVICE,
332+ dwIndex,
333+ ctypes.byref(did)
334+ ):
335+ if ctypes.GetLastError() != ERROR_NO_MORE_ITEMS:
336+ raise ctypes.WinError()
337+ break
338+
339+ dwNeeded = DWORD()
340+ # get the size
341+ if not SetupDiGetDeviceInterfaceDetail(
342+ g_hdi,
343+ ctypes.byref(did),
344+ None, 0, ctypes.byref(dwNeeded),
345+ None
346+ ):
347+ # Ignore ERROR_INSUFFICIENT_BUFFER
348+ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
349+ raise ctypes.WinError()
350+ # allocate buffer
351+ class SP_DEVICE_INTERFACE_DETAIL_DATA_W(ctypes.Structure):
352+ _fields_ = (
353+ ('cbSize', DWORD),
354+ ('DevicePath', WCHAR*(dwNeeded.value - ctypes.sizeof(DWORD))),
355+ )
356+ def __str__(self):
357+ return "DevicePath:%s" % (self.DevicePath,)
358+ idd = SP_DEVICE_INTERFACE_DETAIL_DATA_W()
359+ idd.cbSize = SIZEOF_SP_DEVICE_INTERFACE_DETAIL_DATA_W
360+ devinfo = SP_DEVINFO_DATA()
361+ devinfo.cbSize = ctypes.sizeof(devinfo)
362+ if not SetupDiGetDeviceInterfaceDetail(
363+ g_hdi,
364+ ctypes.byref(did),
365+ ctypes.byref(idd), dwNeeded, None,
366+ ctypes.byref(devinfo)
367+ ):
368+ raise ctypes.WinError()
369+
370+ # hardware ID
371+ if not SetupDiGetDeviceRegistryProperty(
372+ g_hdi,
373+ ctypes.byref(devinfo),
374+ SPDRP_HARDWAREID,
375+ None,
376+ ctypes.byref(buf), ctypes.sizeof(buf) - 1,
377+ None
378+ ):
379+ # Ignore ERROR_INSUFFICIENT_BUFFER
380+ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
381+ raise ctypes.WinError()
382+ else:
383+ # The string is of the form "usb\VID_xxxx&PID_xxxx&..."
384+ usbId = buf.value[4:21] # VID_xxxx&PID_xxxx
385+ if _isDebug():
386+ log.debug("%r" % usbId)
387+ yield usbId
388+ finally:
389+ SetupDiDestroyDeviceInfoList(g_hdi)
390+ if _isDebug():
391+ log.debug("Finished listing USB devices")
392+
393+_hidGuid = None
394+def listHidDevices(onlyAvailable=True):
395+ """List HID devices on the system.
396+ @param onlyAvailable: Only return devices that are currently available.
397+ @type onlyAvailable: bool
398+ @return: Generates dicts including keys such as hardwareID,
399+ usbID (in the form "VID_xxxx&PID_xxxx")
400+ and devicePath.
401+ @rtype: generator of dict
402+ """
403+ global _hidGuid
404+ if not _hidGuid:
405+ _hidGuid = GUID()
406+ ctypes.windll.hid.HidD_GetHidGuid(ctypes.byref(_hidGuid))
407+
408+ flags = DIGCF_DEVICEINTERFACE
409+ if onlyAvailable:
410+ flags |= DIGCF_PRESENT
411+
412+ buf = ctypes.create_unicode_buffer(1024)
413+ g_hdi = SetupDiGetClassDevs(_hidGuid, None, NULL, flags)
414+ try:
415+ for dwIndex in xrange(256):
416+ did = SP_DEVICE_INTERFACE_DATA()
417+ did.cbSize = ctypes.sizeof(did)
418+
419+ if not SetupDiEnumDeviceInterfaces(
420+ g_hdi,
421+ None,
422+ _hidGuid,
423+ dwIndex,
424+ ctypes.byref(did)
425+ ):
426+ if ctypes.GetLastError() != ERROR_NO_MORE_ITEMS:
427+ raise ctypes.WinError()
428+ break
429+
430+ dwNeeded = DWORD()
431+ # get the size
432+ if not SetupDiGetDeviceInterfaceDetail(
433+ g_hdi,
434+ ctypes.byref(did),
435+ None, 0, ctypes.byref(dwNeeded),
436+ None
437+ ):
438+ # Ignore ERROR_INSUFFICIENT_BUFFER
439+ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
440+ raise ctypes.WinError()
441+ # allocate buffer
442+ class SP_DEVICE_INTERFACE_DETAIL_DATA_W(ctypes.Structure):
443+ _fields_ = (
444+ ('cbSize', DWORD),
445+ ('DevicePath', WCHAR*(dwNeeded.value - ctypes.sizeof(DWORD))),
446+ )
447+ def __str__(self):
448+ return "DevicePath:%s" % (self.DevicePath,)
449+ idd = SP_DEVICE_INTERFACE_DETAIL_DATA_W()
450+ idd.cbSize = SIZEOF_SP_DEVICE_INTERFACE_DETAIL_DATA_W
451+ devinfo = SP_DEVINFO_DATA()
452+ devinfo.cbSize = ctypes.sizeof(devinfo)
453+ if not SetupDiGetDeviceInterfaceDetail(
454+ g_hdi,
455+ ctypes.byref(did),
456+ ctypes.byref(idd), dwNeeded, None,
457+ ctypes.byref(devinfo)
458+ ):
459+ raise ctypes.WinError()
460+
461+ # hardware ID
462+ if not SetupDiGetDeviceRegistryProperty(
463+ g_hdi,
464+ ctypes.byref(devinfo),
465+ SPDRP_HARDWAREID,
466+ None,
467+ ctypes.byref(buf), ctypes.sizeof(buf) - 1,
468+ None
469+ ):
470+ # Ignore ERROR_INSUFFICIENT_BUFFER
471+ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
472+ raise ctypes.WinError()
473+ else:
474+ hwId = buf.value
475+ info = {
476+ "hardwareID": hwId,
477+ "devicePath": idd.DevicePath}
478+ hwId = hwId.split("\\", 1)[1]
479+ if hwId.startswith("VID"):
480+ info["usbID"] = hwId[:17] # VID_xxxx&PID_xxxx
481+ if _isDebug():
482+ log.debug("%r" % info)
483+ yield info
484+ finally:
485+ SetupDiDestroyDeviceInfoList(g_hdi)
486+ if _isDebug():
487+ log.debug("Finished listing HID devices")
--- a/source/winKernel.py
+++ b/source/winKernel.py
@@ -226,3 +226,6 @@ def DuplicateHandle(sourceProcessHandle, sourceHandle, targetProcessHandle, desi
226226 if kernel32.DuplicateHandle(sourceProcessHandle, sourceHandle, targetProcessHandle, byref(targetHandle), desiredAccess, inheritHandle, options) == 0:
227227 raise WinError()
228228 return targetHandle.value
229+
230+PAPCFUNC = ctypes.WINFUNCTYPE(None, ctypes.wintypes.ULONG)
231+THREAD_SET_CONTEXT = 16
--- a/user_docs/en/changes.t2t
+++ b/user_docs/en/changes.t2t
@@ -9,7 +9,9 @@
99 - New braille translation tables: Polish 8 dot computer braille, Mongolian. (#5537, #5574)
1010 - You can turn off the braille cursor and change its shape using the new Show cursor and Cursor shape options in the Braille Settings dialog. (#5198)
1111 - NVDA can now connect to a HIMS Smart Beetle braille display via Bluetooth. (#5607)
12-- NVDA can optionally lower the volume of other sounds when installed on Windows 8 and later. This can be configured using the Audio ducking mode option in the NVDA General Settings dialog or by pressing NVDA+shift+d. (#3830, #5575)
12+- NVDA can optionally lower the volume of other sounds when installed on Windows 8 and later. This can be configured using the Audio ducking mode option in the NVDA General Settings dialog or by pressing NVDA+shift+d. (#3830, #5575)
13+- Support for the APH Refreshabraille in HID mode and the Baum VarioUltra and Pronto! when connected via USB. (#5609)
14+- Support for HumanWare Brailliant BI/B braille displays when the protocol is set to OpenBraille. (#5612)
1315
1416
1517 == Changes ==
@@ -37,12 +39,19 @@
3739 - When a toggle button is focused, NVDA now reports when it is changed from pressed to not pressed. (#5441)
3840 - Reporting of mouse shape changes again works as expected. (#5595)
3941 - When speaking line indentation, non-breaking spaces are now treated as normal spaces. Previously, this could cause announcements such as "space space space" instead of "3 space". (#5610)
42+- When closing a modern Microsoft input method candidate list, focus is correctly restored to either the input composition or the underlying document. (#4145)
4043
4144
4245 == Changes for Developers ==
4346 - The new audioDucking.AudioDucker class allows code which outputs audio to indicate when background audio should be ducked. (#3830)
4447 - nvwave.WavePlayer's constructor now has a wantDucking keyword argument which specifies whether background audio should be ducked while audio is playing. (#3830)
4548 - When this is enabled (which is the default), it is essential that WavePlayer.idle be called when appropriate.
49+- Enhanced I/O for braille displays: (#5609)
50+ - Thread-safe braille display drivers can declare themselves as such using the BrailleDisplayDriver.isThreadSafe attribute. A driver must be thread-safe to benefit from the following features.
51+ - Data is written to thread-safe braille display drivers in the background, thus improving performance.
52+ - hwIo.Serial extends pyserial to call a callable when data is received instead of drivers having to poll.
53+ - hwIo.Hid provides support for braille displays communicating via USB HID.
54+ - hwPortUtils and hwIo can optionally provide detailed debug logging, including devices found and all data sent and received.
4655
4756
4857 = 2015.4 =
--- a/user_docs/en/userGuide.t2t
+++ b/user_docs/en/userGuide.t2t
@@ -1658,14 +1658,13 @@ Please see the display's documentation for descriptions of where these keys can
16581658 ++ Baum/Humanware/APH Braille Displays ++
16591659 Several [Baum http://www.baum.de/cms/en/], [HumanWare http://www.humanware.com/] and [APH http://www.aph.org/] displays are supported when connected via USB or bluetooth.
16601660 These include:
1661-- Baum: SuperVario, PocketVario, VarioUltra (Bluetooth only), Pronto! (Bluetooth only)
1661+- Baum: SuperVario, PocketVario, VarioUltra, Pronto!
16621662 - HumanWare: Brailliant, BrailleConnect
16631663 - APH: Refreshabraille
16641664 -
16651665 Some other displays manufactured by Baum may also work, though this has not been tested.
16661666
1667-If connecting via USB, you must first install the USB drivers provided by the manufacturer.
1668-For the APH Refreshabraille, the USB mode must be set to serial.
1667+If connecting via USB to displays other than the VarioUltra, Pronto! or Refreshabraille with USB mode set to HID, you must first install the USB drivers provided by the manufacturer.
16691668
16701669 Following are the key assignments for this display with NVDA.
16711670 Please see the display's documentation for descriptions of where these keys can be found.
@@ -1722,7 +1721,8 @@ Please see the display's documentation for descriptions of where these keys can
17221721
17231722 ++ HumanWare Brailliant BI/B Series ++
17241723 The Brailliant BI and B series of displays from [HumanWare http://www.humanware.com/], including the BI 32, BI 40 and B 80, are supported when connected via USB or bluetooth.
1725-If connecting via USB, you must first install the USB drivers provided by the manufacturer.
1724+If connecting via USB with the protocol set to HumanWare, you must first install the USB drivers provided by the manufacturer.
1725+USB drivers are not required if the protocol is set to OpenBraille.
17261726
17271727 Following are the key assignments for this display with NVDA.
17281728 Please see the display's documentation for descriptions of where these keys can be found.
Show on old repository browser