1- # -*- coding: UTF-8 -*-
21# A part of NonVisual Desktop Access (NVDA)
32# This file is covered by the GNU General Public License.
43# See the file COPYING for more details.
5049import brailleViewer
5150from autoSettingsUtils .driverSetting import BooleanDriverSetting , NumericDriverSetting
5251from utils .security import objectBelowLockScreenAndWindowsIsLocked
52+ import hwIo
5353
5454if TYPE_CHECKING :
5555 from NVDAObjects import NVDAObject
@@ -1771,6 +1771,10 @@ class BrailleHandler(baseObject.AutoPropertyObject):
17711771 (TETHER_REVIEW ,_ ("to review" ))
17721772 ]
17731773
1774+ queuedWrite : Optional [List [int ]] = None
1775+ queuedWriteLock : threading .Lock
1776+ ackTimerHandle : int
1777+
17741778 def __init__ (self ):
17751779 louisHelper .initialize ()
17761780 self .display : Optional [BrailleDisplayDriver ] = None
@@ -1792,6 +1796,10 @@ def __init__(self):
17921796 self ._detector = None
17931797 self ._rawText = u""
17941798
1799+ self .queuedWriteLock = threading .Lock ()
1800+ self .ackTimerHandle = winKernel .createWaitableTimer ()
1801+ self ._ackTimeoutResetterApc = winKernel .PAPCFUNC (self ._ackTimeoutResetter )
1802+
17951803 brailleViewer .postBrailleViewerToolToggledAction .register (self ._onBrailleViewerChangedState )
17961804
17971805 def terminate (self ):
@@ -1806,7 +1814,11 @@ def terminate(self):
18061814 if self .display :
18071815 self .display .terminate ()
18081816 self .display = None
1809- _BgThread .stop ()
1817+ if self .ackTimerHandle :
1818+ if not ctypes .windll .kernel32 .CancelWaitableTimer (self .ackTimerHandle ):
1819+ raise ctypes .WinError ()
1820+ winKernel .closeHandle (self .ackTimerHandle )
1821+ self .ackTimerHandle = None
18101822 louisHelper .terminate ()
18111823
18121824 def getTether (self ):
@@ -1896,9 +1908,6 @@ def setDisplayByName( # noqa: C901
18961908 # Re-initialize with supported kwargs.
18971909 extensionPoints .callWithSupportedKwargs (newDisplay .__init__ , ** kwargs )
18981910 else :
1899- if newDisplay .isThreadSafe :
1900- # Start the thread if it wasn't already.
1901- _BgThread .start ()
19021911 try :
19031912 newDisplay = newDisplay (** kwargs )
19041913 except TypeError :
@@ -1959,7 +1968,7 @@ def _updateDisplay(self):
19591968 # Make sure we start the blink timer from the main thread to avoid wx assertions
19601969 wx .CallAfter (self ._cursorBlinkTimer .Start ,blinkRate )
19611970
1962- def _writeCells (self , cells ):
1971+ def _writeCells (self , cells : List [ int ] ):
19631972 brailleViewer .update (cells , self ._rawText )
19641973 if not self .display .isThreadSafe :
19651974 try :
@@ -1968,16 +1977,21 @@ def _writeCells(self, cells):
19681977 log .error ("Error displaying cells. Disabling display" , exc_info = True )
19691978 self .handleDisplayUnavailable ()
19701979 return
1971- with _BgThread .queuedWriteLock :
1972- alreadyQueued = _BgThread .queuedWrite
1973- _BgThread .queuedWrite = cells
1980+ with self .queuedWriteLock :
1981+ alreadyQueued : Optional [ List [ int ]] = self .queuedWrite
1982+ self .queuedWrite = cells
19741983 # If a write was already queued, we don't need to queue another;
19751984 # we just replace the data.
19761985 # This means that if multiple writes occur while an earlier write is still in progress,
19771986 # we skip all but the last.
19781987 if not alreadyQueued and not self .display ._awaitingAck :
19791988 # Queue a call to the background thread.
1980- _BgThread .queueApc (_BgThread .executor )
1989+ self ._writeCellsInBackground ()
1990+
1991+ def _writeCellsInBackground (self ):
1992+ """Writes cells to a braille display in the background by queuing a function to the i/o thread.
1993+ """
1994+ hwIo .bgThread .queueAsApc (self ._bgThreadExecutor )
19811995
19821996 def _displayWithCursor (self ):
19831997 if not self ._cells :
@@ -2285,97 +2299,48 @@ def _disableDetection(self):
22852299 self ._detector = None
22862300 self ._detectionEnabled = False
22872301
2288- class _BgThread :
2289- """A singleton background thread used for background writes and raw braille display I/O.
2290- """
2291-
2292- thread = None
2293- exit = False
2294- queuedWrite = None
2295-
2296- @classmethod
2297- def start (cls ):
2298- if cls .thread :
2299- return
2300- cls .queuedWriteLock = threading .Lock ()
2301- thread = cls .thread = threading .Thread (
2302- name = f"{ cls .__module__ } .{ cls .__qualname__ } " ,
2303- target = cls .func
2304- )
2305- thread .daemon = True
2306- thread .start ()
2307- cls .handle = ctypes .windll .kernel32 .OpenThread (winKernel .THREAD_SET_CONTEXT , False , thread .ident )
2308- cls .ackTimerHandle = winKernel .createWaitableTimer ()
2309-
2310- @classmethod
2311- def queueApc (cls , func , param = 0 ):
2312- # Ensure the thread is running
2313- cls .start ()
2314- ctypes .windll .kernel32 .QueueUserAPC (func , cls .handle , param )
2315-
2316- @classmethod
2317- def stop (cls , timeout = None ):
2318- if not cls .thread :
2319- return
2320- cls .exit = True
2321- if not ctypes .windll .kernel32 .CancelWaitableTimer (cls .ackTimerHandle ):
2322- raise ctypes .WinError ()
2323- winKernel .closeHandle (cls .ackTimerHandle )
2324- cls .ackTimerHandle = None
2325- # Wake up the thread. It will exit when it sees exit is True.
2326- cls .queueApc (cls .executor )
2327- cls .thread .join (timeout )
2328- cls .exit = False
2329- winKernel .closeHandle (cls .handle )
2330- cls .handle = None
2331- cls .thread = None
2332-
2333- @winKernel .PAPCFUNC
2334- def executor (param ):
2335- if _BgThread .exit :
2336- # func will see this and exit.
2337- return
2338- if not handler .display :
2339- # Sometimes, the executor is triggered when a display is not fully initialized.
2302+ def _bgThreadExecutor (self , param : int ):
2303+ """Executed as APC when cells have to be written to a display asynchronously.
2304+ """
2305+ if not self .display :
2306+ # Sometimes, the bg thread executor is triggered when a display is not fully initialized.
23402307 # For example, this happens when handling an ACK during initialisation.
23412308 # We can safely ignore this.
23422309 return
2343- if handler .display ._awaitingAck :
2310+ if self .display ._awaitingAck :
23442311 # Do not write cells when we are awaiting an ACK
23452312 return
2346- with _BgThread .queuedWriteLock :
2347- data = _BgThread .queuedWrite
2348- _BgThread .queuedWrite = None
2313+ with self .queuedWriteLock :
2314+ data : Optional [ List [ int ]] = self .queuedWrite
2315+ self .queuedWrite = None
23492316 if not data :
23502317 return
23512318 try :
2352- handler .display .display (data )
2319+ self .display .display (data )
23532320 except :
23542321 log .error ("Error displaying cells. Disabling display" , exc_info = True )
2355- handler .handleDisplayUnavailable ()
2322+ self .handleDisplayUnavailable ()
23562323 else :
2357- if handler .display .receivesAckPackets :
2358- handler .display ._awaitingAck = True
2324+ if self .display .receivesAckPackets :
2325+ self .display ._awaitingAck = True
2326+ # Wait twice the display driver timeout for acknowledgement packets
2327+ # Note: timeout is in seconds whereas setWaitableTimer expects milliseconds
23592328 winKernel .setWaitableTimer (
2360- _BgThread .ackTimerHandle ,
2361- int (handler .display .timeout * 2000 ),
2329+ self .ackTimerHandle ,
2330+ int (self .display .timeout * 2000 ),
23622331 0 ,
2363- _BgThread . ackTimeoutResetter
2332+ self . _ackTimeoutResetterApc
23642333 )
23652334
2366- @winKernel .PAPCFUNC
2367- def ackTimeoutResetter (param ):
2368- if handler .display .receivesAckPackets and handler .display ._awaitingAck :
2369- log .debugWarning ("Waiting for %s ACK packet timed out" % handler .display .name )
2370- handler .display ._awaitingAck = False
2371- _BgThread .queueApc (_BgThread .executor )
2372-
2373- @classmethod
2374- def func (cls ):
2375- while True :
2376- ctypes .windll .kernel32 .SleepEx (winKernel .INFINITE , True )
2377- if cls .exit :
2378- break
2335+ def _ackTimeoutResetter (self , param : int ):
2336+ if (
2337+ self .display
2338+ and self .display .receivesAckPackets
2339+ and self .display ._awaitingAck
2340+ ):
2341+ log .debugWarning (f"Waiting for { self .display .name } ACK packet timed out" )
2342+ self .display ._awaitingAck = False
2343+ self ._writeCellsInBackground ()
23792344
23802345
23812346# Maps old braille display driver names to new drivers that supersede old drivers.
@@ -2464,9 +2429,8 @@ class BrailleDisplayDriver(driverHandler.Driver):
24642429 _awaitingAck = False
24652430 #: Maximum timeout to use for communication with a device (in seconds).
24662431 #: This can be used for serial connections.
2467- #: Furthermore, it is used by L{_BgThread} to stop waiting for missed acknowledgement packets.
2468- #: @type: float
2469- timeout = 0.2
2432+ #: Furthermore, it is used to stop waiting for missed acknowledgement packets.
2433+ timeout : float = 0.2
24702434
24712435 def __init__ (self , port : typing .Union [None , str , bdDetect .DeviceMatch ] = None ):
24722436 """Constructor
@@ -2663,10 +2627,10 @@ def _handleAck(self):
26632627 """Base implementation to handle acknowledgement packets."""
26642628 if not self .receivesAckPackets :
26652629 raise NotImplementedError ("This display driver does not support ACK packet handling" )
2666- if not ctypes .windll .kernel32 .CancelWaitableTimer (_BgThread .ackTimerHandle ):
2630+ if not ctypes .windll .kernel32 .CancelWaitableTimer (handler .ackTimerHandle ):
26672631 raise ctypes .WinError ()
26682632 self ._awaitingAck = False
2669- _BgThread . queueApc ( _BgThread . executor )
2633+ handler . _writeCellsInBackground ( )
26702634
26712635 @classmethod
26722636 def DotFirmnessSetting (cls ,defaultVal ,minVal ,maxVal ,useConfig = False ):
0 commit comments