NVDA with Japanese branch
Revision | 284e76d1191c9ad15b0e8588a35f7b4c1300766e (tree) |
---|---|
Time | 2016-01-06 14:34:26 |
Author | Takuya Nishimoto <nishimotz@gmai...> |
Commiter | Takuya Nishimoto |
Merge commit 'fetch_head' into jpbeta
@@ -785,7 +785,7 @@ def processDestroyWinEvent(window,objectID,childID): | ||
785 | 785 | focus=api.getFocusObject() |
786 | 786 | from NVDAObjects.IAccessible.mscandui import BaseCandidateItem |
787 | 787 | 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 | |
789 | 789 | if obj: |
790 | 790 | eventHandler.queueEvent("gainFocus",obj) |
791 | 791 |
@@ -2,13 +2,17 @@ | ||
2 | 2 | #A part of NonVisual Desktop Access (NVDA) |
3 | 3 | #This file is covered by the GNU General Public License. |
4 | 4 | #See the file COPYING for more details. |
5 | -#Copyright (C) 2008-2014 NV Access Limited | |
5 | +#Copyright (C) 2008-2015 NV Access Limited | |
6 | 6 | |
7 | +import sys | |
7 | 8 | import itertools |
8 | 9 | import os |
9 | 10 | import pkgutil |
11 | +import ctypes.wintypes | |
12 | +import threading | |
10 | 13 | import wx |
11 | 14 | import louis |
15 | +import winKernel | |
12 | 16 | import keyboardHandler |
13 | 17 | import baseObject |
14 | 18 | import config |
@@ -1489,6 +1493,7 @@ class BrailleHandler(baseObject.AutoPropertyObject): | ||
1489 | 1493 | if self.display: |
1490 | 1494 | self.display.terminate() |
1491 | 1495 | self.display = None |
1496 | + _BgThread.stop() | |
1492 | 1497 | |
1493 | 1498 | def _get_tether(self): |
1494 | 1499 | return config.conf["braille"]["tetherTo"] |
@@ -1526,6 +1531,9 @@ class BrailleHandler(baseObject.AutoPropertyObject): | ||
1526 | 1531 | newDisplay = self.display |
1527 | 1532 | newDisplay.__init__(**kwargs) |
1528 | 1533 | else: |
1534 | + if newDisplay.isThreadSafe: | |
1535 | + # Start the thread if it wasn't already. | |
1536 | + _BgThread.start() | |
1529 | 1537 | newDisplay = newDisplay(**kwargs) |
1530 | 1538 | if self.display: |
1531 | 1539 | try: |
@@ -1557,13 +1565,28 @@ class BrailleHandler(baseObject.AutoPropertyObject): | ||
1557 | 1565 | self._cursorBlinkTimer = wx.PyTimer(self._blink) |
1558 | 1566 | self._cursorBlinkTimer.Start(blinkRate) |
1559 | 1567 | |
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 | + | |
1560 | 1583 | def _displayWithCursor(self): |
1561 | 1584 | if not self._cells: |
1562 | 1585 | return |
1563 | 1586 | cells = list(self._cells) |
1564 | 1587 | if self._cursorPos is not None and self._cursorBlinkUp: |
1565 | 1588 | cells[self._cursorPos] |= config.conf["braille"]["cursorShape"] |
1566 | - self.display.display(cells) | |
1589 | + self._writeCells(cells) | |
1567 | 1590 | |
1568 | 1591 | def _blink(self): |
1569 | 1592 | self._cursorBlinkUp = not self._cursorBlinkUp |
@@ -1737,6 +1760,60 @@ class BrailleHandler(baseObject.AutoPropertyObject): | ||
1737 | 1760 | if display != self.display.name: |
1738 | 1761 | self.setDisplayByName(display) |
1739 | 1762 | |
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 | + | |
1740 | 1817 | def initialize(): |
1741 | 1818 | global handler |
1742 | 1819 | config.addConfigDirsToPythonPackagePath(brailleDisplayDrivers) |
@@ -1773,6 +1850,8 @@ class BrailleDisplayDriver(baseObject.AutoPropertyObject): | ||
1773 | 1850 | They should subclass L{BrailleDisplayGesture} and execute instances of those gestures using L{inputCore.manager.executeGesture}. |
1774 | 1851 | These gestures can be mapped in L{gestureMap}. |
1775 | 1852 | 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. | |
1776 | 1855 | """ |
1777 | 1856 | #: The name of the braille display; must be the original module file name. |
1778 | 1857 | #: @type: str |
@@ -1780,6 +1859,14 @@ class BrailleDisplayDriver(baseObject.AutoPropertyObject): | ||
1780 | 1859 | #: A description of the braille display. |
1781 | 1860 | #: @type: str |
1782 | 1861 | 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 | |
1783 | 1870 | |
1784 | 1871 | @classmethod |
1785 | 1872 | def check(cls): |
@@ -7,22 +7,22 @@ | ||
7 | 7 | |
8 | 8 | import time |
9 | 9 | from collections import OrderedDict |
10 | -import wx | |
11 | -import serial | |
10 | +from cStringIO import StringIO | |
12 | 11 | import hwPortUtils |
13 | 12 | import braille |
14 | 13 | import inputCore |
15 | 14 | from logHandler import log |
16 | 15 | import brailleInput |
16 | +import hwIo | |
17 | 17 | |
18 | 18 | TIMEOUT = 0.2 |
19 | 19 | BAUD_RATE = 19200 |
20 | -READ_INTERVAL = 50 | |
21 | 20 | |
22 | 21 | ESCAPE = "\x1b" |
23 | 22 | |
24 | 23 | BAUM_DISPLAY_DATA = "\x01" |
25 | 24 | BAUM_CELL_COUNT = "\x01" |
25 | +BAUM_REQUEST_INFO = "\x02" | |
26 | 26 | BAUM_PROTOCOL_ONOFF = "\x15" |
27 | 27 | BAUM_COMMUNICATION_CHANNEL = "\x16" |
28 | 28 | BAUM_POWERDOWN = "\x17" |
@@ -55,7 +55,7 @@ KEY_NAMES = { | ||
55 | 55 | BAUM_JOYSTICK_KEYS: ("up", "left", "down", "right", "select"), |
56 | 56 | } |
57 | 57 | |
58 | -USB_IDS = frozenset(( | |
58 | +USB_IDS_SER = { | |
59 | 59 | "VID_0403&PID_FE70", # Vario 40 |
60 | 60 | "VID_0403&PID_FE71", # PocketVario |
61 | 61 | "VID_0403&PID_FE72", # SuperVario/Brailliant 40 |
@@ -76,7 +76,18 @@ USB_IDS = frozenset(( | ||
76 | 76 | "VID_0904&PID_2015", # EcoVario 64 |
77 | 77 | "VID_0904&PID_2016", # EcoVario 80 |
78 | 78 | "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 | +} | |
80 | 91 | |
81 | 92 | BLUETOOTH_NAMES = ( |
82 | 93 | "Baum SuperVario", |
@@ -94,6 +105,7 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): | ||
94 | 105 | name = "baum" |
95 | 106 | # Translators: Names of braille displays. |
96 | 107 | description = _("Baum/HumanWare/APH braille displays") |
108 | + isThreadSafe = True | |
97 | 109 | |
98 | 110 | @classmethod |
99 | 111 | def check(cls): |
@@ -115,18 +127,21 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): | ||
115 | 127 | |
116 | 128 | @classmethod |
117 | 129 | 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" | |
118 | 133 | # Try bluetooth ports last. |
119 | 134 | for portInfo in sorted(comPorts, key=lambda item: "bluetoothName" in item): |
120 | 135 | port = portInfo["port"] |
121 | 136 | hwID = portInfo["hardwareID"] |
122 | 137 | if hwID.startswith(r"FTDIBUS\COMPORT"): |
123 | 138 | # USB. |
124 | - portType = "USB" | |
139 | + portType = "USB serial" | |
125 | 140 | try: |
126 | 141 | usbID = hwID.split("&", 1)[1] |
127 | 142 | except IndexError: |
128 | 143 | continue |
129 | - if usbID not in USB_IDS: | |
144 | + if usbID not in USB_IDS_SER: | |
130 | 145 | continue |
131 | 146 | elif "bluetoothName" in portInfo: |
132 | 147 | # Bluetooth. |
@@ -150,92 +165,90 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): | ||
150 | 165 | for port, portType in tryPorts: |
151 | 166 | # At this point, a port bound to this display has been found. |
152 | 167 | # Try talking to the display. |
168 | + self.isHid = portType == "USB HID" | |
153 | 169 | 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: | |
156 | 175 | 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): | |
166 | 188 | # 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 | |
168 | 192 | if self.numCells: |
169 | 193 | # A display responded. |
170 | 194 | log.info("Found {device} connected via {type} ({port})".format( |
171 | 195 | device=self._deviceID, type=portType, port=port)) |
172 | 196 | break |
197 | + self._dev.close() | |
173 | 198 | |
174 | 199 | else: |
175 | 200 | raise RuntimeError("No Baum display found") |
176 | 201 | |
177 | - self._readTimer = wx.PyTimer(self._handleResponses) | |
178 | - self._readTimer.Start(READ_INTERVAL) | |
179 | 202 | self._keysDown = {} |
180 | 203 | self._ignoreKeyReleases = False |
181 | 204 | |
182 | 205 | def terminate(self): |
183 | 206 | try: |
184 | 207 | 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 | |
188 | 213 | 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() | |
192 | 217 | |
193 | 218 | def _sendRequest(self, command, arg=""): |
194 | 219 | if isinstance(arg, (int, bool)): |
195 | 220 | 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))) | |
223 | 226 | |
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) | |
225 | 238 | length = BAUM_RSP_LENGTHS.get(command, 0) |
226 | 239 | if command == BAUM_ROUTING_KEYS: |
227 | 240 | 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) | |
230 | 247 | |
231 | 248 | def _handleResponse(self, command, arg): |
232 | 249 | if command == BAUM_CELL_COUNT: |
233 | 250 | self.numCells = ord(arg) |
234 | 251 | 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) | |
239 | 252 | # Short ids can be padded with either nulls or spaces. |
240 | 253 | self._deviceID = arg.rstrip("\0 ") |
241 | 254 | elif command in KEY_NAMES: |
@@ -5,22 +5,21 @@ | ||
5 | 5 | #Copyright (C) 2012-2015 NV Access Limited |
6 | 6 | |
7 | 7 | import os |
8 | -import time | |
9 | 8 | import _winreg |
10 | 9 | import itertools |
11 | -import wx | |
12 | 10 | import serial |
13 | 11 | import hwPortUtils |
14 | 12 | import braille |
15 | 13 | import inputCore |
16 | 14 | from logHandler import log |
17 | 15 | import brailleInput |
16 | +import hwIo | |
18 | 17 | |
19 | 18 | TIMEOUT = 0.2 |
20 | 19 | BAUD_RATE = 115200 |
21 | 20 | PARITY = serial.PARITY_EVEN |
22 | -READ_INTERVAL = 50 | |
23 | 21 | |
22 | +# Serial | |
24 | 23 | HEADER = "\x1b" |
25 | 24 | MSG_INIT = "\x00" |
26 | 25 | MSG_INIT_RESP = "\x01" |
@@ -28,6 +27,12 @@ MSG_DISPLAY = "\x02" | ||
28 | 27 | MSG_KEY_DOWN = "\x05" |
29 | 28 | MSG_KEY_UP = "\x06" |
30 | 29 | |
30 | +# HID | |
31 | +HR_CAPS = "\x01" | |
32 | +HR_KEYS = "\x04" | |
33 | +HR_BRAILLE = "\x05" | |
34 | +HR_POWEROFF = "\x07" | |
35 | + | |
31 | 36 | KEY_NAMES = { |
32 | 37 | # Braille keyboard. |
33 | 38 | 2: "dot1", |
@@ -58,7 +63,12 @@ DOT8_KEY = 9 | ||
58 | 63 | SPACE_KEY = 10 |
59 | 64 | |
60 | 65 | 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. | |
62 | 72 | try: |
63 | 73 | rootKey = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Enum\USB\Vid_1c71&Pid_c005") |
64 | 74 | except WindowsError: |
@@ -73,7 +83,7 @@ def _getPorts(): | ||
73 | 83 | break |
74 | 84 | try: |
75 | 85 | 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] | |
77 | 87 | except WindowsError: |
78 | 88 | continue |
79 | 89 |
@@ -90,6 +100,7 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): | ||
90 | 100 | name = "brailliantB" |
91 | 101 | # Translators: The name of a series of braille displays. |
92 | 102 | description = _("HumanWare Brailliant BI/B series") |
103 | + isThreadSafe = True | |
93 | 104 | |
94 | 105 | @classmethod |
95 | 106 | def check(cls): |
@@ -105,77 +116,71 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): | ||
105 | 116 | self.numCells = 0 |
106 | 117 | |
107 | 118 | for portType, port in _getPorts(): |
119 | + self.isHid = portType == "USB HID" | |
108 | 120 | # Try talking to the display. |
109 | 121 | 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: | |
112 | 127 | 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) | |
125 | 142 | if self.numCells: |
126 | 143 | # A display responded. |
127 | 144 | log.info("Found display with {cells} cells connected via {type} ({port})".format( |
128 | 145 | cells=self.numCells, type=portType, port=port)) |
129 | 146 | break |
147 | + self._dev.close() | |
130 | 148 | |
131 | 149 | else: |
132 | 150 | raise RuntimeError("No display found") |
133 | 151 | |
134 | - self._readTimer = wx.PyTimer(self._handleResponses) | |
135 | - self._readTimer.Start(READ_INTERVAL) | |
136 | 152 | self._keysDown = set() |
137 | 153 | self._ignoreKeyReleases = False |
138 | 154 | |
139 | 155 | def terminate(self): |
140 | 156 | try: |
141 | 157 | super(BrailleDisplayDriver, self).terminate() |
142 | - self._readTimer.Stop() | |
143 | - self._readTimer = None | |
144 | 158 | 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() | |
148 | 162 | |
149 | - def _sendMessage(self, msgId, payload=""): | |
163 | + def _serSendMessage(self, msgId, payload=""): | |
150 | 164 | if isinstance(payload, (int, bool)): |
151 | 165 | payload = chr(payload) |
152 | - self._ser.write("{header}{id}{length}{payload}".format( | |
166 | + self._dev.write("{header}{id}{length}{payload}".format( | |
153 | 167 | header=HEADER, id=msgId, |
154 | 168 | length=chr(len(payload)), payload=payload)) |
155 | 169 | |
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) | |
162 | 178 | |
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): | |
175 | 180 | if msgId == MSG_INIT_RESP: |
176 | 181 | if ord(payload[0]) != 0: |
177 | 182 | # 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) | |
179 | 184 | return |
180 | 185 | self.numCells = ord(payload[2]) |
181 | 186 |
@@ -187,22 +192,50 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver): | ||
187 | 192 | |
188 | 193 | elif msgId == MSG_KEY_UP: |
189 | 194 | 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() | |
198 | 196 | self._keysDown.discard(payload) |
199 | 197 | |
200 | 198 | else: |
201 | 199 | log.debugWarning("Unknown message: id {id!r}, payload {payload!r}".format(id=msgId, payload=payload)) |
202 | 200 | |
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 | + | |
203 | 229 | def display(self, cells): |
204 | 230 | # 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) | |
206 | 239 | |
207 | 240 | gestureMap = inputCore.GlobalGestureMap({ |
208 | 241 | "globalCommands.GlobalCommands": { |
@@ -232,6 +232,9 @@ confspec = ConfigObj(StringIO( | ||
232 | 232 | reportReadingStringChanges = boolean(default=True) |
233 | 233 | reportCompositionStringChanges = boolean(default=True) |
234 | 234 | |
235 | +[debugLog] | |
236 | + hwIo = boolean(default=false) | |
237 | + | |
235 | 238 | [upgrade] |
236 | 239 | newLaptopKeyboardLayout = boolean(default=false) |
237 | 240 | """ |
@@ -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) |
@@ -1,7 +1,7 @@ | ||
1 | 1 | #hwPortUtils.py |
2 | 2 | #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 | |
5 | 5 | |
6 | 6 | """Utilities for working with hardware connection ports. |
7 | 7 | """ |
@@ -11,6 +11,8 @@ import ctypes | ||
11 | 11 | from ctypes.wintypes import BOOL, WCHAR, HWND, DWORD, ULONG, WORD |
12 | 12 | import _winreg as winreg |
13 | 13 | from winKernel import SYSTEMTIME |
14 | +import config | |
15 | +from logHandler import log | |
14 | 16 | |
15 | 17 | def ValidHandle(value): |
16 | 18 | if value == 0: |
@@ -93,6 +95,8 @@ SetupDiGetDeviceRegistryProperty.restype = BOOL | ||
93 | 95 | |
94 | 96 | GUID_CLASS_COMPORT = GUID(0x86e0d1e0L, 0x8089, 0x11d0, |
95 | 97 | (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)) | |
96 | 100 | |
97 | 101 | DIGCF_PRESENT = 2 |
98 | 102 | DIGCF_DEVICEINTERFACE = 16 |
@@ -105,12 +109,15 @@ ERROR_NO_MORE_ITEMS = 259 | ||
105 | 109 | DICS_FLAG_GLOBAL = 0x00000001 |
106 | 110 | DIREG_DEV = 0x00000001 |
107 | 111 | |
112 | +def _isDebug(): | |
113 | + return config.conf["debugLog"]["hwIo"] | |
114 | + | |
108 | 115 | def listComPorts(onlyAvailable=True): |
109 | 116 | """List com ports on the system. |
110 | 117 | @param onlyAvailable: Only return ports that are currently available. |
111 | 118 | @type onlyAvailable: bool |
112 | 119 | @return: Generates dicts including keys of port, friendlyName and hardwareID. |
113 | - @rtype: generator of (str, str, str) | |
120 | + @rtype: generator of dict | |
114 | 121 | """ |
115 | 122 | flags = DIGCF_DEVICEINTERFACE |
116 | 123 | if onlyAvailable: |
@@ -221,10 +228,14 @@ def listComPorts(onlyAvailable=True): | ||
221 | 228 | else: |
222 | 229 | entry["friendlyName"] = buf.value |
223 | 230 | |
231 | + if _isDebug(): | |
232 | + log.debug("%r" % entry) | |
224 | 233 | yield entry |
225 | 234 | |
226 | 235 | finally: |
227 | 236 | SetupDiDestroyDeviceInfoList(g_hdi) |
237 | + if _isDebug(): | |
238 | + log.debug("Finished listing com ports") | |
228 | 239 | |
229 | 240 | BLUETOOTH_MAX_NAME_SIZE = 248 |
230 | 241 | BTH_ADDR = BLUETOOTH_ADDRESS = ULONGLONG |
@@ -295,3 +306,182 @@ def getWidcommBluetoothPortInfo(port): | ||
295 | 306 | name = winreg.QueryValueEx(itemKey, "BDName")[0] |
296 | 307 | return addr, name |
297 | 308 | 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") |
@@ -226,3 +226,6 @@ def DuplicateHandle(sourceProcessHandle, sourceHandle, targetProcessHandle, desi | ||
226 | 226 | if kernel32.DuplicateHandle(sourceProcessHandle, sourceHandle, targetProcessHandle, byref(targetHandle), desiredAccess, inheritHandle, options) == 0: |
227 | 227 | raise WinError() |
228 | 228 | return targetHandle.value |
229 | + | |
230 | +PAPCFUNC = ctypes.WINFUNCTYPE(None, ctypes.wintypes.ULONG) | |
231 | +THREAD_SET_CONTEXT = 16 |
@@ -9,7 +9,9 @@ | ||
9 | 9 | - New braille translation tables: Polish 8 dot computer braille, Mongolian. (#5537, #5574) |
10 | 10 | - 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) |
11 | 11 | - 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) | |
13 | 15 | |
14 | 16 | |
15 | 17 | == Changes == |
@@ -37,12 +39,19 @@ | ||
37 | 39 | - When a toggle button is focused, NVDA now reports when it is changed from pressed to not pressed. (#5441) |
38 | 40 | - Reporting of mouse shape changes again works as expected. (#5595) |
39 | 41 | - 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) | |
40 | 43 | |
41 | 44 | |
42 | 45 | == Changes for Developers == |
43 | 46 | - The new audioDucking.AudioDucker class allows code which outputs audio to indicate when background audio should be ducked. (#3830) |
44 | 47 | - nvwave.WavePlayer's constructor now has a wantDucking keyword argument which specifies whether background audio should be ducked while audio is playing. (#3830) |
45 | 48 | - 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. | |
46 | 55 | |
47 | 56 | |
48 | 57 | = 2015.4 = |
@@ -1658,14 +1658,13 @@ Please see the display's documentation for descriptions of where these keys can | ||
1658 | 1658 | ++ Baum/Humanware/APH Braille Displays ++ |
1659 | 1659 | 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. |
1660 | 1660 | These include: |
1661 | -- Baum: SuperVario, PocketVario, VarioUltra (Bluetooth only), Pronto! (Bluetooth only) | |
1661 | +- Baum: SuperVario, PocketVario, VarioUltra, Pronto! | |
1662 | 1662 | - HumanWare: Brailliant, BrailleConnect |
1663 | 1663 | - APH: Refreshabraille |
1664 | 1664 | - |
1665 | 1665 | Some other displays manufactured by Baum may also work, though this has not been tested. |
1666 | 1666 | |
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. | |
1669 | 1668 | |
1670 | 1669 | Following are the key assignments for this display with NVDA. |
1671 | 1670 | 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 | ||
1722 | 1721 | |
1723 | 1722 | ++ HumanWare Brailliant BI/B Series ++ |
1724 | 1723 | 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. | |
1726 | 1726 | |
1727 | 1727 | Following are the key assignments for this display with NVDA. |
1728 | 1728 | Please see the display's documentation for descriptions of where these keys can be found. |