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.
1010See L{braille.BrailleDisplayDriver.isThreadSafe}.
1111"""
1212
13+ from __future__ import annotations
1314import sys
1415import ctypes
1516from ctypes import byref
1617from ctypes .wintypes import DWORD
1718from typing import Optional , Any , Union , Tuple , Callable
18-
19+ import weakref
1920import serial
2021from serial .win32 import OVERLAPPED , FILE_FLAG_OVERLAPPED , INVALID_HANDLE_VALUE , ERROR_IO_PENDING , COMMTIMEOUTS , CreateFile , SetCommTimeouts
2122import winKernel
2223import braille
2324from logHandler import log
2425import config
2526import time
27+ from .ioThread import IoThread
28+ # LPOVERLAPPED_COMPLETION_ROUTINE is imported for backwards compatibility.
29+ from .ioThread import LPOVERLAPPED_COMPLETION_ROUTINE # NOQA: F401
2630
27- LPOVERLAPPED_COMPLETION_ROUTINE = ctypes .WINFUNCTYPE (None , DWORD , DWORD , serial .win32 .LPOVERLAPPED )
2831
2932def _isDebug ():
3033 return config .conf ["debugLog" ]["hwIo" ]
3134
35+
3236class IoBase (object ):
3337 """Base class for raw I/O.
3438 This watches for data of a specified size and calls a callback when it is received.
3539 """
40+ _ioThreadRef : weakref .ReferenceType [IoThread ]
3641
3742 def __init__ (
3843 self ,
3944 fileHandle : Union [ctypes .wintypes .HANDLE ],
4045 onReceive : Callable [[bytes ], None ],
4146 writeFileHandle : Optional [ctypes .wintypes .HANDLE ] = None ,
4247 onReceiveSize : int = 1 ,
43- onReadError : Optional [Callable [[int ], bool ]] = None
48+ onReadError : Optional [Callable [[int ], bool ]] = None ,
49+ ioThread : Optional [IoThread ] = None ,
4450 ):
4551 """Constructor.
4652 @param fileHandle: A handle to an open I/O device opened for overlapped I/O.
@@ -50,8 +56,10 @@ def __init__(
5056 @param writeFileHandle: A handle to an open output device opened for overlapped I/O.
5157 @param onReceiveSize: The size (in bytes) of the data with which to call C{onReceive}.
5258 @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
59+ and returns True if the I/O loop should exit cleanly or False if an
60+ exception should be thrown
61+ @param ioThread: If provided, the I/O thread used for background reads.
62+ if C{None}, defaults to L{hwIo.bgThread}
5563 """
5664 self ._file = fileHandle
5765 self ._onReceive = onReceive
@@ -61,18 +69,21 @@ def __init__(
6169 self ._readBuf = ctypes .create_string_buffer (onReceiveSize )
6270 self ._readOl = OVERLAPPED ()
6371 self ._recvEvt = winKernel .createEvent ()
64- 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 (self ._asyncRead )
7687
7788 def waitForRead (self , timeout :Union [int , float ]) -> bool :
7889 """Wait for a chunk of data to be received and processed.
@@ -137,11 +148,20 @@ def __del__(self):
137148 if _isDebug ():
138149 log .debugWarning ("Couldn't delete object gracefully" , exc_info = True )
139150
140- def _asyncRead (self ):
151+ def _asyncRead (self , param : Optional [int ] = None ):
152+ ioThread = self ._ioThreadRef ()
153+ if not ioThread :
154+ raise RuntimeError ("I/O thread is no longer available" )
141155 # Wait for _readSize bytes of data.
142156 # _ioDone will call onReceive once it is received.
143157 # onReceive can then optionally read additional bytes if it knows these are coming.
144- ctypes .windll .kernel32 .ReadFileEx (self ._file , self ._readBuf , self ._readSize , byref (self ._readOl ), self ._ioDoneInst )
158+ ctypes .windll .kernel32 .ReadFileEx (
159+ self ._file ,
160+ self ._readBuf ,
161+ self ._readSize ,
162+ byref (self ._readOl ),
163+ ioThread .queueCompletionRoutine (self ._ioDone )
164+ )
145165
146166 def _ioDone (self , error , numberOfBytes : int , overlapped ):
147167 if not self ._onReceive :
@@ -173,21 +193,30 @@ def _notifyReceive(self, data: bytes):
173193 except :
174194 log .error ("" , exc_info = True )
175195
196+
176197class Serial (IoBase ):
177198 """Raw I/O for serial devices.
178199 This extends pyserial to call a callback when data is received.
179200 """
180201
181202 def __init__ (
182- self ,
183- * args ,
184- onReceive : Callable [[bytes ], None ],
185- ** kwargs ):
203+ self ,
204+ * args ,
205+ onReceive : Callable [[bytes ], None ],
206+ onReadError : Optional [Callable [[int ], bool ]] = None ,
207+ ioThread : Optional [IoThread ] = None ,
208+ ** kwargs
209+ ):
186210 """Constructor.
187211 Pass the arguments you would normally pass to L{serial.Serial}.
188- There is also one additional required keyword argument .
212+ There are also some additional keyword arguments ( the first is required) .
189213 @param onReceive: A callable taking a byte of received data as its only argument.
190214 This callable can then call C{read} to get additional data if desired.
215+ @param onReadError: If provided, a callback that takes the error code for a failed read
216+ and returns True if the I/O loop should exit cleanly or False if an
217+ exception should be thrown
218+ @param ioThread: If provided, the I/O thread used for background reads.
219+ if C{None}, defaults to L{hwIo.bgThread}
191220 """
192221 self ._ser = None
193222 self .port = args [0 ] if len (args ) >= 1 else kwargs ["port" ]
@@ -202,7 +231,12 @@ def __init__(
202231 self ._origTimeout = self ._ser .timeout
203232 # We don't want a timeout while we're waiting for data.
204233 self ._setTimeout (None )
205- super (Serial , self ).__init__ (self ._ser ._port_handle , onReceive )
234+ super ().__init__ (
235+ self ._ser ._port_handle ,
236+ onReceive ,
237+ onReadError = onReadError ,
238+ ioThread = ioThread
239+ )
206240
207241 def read (self , size = 1 ) -> bytes :
208242 data = self ._ser .read (size )
@@ -257,16 +291,19 @@ def __init__(
257291 self , path : str , epIn : int , epOut : int ,
258292 onReceive : Callable [[bytes ], None ],
259293 onReceiveSize : int = 1 ,
260- onReadError : Optional [Callable [[int ], bool ]] = None
294+ onReadError : Optional [Callable [[int ], bool ]] = None ,
295+ ioThread : Optional [IoThread ] = None ,
261296 ):
262297 """Constructor.
263298 @param path: The device path.
264299 @param epIn: The endpoint to read data from.
265300 @param epOut: The endpoint to write data to.
266301 @param onReceive: A callable taking a received input report as its only argument.
267302 @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.
303+ It takes an error code and returns True if the error has been handled,
304+ allowing the read loop to exit cleanly, or False if an exception should be thrown.
305+ @param ioThread: If provided, the I/O thread used for background reads.
306+ if C{None}, defaults to L{hwIo.bgThread}
270307 """
271308 if _isDebug ():
272309 log .debug ("Opening device %s" % path )
@@ -284,9 +321,14 @@ def __init__(
284321 if _isDebug ():
285322 log .debug ("Open write handle failed: %s" % ctypes .WinError ())
286323 raise ctypes .WinError ()
287- super (Bulk , self ).__init__ (readHandle , onReceive ,
288- writeFileHandle = writeHandle , onReceiveSize = onReceiveSize ,
289- onReadError = onReadError )
324+ super ().__init__ (
325+ readHandle ,
326+ onReceive ,
327+ writeFileHandle = writeHandle ,
328+ onReceiveSize = onReceiveSize ,
329+ onReadError = onReadError ,
330+ ioThread = ioThread
331+ )
290332
291333 def close (self ):
292334 super (Bulk , self ).close ()
0 commit comments