Skip to content

Commit 37c0203

Browse files
authored
Merge 7b03697 into 99a9ea8
2 parents 99a9ea8 + 7b03697 commit 37c0203

2 files changed

Lines changed: 75 additions & 25 deletions

File tree

source/hwIo/base.py

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# A part of NonVisual Desktop Access (NVDA)
22
# This file is covered by the GNU General Public License.
33
# See the file COPYING for more details.
4-
# Copyright (C) 2015-2018 NV Access Limited, Babbage B.V.
4+
# Copyright (C) 2015-2023 NV Access Limited, Babbage B.V., Leonard de Ruijter
55

66

77
"""Raw input/output for braille displays via serial and HID.
@@ -10,37 +10,42 @@
1010
See L{braille.BrailleDisplayDriver.isThreadSafe}.
1111
"""
1212

13+
from __future__ import annotations
1314
import sys
1415
import ctypes
1516
from ctypes import byref
1617
from ctypes.wintypes import DWORD
1718
from typing import Optional, Any, Union, Tuple, Callable
18-
19+
import weakref
1920
import serial
2021
from serial.win32 import OVERLAPPED, FILE_FLAG_OVERLAPPED, INVALID_HANDLE_VALUE, ERROR_IO_PENDING, COMMTIMEOUTS, CreateFile, SetCommTimeouts
2122
import winKernel
2223
import braille
2324
from logHandler import log
2425
import config
2526
import time
27+
from .ioThread import IoThread
2628

2729
LPOVERLAPPED_COMPLETION_ROUTINE = ctypes.WINFUNCTYPE(None, DWORD, DWORD, serial.win32.LPOVERLAPPED)
2830

2931
def _isDebug():
3032
return config.conf["debugLog"]["hwIo"]
3133

34+
3235
class IoBase(object):
3336
"""Base class for raw I/O.
3437
This watches for data of a specified size and calls a callback when it is received.
3538
"""
39+
_ioThreadRef: weakref.ReferenceType[IoThread]
3640

3741
def __init__(
3842
self,
3943
fileHandle: Union[ctypes.wintypes.HANDLE],
4044
onReceive: Callable[[bytes], None],
4145
writeFileHandle: Optional[ctypes.wintypes.HANDLE] = None,
4246
onReceiveSize: int = 1,
43-
onReadError: Optional[Callable[[int], bool]] = None
47+
onReadError: Optional[Callable[[int], bool]] = None,
48+
ioThread: Optional[IoThread] = None,
4449
):
4550
"""Constructor.
4651
@param fileHandle: A handle to an open I/O device opened for overlapped I/O.
@@ -50,8 +55,10 @@ def __init__(
5055
@param writeFileHandle: A handle to an open output device opened for overlapped I/O.
5156
@param onReceiveSize: The size (in bytes) of the data with which to call C{onReceive}.
5257
@param onReadError: If provided, a callback that takes the error code for a failed read
53-
and returns True if the I/O loop should exit cleanly or False if an
54-
exception should be thrown
58+
and returns True if the I/O loop should exit cleanly or False if an
59+
exception should be thrown
60+
@param ioThread: If provided, the I/O thread used for background reads.
61+
if C{None}, defaults to L{hwIo.bgThread}
5562
"""
5663
self._file = fileHandle
5764
self._onReceive = onReceive
@@ -63,16 +70,20 @@ def __init__(
6370
self._recvEvt = winKernel.createEvent()
6471
self._ioDoneInst = LPOVERLAPPED_COMPLETION_ROUTINE(self._ioDone)
6572
self._writeOl = OVERLAPPED()
73+
if ioThread is None:
74+
from . import bgThread as ioThread
75+
self._ioThreadRef = weakref.ref(ioThread)
6676
# Do the initial read.
6777
self._initialRead()
6878

6979
def _initialRead(self):
70-
"""Performs the initial background read by queuing it as an APC to the IO background thread.
71-
This method is tied to the built-in i/o thread.
72-
It can be overridden to do an initial read on a different thread.
80+
"""Performs the initial background read by queuing it as an APC to the IO background thread
81+
provided at initialization time.
7382
"""
74-
from . import bgThread
75-
bgThread.queueAsApc(lambda param: self._asyncRead())
83+
ioThread = self._ioThreadRef()
84+
if not ioThread:
85+
raise RuntimeError("I/O thread is no longer available")
86+
ioThread.queueAsApc(lambda param: self._asyncRead())
7687

7788
def waitForRead(self, timeout:Union[int, float]) -> bool:
7889
"""Wait for a chunk of data to be received and processed.
@@ -173,21 +184,30 @@ def _notifyReceive(self, data: bytes):
173184
except:
174185
log.error("", exc_info=True)
175186

187+
176188
class Serial(IoBase):
177189
"""Raw I/O for serial devices.
178190
This extends pyserial to call a callback when data is received.
179191
"""
180192

181193
def __init__(
182-
self,
183-
*args,
184-
onReceive: Callable[[bytes], None],
185-
**kwargs):
194+
self,
195+
*args,
196+
onReceive: Callable[[bytes], None],
197+
onReadError: Optional[Callable[[int], bool]] = None,
198+
ioThread: Optional[IoThread] = None,
199+
**kwargs
200+
):
186201
"""Constructor.
187202
Pass the arguments you would normally pass to L{serial.Serial}.
188-
There is also one additional required keyword argument.
203+
There are also some additional keyword arguments ( the first is required).
189204
@param onReceive: A callable taking a byte of received data as its only argument.
190205
This callable can then call C{read} to get additional data if desired.
206+
@param onReadError: If provided, a callback that takes the error code for a failed read
207+
and returns True if the I/O loop should exit cleanly or False if an
208+
exception should be thrown
209+
@param ioThread: If provided, the I/O thread used for background reads.
210+
if C{None}, defaults to L{hwIo.bgThread}
191211
"""
192212
self._ser = None
193213
self.port = args[0] if len(args) >= 1 else kwargs["port"]
@@ -202,7 +222,12 @@ def __init__(
202222
self._origTimeout = self._ser.timeout
203223
# We don't want a timeout while we're waiting for data.
204224
self._setTimeout(None)
205-
super(Serial, self).__init__(self._ser._port_handle, onReceive)
225+
super().__init__(
226+
self._ser._port_handle,
227+
onReceive,
228+
onReadError=onReadError,
229+
ioThread=ioThread
230+
)
206231

207232
def read(self, size=1) -> bytes:
208233
data = self._ser.read(size)
@@ -257,16 +282,19 @@ def __init__(
257282
self, path: str, epIn: int, epOut: int,
258283
onReceive: Callable[[bytes], None],
259284
onReceiveSize: int = 1,
260-
onReadError: Optional[Callable[[int], bool]] = None
285+
onReadError: Optional[Callable[[int], bool]] = None,
286+
ioThread: Optional[IoThread] = None,
261287
):
262288
"""Constructor.
263289
@param path: The device path.
264290
@param epIn: The endpoint to read data from.
265291
@param epOut: The endpoint to write data to.
266292
@param onReceive: A callable taking a received input report as its only argument.
267293
@param onReadError: An optional callable that handles read errors.
268-
It takes an error code and returns True if the error has been handled,
269-
allowing the read loop to exit cleanly, or False if an exception should be thrown.
294+
It takes an error code and returns True if the error has been handled,
295+
allowing the read loop to exit cleanly, or False if an exception should be thrown.
296+
@param ioThread: If provided, the I/O thread used for background reads.
297+
if C{None}, defaults to L{hwIo.bgThread}
270298
"""
271299
if _isDebug():
272300
log.debug("Opening device %s" % path)
@@ -284,9 +312,14 @@ def __init__(
284312
if _isDebug():
285313
log.debug("Open write handle failed: %s" % ctypes.WinError())
286314
raise ctypes.WinError()
287-
super(Bulk, self).__init__(readHandle, onReceive,
288-
writeFileHandle=writeHandle, onReceiveSize=onReceiveSize,
289-
onReadError=onReadError)
315+
super().__init__(
316+
readHandle,
317+
onReceive,
318+
writeFileHandle=writeHandle,
319+
onReceiveSize=onReceiveSize,
320+
onReadError=onReadError,
321+
ioThread=ioThread
322+
)
290323

291324
def close(self):
292325
super(Bulk, self).close()

source/hwIo/hid.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
import ctypes
1313
from ctypes import byref
1414
from ctypes.wintypes import USHORT
15-
from typing import Tuple, Callable
15+
from typing import Tuple, Callable, Optional
16+
from .ioThread import IoThread
1617

1718
from serial.win32 import FILE_FLAG_OVERLAPPED, INVALID_HANDLE_VALUE, CreateFile
1819
import winKernel
@@ -126,12 +127,24 @@ class Hid(IoBase):
126127
"""
127128
_featureSize: int
128129

129-
def __init__(self, path: str, onReceive: Callable[[bytes], None], exclusive: bool = True):
130+
def __init__(
131+
self,
132+
path: str,
133+
onReceive: Callable[[bytes], None],
134+
exclusive: bool = True,
135+
onReadError: Optional[Callable[[int], bool]] = None,
136+
ioThread: Optional[IoThread] = None,
137+
):
130138
"""Constructor.
131139
@param path: The device path.
132140
This can be retrieved using L{hwPortUtils.listHidDevices}.
133141
@param onReceive: A callable taking a received input report as its only argument.
134142
@param exclusive: Whether to block other application's access to this device.
143+
@param onReadError: An optional callable that handles read errors.
144+
It takes an error code and returns True if the error has been handled,
145+
allowing the read loop to exit cleanly, or False if an exception should be thrown.
146+
@param ioThread: If provided, the I/O thread used for background reads.
147+
if C{None}, defaults to L{hwIo.bgThread}
135148
"""
136149
if _isDebug():
137150
log.debug("Opening device %s" % path)
@@ -168,7 +181,11 @@ def __init__(self, path: str, onReceive: Callable[[bytes], None], exclusive: boo
168181
self._readSize = caps.InputReportByteLength
169182
# Reading any less than caps.InputReportByteLength is an error.
170183
super().__init__(
171-
handle, onReceive, onReceiveSize=caps.InputReportByteLength
184+
handle,
185+
onReceive,
186+
onReceiveSize=caps.InputReportByteLength,
187+
onReadError=onReadError,
188+
ioThread=ioThread
172189
)
173190

174191
@property

0 commit comments

Comments
 (0)