Skip to content
57 changes: 55 additions & 2 deletions source/brailleDisplayDrivers/handyTech.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
Comment thread
codeofdusk marked this conversation as resolved.
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."""
Expand Down Expand Up @@ -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):
Expand Down
Loading