Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 55 additions & 91 deletions source/braille.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: UTF-8 -*-
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
Expand Down Expand Up @@ -50,6 +49,7 @@
import brailleViewer
from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting
from utils.security import objectBelowLockScreenAndWindowsIsLocked
import hwIo

if TYPE_CHECKING:
from NVDAObjects import NVDAObject
Expand Down Expand Up @@ -1771,6 +1771,10 @@ class BrailleHandler(baseObject.AutoPropertyObject):
(TETHER_REVIEW,_("to review"))
]

queuedWrite: Optional[List[int]] = None
queuedWriteLock: threading.Lock
ackTimerHandle: int

def __init__(self):
louisHelper.initialize()
self.display: Optional[BrailleDisplayDriver] = None
Expand All @@ -1792,6 +1796,10 @@ def __init__(self):
self._detector = None
self._rawText = u""

self.queuedWriteLock = threading.Lock()
self.ackTimerHandle = winKernel.createWaitableTimer()
self._ackTimeoutResetterApc = winKernel.PAPCFUNC(self._ackTimeoutResetter)

brailleViewer.postBrailleViewerToolToggledAction.register(self._onBrailleViewerChangedState)

def terminate(self):
Expand All @@ -1806,7 +1814,11 @@ def terminate(self):
if self.display:
self.display.terminate()
self.display = None
_BgThread.stop()
if self.ackTimerHandle:
if not ctypes.windll.kernel32.CancelWaitableTimer(self.ackTimerHandle):
raise ctypes.WinError()
winKernel.closeHandle(self.ackTimerHandle)
self.ackTimerHandle = None
louisHelper.terminate()

def getTether(self):
Expand Down Expand Up @@ -1896,9 +1908,6 @@ def setDisplayByName( # noqa: C901
# Re-initialize with supported kwargs.
extensionPoints.callWithSupportedKwargs(newDisplay.__init__, **kwargs)
else:
if newDisplay.isThreadSafe:
# Start the thread if it wasn't already.
_BgThread.start()
try:
newDisplay = newDisplay(**kwargs)
except TypeError:
Expand Down Expand Up @@ -1959,7 +1968,7 @@ def _updateDisplay(self):
# Make sure we start the blink timer from the main thread to avoid wx assertions
wx.CallAfter(self._cursorBlinkTimer.Start,blinkRate)

def _writeCells(self, cells):
def _writeCells(self, cells: List[int]):
brailleViewer.update(cells, self._rawText)
if not self.display.isThreadSafe:
try:
Expand All @@ -1968,16 +1977,21 @@ def _writeCells(self, cells):
log.error("Error displaying cells. Disabling display", exc_info=True)
self.handleDisplayUnavailable()
return
with _BgThread.queuedWriteLock:
alreadyQueued = _BgThread.queuedWrite
_BgThread.queuedWrite = cells
with self.queuedWriteLock:
alreadyQueued: Optional[List[int]] = self.queuedWrite
self.queuedWrite = cells
# If a write was already queued, we don't need to queue another;
# we just replace the data.
# This means that if multiple writes occur while an earlier write is still in progress,
# we skip all but the last.
if not alreadyQueued and not self.display._awaitingAck:
# Queue a call to the background thread.
_BgThread.queueApc(_BgThread.executor)
self._writeCellsInBackground()

def _writeCellsInBackground(self):
"""Writes cells to a braille display in the background by queuing a function to the i/o thread.
"""
hwIo.bgThread.queueAsApc(self._bgThreadExecutor)

def _displayWithCursor(self):
if not self._cells:
Expand Down Expand Up @@ -2285,97 +2299,48 @@ def _disableDetection(self):
self._detector = None
self._detectionEnabled = False

class _BgThread:
"""A singleton background thread used for background writes and raw braille display I/O.
"""

thread = None
exit = False
queuedWrite = None

@classmethod
def start(cls):
if cls.thread:
return
cls.queuedWriteLock = threading.Lock()
thread = cls.thread = threading.Thread(
name=f"{cls.__module__}.{cls.__qualname__}",
target=cls.func
)
thread.daemon = True
thread.start()
cls.handle = ctypes.windll.kernel32.OpenThread(winKernel.THREAD_SET_CONTEXT, False, thread.ident)
cls.ackTimerHandle = winKernel.createWaitableTimer()

@classmethod
def queueApc(cls, func, param=0):
# Ensure the thread is running
cls.start()
ctypes.windll.kernel32.QueueUserAPC(func, cls.handle, param)

@classmethod
def stop(cls, timeout=None):
if not cls.thread:
return
cls.exit = True
if not ctypes.windll.kernel32.CancelWaitableTimer(cls.ackTimerHandle):
raise ctypes.WinError()
winKernel.closeHandle(cls.ackTimerHandle)
cls.ackTimerHandle = None
# Wake up the thread. It will exit when it sees exit is True.
cls.queueApc(cls.executor)
cls.thread.join(timeout)
cls.exit = False
winKernel.closeHandle(cls.handle)
cls.handle = None
cls.thread = None

@winKernel.PAPCFUNC
def executor(param):
if _BgThread.exit:
# func will see this and exit.
return
if not handler.display:
# Sometimes, the executor is triggered when a display is not fully initialized.
def _bgThreadExecutor(self, param: int):
"""Executed as APC when cells have to be written to a display asynchronously.
"""
if not self.display:
# Sometimes, the bg thread executor is triggered when a display is not fully initialized.
# For example, this happens when handling an ACK during initialisation.
# We can safely ignore this.
return
if handler.display._awaitingAck:
if self.display._awaitingAck:
# Do not write cells when we are awaiting an ACK
return
with _BgThread.queuedWriteLock:
data = _BgThread.queuedWrite
_BgThread.queuedWrite = None
with self.queuedWriteLock:
data: Optional[List[int]] = self.queuedWrite
self.queuedWrite = None
if not data:
return
try:
handler.display.display(data)
self.display.display(data)
except:
log.error("Error displaying cells. Disabling display", exc_info=True)
handler.handleDisplayUnavailable()
self.handleDisplayUnavailable()
else:
if handler.display.receivesAckPackets:
handler.display._awaitingAck = True
if self.display.receivesAckPackets:
self.display._awaitingAck = True
# Wait twice the display driver timeout for acknowledgement packets
# Note: timeout is in seconds whereas setWaitableTimer expects milliseconds
winKernel.setWaitableTimer(
_BgThread.ackTimerHandle,
int(handler.display.timeout*2000),
self.ackTimerHandle,
int(self.display.timeout * 2000),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know about this value? Why is it multiplied by 2000?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is converted to milli seconds and then multiplied by two. I added a descriptive comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be clearer if the two intentions were separated (IE, twice time out, convert to MS). Using named constants is a step better than a comment because the name is more likely to be updated to stay correct than a nearby comment.

				SECOND_TO_MS = 1000
				winKernel.setWaitableTimer(
					self.ackTimerHandle,
					# Wait twice the display driver timeout for acknowledgement packets
					# Note: timeout is in seconds whereas setWaitableTimer expects milliseconds
					int(self.display.timeout * 2 * SECOND_TO_MS ),

0,
_BgThread.ackTimeoutResetter
self._ackTimeoutResetterApc
)

@winKernel.PAPCFUNC
def ackTimeoutResetter(param):
if handler.display.receivesAckPackets and handler.display._awaitingAck:
log.debugWarning("Waiting for %s ACK packet timed out"%handler.display.name)
handler.display._awaitingAck = False
_BgThread.queueApc(_BgThread.executor)

@classmethod
def func(cls):
while True:
ctypes.windll.kernel32.SleepEx(winKernel.INFINITE, True)
if cls.exit:
break
def _ackTimeoutResetter(self, param: int):
if (
self.display
and self.display.receivesAckPackets
and self.display._awaitingAck
):
log.debugWarning(f"Waiting for {self.display.name} ACK packet timed out")
self.display._awaitingAck = False
self._writeCellsInBackground()


# Maps old braille display driver names to new drivers that supersede old drivers.
Expand Down Expand Up @@ -2464,9 +2429,8 @@ class BrailleDisplayDriver(driverHandler.Driver):
_awaitingAck = False
#: Maximum timeout to use for communication with a device (in seconds).
#: This can be used for serial connections.
#: Furthermore, it is used by L{_BgThread} to stop waiting for missed acknowledgement packets.
#: @type: float
timeout = 0.2
#: Furthermore, it is used to stop waiting for missed acknowledgement packets.
timeout: float = 0.2

def __init__(self, port: typing.Union[None, str, bdDetect.DeviceMatch] = None):
"""Constructor
Expand Down Expand Up @@ -2663,10 +2627,10 @@ def _handleAck(self):
"""Base implementation to handle acknowledgement packets."""
if not self.receivesAckPackets:
raise NotImplementedError("This display driver does not support ACK packet handling")
if not ctypes.windll.kernel32.CancelWaitableTimer(_BgThread.ackTimerHandle):
if not ctypes.windll.kernel32.CancelWaitableTimer(handler.ackTimerHandle):
raise ctypes.WinError()
self._awaitingAck = False
_BgThread.queueApc(_BgThread.executor)
handler._writeCellsInBackground()

@classmethod
def DotFirmnessSetting(cls,defaultVal,minVal,maxVal,useConfig=False):
Expand Down
10 changes: 10 additions & 0 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ def resetConfiguration(factoryDefaults=False):
import speech
import vision
import inputCore
import hwIo
import tones
log.debug("Terminating vision")
vision.terminate()
Expand All @@ -223,6 +224,8 @@ def resetConfiguration(factoryDefaults=False):
speech.terminate()
log.debug("terminating tones")
tones.terminate()
log.debug("Terminating background i/o")
hwIo.terminate()
log.debug("terminating addonHandler")
addonHandler.terminate()
log.debug("Reloading config")
Expand All @@ -237,6 +240,9 @@ def resetConfiguration(factoryDefaults=False):
languageHandler.setLanguage(lang)
# Addons
addonHandler.initialize()
# Hardware background i/o
log.debug("initializing background i/o")
hwIo.initialize()
# Tones
tones.initialize()
#Speech
Expand Down Expand Up @@ -538,6 +544,9 @@ def main():
import NVDAHelper
log.debug("Initializing NVDAHelper")
NVDAHelper.initialize()
log.debug("initializing background i/o")
import hwIo
hwIo.initialize()
log.debug("Initializing tones")
import tones
tones.initialize()
Expand Down Expand Up @@ -804,6 +813,7 @@ def _doPostNvdaStartupAction():
_terminate(brailleInput)
_terminate(braille)
_terminate(speech)
_terminate(hwIo)
_terminate(addonHandler)
_terminate(garbageHandler)
# DMP is only started if needed.
Expand Down
15 changes: 15 additions & 0 deletions source/hwIo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,18 @@
getByte
)
from .hid import Hid # noqa: F401
from .ioThread import IoThread

bgThread: IoThread


def initialize():
global bgThread
bgThread = IoThread()
bgThread.start()


def terminate():
global bgThread
bgThread.stop()
bgThread = None
16 changes: 9 additions & 7 deletions source/hwIo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,15 @@ def __init__(
self._ioDoneInst = LPOVERLAPPED_COMPLETION_ROUTINE(self._ioDone)
self._writeOl = OVERLAPPED()
# Do the initial read.
@winKernel.PAPCFUNC
def init(param):
self._initApc = None
self._asyncRead()
# Ensure the APC stays alive until it runs.
self._initApc = init
braille._BgThread.queueApc(init)
self._initialRead()

def _initialRead(self):
"""Performs the initial background read by queuing it as an APC to the IO background thread.
This method is tied to the built-in i/o thread.
It can be overridden to do an initial read on a different thread.
"""
from . import bgThread
bgThread.queueAsApc(lambda param: self._asyncRead())

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