From 09a80d8a1e7797b727a47d1bc3989a9571b76a46 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Tue, 16 Jun 2026 14:42:17 -0700 Subject: [PATCH 1/9] Interpret reading position packets from HandyTech Active Tactile Control This commit surfaces no user behaviour; to be done in future work. --- source/brailleDisplayDrivers/handyTech.py | 57 ++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index 628c4c0e67f..fcfcfd662b7 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,63 @@ 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 + + def __init__(self, display): + super().__init__(display) + self._readingPosition: Optional[int] = None + self._lastRefreshTime = 0.0 + def postInit(self): super(AtcMixin, self).postInit() log.debug("Enabling ATC") self._display.atc = True + def display(self, cells: List[int]): + self._lastRefreshTime = time.monotonic() + super().display(cells) + + 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 + reading_position = packet[1] + elapsed_since_refresh = time.monotonic() - self._lastRefreshTime + is_settling_after_refresh = elapsed_since_refresh < self.READ_SUPPRESS_AFTER_REFRESH_SECONDS + if reading_position == self.UNKNOWN_READING_POSITION: + if is_settling_after_refresh: + return + self._readingPosition = None + return + if reading_position >= self.numCells: + log.debugWarning( + f"Display has {self.numCells} cells but reported an ATC " + f"reading position of {reading_position}", + ) + 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 is_settling_after_refresh and self._readingPosition is None: + return + if ( + is_settling_after_refresh + and self._readingPosition is not None + and abs(reading_position - self._readingPosition) > 2 + ): + return + if reading_position == self._readingPosition: + return + self._readingPosition = reading_position + class TimeSyncFirmnessMixin(object): """Functionality for displays that support time synchronization and dot firmness adjustments.""" @@ -1078,6 +1130,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): From e34932ca65e6d11ad7e2f5d8166f4ce07bc90dba Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Tue, 16 Jun 2026 17:11:38 -0700 Subject: [PATCH 2/9] Review actions --- source/brailleDisplayDrivers/handyTech.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index fcfcfd662b7..a86cf5e4784 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -261,6 +261,7 @@ class AtcMixin(object): # 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) @@ -268,13 +269,13 @@ def __init__(self, display): self._lastRefreshTime = 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]): - self._lastRefreshTime = time.monotonic() super().display(cells) + self._lastRefreshTime = time.monotonic() def handleReadingPosition(self, packet: bytes) -> None: if len(packet) != self.READING_POSITION_PACKET_LENGTH: @@ -300,14 +301,11 @@ def handleReadingPosition(self, packet: bytes) -> None: # 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 is_settling_after_refresh and self._readingPosition is None: - return - if ( - is_settling_after_refresh - and self._readingPosition is not None - and abs(reading_position - self._readingPosition) > 2 - ): - return + if is_settling_after_refresh: + if self._readingPosition is None: + return + if abs(reading_position - self._readingPosition) > self.READING_POSITION_MAX_JUMP_DURING_SETTLE: + return if reading_position == self._readingPosition: return self._readingPosition = reading_position From 65e8c07d6ac93481e04d42a28c8cf55b63e90326 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Tue, 30 Jun 2026 13:54:53 -0700 Subject: [PATCH 3/9] Update source/brailleDisplayDrivers/handyTech.py Co-authored-by: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> --- source/brailleDisplayDrivers/handyTech.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index a86cf5e4784..b69481ca6c0 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -265,7 +265,7 @@ class AtcMixin(object): def __init__(self, display): super().__init__(display) - self._readingPosition: Optional[int] = None + self._readingPosition: int | None = None self._lastRefreshTime = 0.0 def postInit(self): From 530a39b44ebec0e9ce7c0e425b00f8d589843174 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Tue, 30 Jun 2026 13:55:07 -0700 Subject: [PATCH 4/9] Update source/brailleDisplayDrivers/handyTech.py Co-authored-by: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> --- source/brailleDisplayDrivers/handyTech.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index b69481ca6c0..a99c5b887f3 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -273,7 +273,7 @@ def postInit(self): log.debug("Enabling ATC") self._display.atc = True - def display(self, cells: List[int]): + def display(self, cells: list[int]): super().display(cells) self._lastRefreshTime = time.monotonic() From 76473582d65a8d51cd76b70be67b2348154530e4 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Tue, 30 Jun 2026 13:55:25 -0700 Subject: [PATCH 5/9] Update source/brailleDisplayDrivers/handyTech.py Co-authored-by: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> --- source/brailleDisplayDrivers/handyTech.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index a99c5b887f3..cd1edf04376 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -266,7 +266,7 @@ class AtcMixin(object): def __init__(self, display): super().__init__(display) self._readingPosition: int | None = None - self._lastRefreshTime = 0.0 + self._lastRefreshTime: float = 0.0 def postInit(self): super().postInit() From 91b38b9031409f2ef71c36acc0754dd4f7a95b66 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Tue, 30 Jun 2026 13:55:43 -0700 Subject: [PATCH 6/9] Update source/brailleDisplayDrivers/handyTech.py Co-authored-by: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> --- source/brailleDisplayDrivers/handyTech.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index cd1edf04376..f22cda6f589 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -283,7 +283,7 @@ def handleReadingPosition(self, packet: bytes) -> None: f"Unexpected ATC reading position packet with length {len(packet)}: {packet!r}", ) return - reading_position = packet[1] + readingPosition = packet[1] elapsed_since_refresh = time.monotonic() - self._lastRefreshTime is_settling_after_refresh = elapsed_since_refresh < self.READ_SUPPRESS_AFTER_REFRESH_SECONDS if reading_position == self.UNKNOWN_READING_POSITION: From c7bc71bb5be71de76b3ef575f628271f5b97712c Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Tue, 30 Jun 2026 13:55:57 -0700 Subject: [PATCH 7/9] Update source/brailleDisplayDrivers/handyTech.py Co-authored-by: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> --- source/brailleDisplayDrivers/handyTech.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index f22cda6f589..24ed4b8a204 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -284,7 +284,7 @@ def handleReadingPosition(self, packet: bytes) -> None: ) return readingPosition = packet[1] - elapsed_since_refresh = time.monotonic() - self._lastRefreshTime + elapsedSinceRefresh = time.monotonic() - self._lastRefreshTime is_settling_after_refresh = elapsed_since_refresh < self.READ_SUPPRESS_AFTER_REFRESH_SECONDS if reading_position == self.UNKNOWN_READING_POSITION: if is_settling_after_refresh: From e4df86ee9f1028ee93fec3cfdcc24b638cb9bd8f Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Tue, 30 Jun 2026 13:56:10 -0700 Subject: [PATCH 8/9] Update source/brailleDisplayDrivers/handyTech.py Co-authored-by: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> --- source/brailleDisplayDrivers/handyTech.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index 24ed4b8a204..36cdf0273b0 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -285,7 +285,7 @@ def handleReadingPosition(self, packet: bytes) -> None: return readingPosition = packet[1] elapsedSinceRefresh = time.monotonic() - self._lastRefreshTime - is_settling_after_refresh = elapsed_since_refresh < self.READ_SUPPRESS_AFTER_REFRESH_SECONDS + isSettlingAfterRefresh = elapsed_since_refresh < self.READ_SUPPRESS_AFTER_REFRESH_SECONDS if reading_position == self.UNKNOWN_READING_POSITION: if is_settling_after_refresh: return From 3327bd462630af67469a7e4759f553d348f97422 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Tue, 30 Jun 2026 14:15:59 -0700 Subject: [PATCH 9/9] Fix --- source/brailleDisplayDrivers/handyTech.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index 36cdf0273b0..f65c9f4c9ff 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -285,30 +285,30 @@ def handleReadingPosition(self, packet: bytes) -> None: return readingPosition = packet[1] elapsedSinceRefresh = time.monotonic() - self._lastRefreshTime - isSettlingAfterRefresh = elapsed_since_refresh < self.READ_SUPPRESS_AFTER_REFRESH_SECONDS - if reading_position == self.UNKNOWN_READING_POSITION: - if is_settling_after_refresh: + isSettlingAfterRefresh = elapsedSinceRefresh < self.READ_SUPPRESS_AFTER_REFRESH_SECONDS + if readingPosition == self.UNKNOWN_READING_POSITION: + if isSettlingAfterRefresh: return self._readingPosition = None return - if reading_position >= self.numCells: + if readingPosition >= self.numCells: log.debugWarning( f"Display has {self.numCells} cells but reported an ATC " - f"reading position of {reading_position}", + 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 is_settling_after_refresh: + if isSettlingAfterRefresh: if self._readingPosition is None: return - if abs(reading_position - self._readingPosition) > self.READING_POSITION_MAX_JUMP_DURING_SETTLE: + if abs(readingPosition - self._readingPosition) > self.READING_POSITION_MAX_JUMP_DURING_SETTLE: return - if reading_position == self._readingPosition: + if readingPosition == self._readingPosition: return - self._readingPosition = reading_position + self._readingPosition = readingPosition class TimeSyncFirmnessMixin(object):