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
2628
2729LPOVERLAPPED_COMPLETION_ROUTINE = ctypes .WINFUNCTYPE (None , DWORD , DWORD , serial .win32 .LPOVERLAPPED )
2830
2931def _isDebug ():
3032 return config .conf ["debugLog" ]["hwIo" ]
3133
34+
3235class 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+
176188class 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 ()
0 commit comments