diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index 628c4c0e67f..f65c9f4c9ff 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -1,6 +1,6 @@ # A part of NonVisual Desktop Access (NVDA) # Copyright (C) 2008-2026 NV Access Limited, Bram Duvigneau, Babbage B.V., -# Felix Grützmacher (Handy Tech Elektronik GmbH), Leonard de Ruijter +# Felix Grützmacher (Handy Tech Elektronik GmbH), Leonard de Ruijter, Bill Dengler # This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license. # For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt @@ -255,11 +255,61 @@ def display(self, cells: List[int]): class AtcMixin(object): """Support for displays with Active Tactile Control (ATC)""" + READING_POSITION_PACKET_LENGTH = 2 + UNKNOWN_READING_POSITION = 0xFF + # ATC can report transient false reading positions immediately + # after cells are refreshed. Use a short settling window before + # accepting changes. + READ_SUPPRESS_AFTER_REFRESH_SECONDS = 0.25 + READING_POSITION_MAX_JUMP_DURING_SETTLE = 2 + + def __init__(self, display): + super().__init__(display) + self._readingPosition: int | None = None + self._lastRefreshTime: float = 0.0 + def postInit(self): - super(AtcMixin, self).postInit() + super().postInit() log.debug("Enabling ATC") self._display.atc = True + def display(self, cells: list[int]): + super().display(cells) + self._lastRefreshTime = time.monotonic() + + def handleReadingPosition(self, packet: bytes) -> None: + if len(packet) != self.READING_POSITION_PACKET_LENGTH: + log.debugWarning( + f"Unexpected ATC reading position packet with length {len(packet)}: {packet!r}", + ) + return + readingPosition = packet[1] + elapsedSinceRefresh = time.monotonic() - self._lastRefreshTime + isSettlingAfterRefresh = elapsedSinceRefresh < self.READ_SUPPRESS_AFTER_REFRESH_SECONDS + if readingPosition == self.UNKNOWN_READING_POSITION: + if isSettlingAfterRefresh: + return + self._readingPosition = None + return + if readingPosition >= self.numCells: + log.debugWarning( + f"Display has {self.numCells} cells but reported an ATC " + f"reading position of {readingPosition}", + ) + return + + # During refresh settling, preserve confirmed state. Real touches tend + # to remain near the previous cell, while refresh noise often appears + # as a release, a new touch from nowhere, or a large jump. + if isSettlingAfterRefresh: + if self._readingPosition is None: + return + if abs(readingPosition - self._readingPosition) > self.READING_POSITION_MAX_JUMP_DURING_SETTLE: + return + if readingPosition == self._readingPosition: + return + self._readingPosition = readingPosition + class TimeSyncFirmnessMixin(object): """Functionality for displays that support time synchronization and dot firmness adjustments.""" @@ -1078,6 +1128,9 @@ def _handleInputStream(self, htPacketType: bytes, stream): elif extPacketType == HT_EXTPKT_ATC_INFO: # Ignore ATC packets for now pass + elif extPacketType == HT_EXTPKT_READING_POSITION: + if isinstance(self._model, AtcMixin): + self._model.handleReadingPosition(packet) elif extPacketType == HT_EXTPKT_GET_PROTOCOL_PROPERTIES: self.numCells = packet[3] elif isinstance(self._model, TimeSyncFirmnessMixin):