Skip to content

Commit 2fefdf0

Browse files
authored
Merge 22890e7 into 0fc81e9
2 parents 0fc81e9 + 22890e7 commit 2fefdf0

9 files changed

Lines changed: 226 additions & 107 deletions

File tree

source/braille.py

Lines changed: 55 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
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.
@@ -50,6 +49,7 @@
5049
import brailleViewer
5150
from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting
5251
from utils.security import objectBelowLockScreenAndWindowsIsLocked
52+
import hwIo
5353

5454
if 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):

source/core.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ def resetConfiguration(factoryDefaults=False):
212212
import speech
213213
import vision
214214
import inputCore
215+
import hwIo
215216
import tones
216217
log.debug("Terminating vision")
217218
vision.terminate()
@@ -223,6 +224,8 @@ def resetConfiguration(factoryDefaults=False):
223224
speech.terminate()
224225
log.debug("terminating tones")
225226
tones.terminate()
227+
log.debug("Terminating background i/o")
228+
hwIo.terminate()
226229
log.debug("terminating addonHandler")
227230
addonHandler.terminate()
228231
log.debug("Reloading config")
@@ -237,6 +240,9 @@ def resetConfiguration(factoryDefaults=False):
237240
languageHandler.setLanguage(lang)
238241
# Addons
239242
addonHandler.initialize()
243+
# Hardware background i/o
244+
log.debug("initializing background i/o")
245+
hwIo.initialize()
240246
# Tones
241247
tones.initialize()
242248
#Speech
@@ -538,6 +544,9 @@ def main():
538544
import NVDAHelper
539545
log.debug("Initializing NVDAHelper")
540546
NVDAHelper.initialize()
547+
log.debug("initializing background i/o")
548+
import hwIo
549+
hwIo.initialize()
541550
log.debug("Initializing tones")
542551
import tones
543552
tones.initialize()
@@ -804,6 +813,7 @@ def _doPostNvdaStartupAction():
804813
_terminate(brailleInput)
805814
_terminate(braille)
806815
_terminate(speech)
816+
_terminate(hwIo)
807817
_terminate(addonHandler)
808818
_terminate(garbageHandler)
809819
# DMP is only started if needed.

source/hwIo/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,18 @@
2020
getByte
2121
)
2222
from .hid import Hid # noqa: F401
23+
from .ioThread import IoThread
24+
25+
bgThread: IoThread
26+
27+
28+
def initialize():
29+
global bgThread
30+
bgThread = IoThread()
31+
bgThread.start()
32+
33+
34+
def terminate():
35+
global bgThread
36+
bgThread.stop()
37+
bgThread = None

source/hwIo/base.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,15 @@ def __init__(
6464
self._ioDoneInst = LPOVERLAPPED_COMPLETION_ROUTINE(self._ioDone)
6565
self._writeOl = OVERLAPPED()
6666
# Do the initial read.
67-
@winKernel.PAPCFUNC
68-
def init(param):
69-
self._initApc = None
70-
self._asyncRead()
71-
# Ensure the APC stays alive until it runs.
72-
self._initApc = init
73-
braille._BgThread.queueApc(init)
67+
self._initialRead()
68+
69+
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.
73+
"""
74+
from . import bgThread
75+
bgThread.queueAsApc(lambda param: self._asyncRead())
7476

7577
def waitForRead(self, timeout:Union[int, float]) -> bool:
7678
"""Wait for a chunk of data to be received and processed.

0 commit comments

Comments
 (0)