Repository: nvaccess/nvda

Issue:
Make HumanWare BrailleNote driver thread safe
The current HumanWare BrailleNote driver, although native, does not yet utilize the hwIo module. This driver should be updated to support braille display auto detection, since we agreed in #1271 that this will be a prerequisite.

@ragb: Would you be able to do this?


Discussion:
Hi, a few months ago I and @JCSTeh discussed this briefly when I worked on BrailleNote QT support, and we both agreed that moving to HwIO would provide significant benefits. Jamie knows more about thread safety of this driver, and I do have a BrailleNote I can use for testing purposes, so I’d say let’s try it.

 

From: Leonard de Ruijter [mailto:notifications@github.com] 
Sent: Wednesday, September 6, 2017 8:17 AM
To: nvaccess/nvda <nvda@noreply.github.com>
Cc: Subscribed <subscribed@noreply.github.com>
Subject: [nvaccess/nvda] Make HumanWare BrailleNote thread safe (#7574)

 

The current HumanWare BrailleNote driver, although native, does not yet utilize the hwIo module. This driver should be updated to support braille display auto detection, since we agreed in #1271 <https://github.com/nvaccess/nvda/issues/1271>  that this will be a prerequisite.

@ragb <https://github.com/ragb> : Would you be able to do this?

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub <https://github.com/nvaccess/nvda/issues/7574> , or mute the thread <https://github.com/notifications/unsubscribe-auth/AHgLkLEb5kVAwZ-VWWq4pv1ZkMZXpeuVks5sfrdxgaJpZM4POiXw> .


I do not have such a device myself, and from what i've learnt from driver development, it is really hard when you do not have a device. I'd be happy to review code alongside others though.
Any driver that users hwIo should be thread safe. Porting this driver should be fairly simple. The bits that need the most attention relate to the way packets are read. As I understand it, each packet is 2 bytes (except the describe response, which is 3 bytes). Because this is serial, I'd recommend you stick to the default of receiving 1 byte in the callback, then reading 1 further byte for the argument (or 2 if handling the describe response). This is because with serial, you can't trust that the first byte you ever read will be the first byte of a packet; it could be the second byte of a previous packet.

Resolving patch:
diff --git a/source/brailleDisplayDrivers/brailleNote.py b/source/brailleDisplayDrivers/brailleNote.py
--- a/source/brailleDisplayDrivers/brailleNote.py
+++ b/source/brailleDisplayDrivers/brailleNote.py
@@ -2,7 +2,7 @@
 #A part of NonVisual Desktop Access (NVDA)
 #This file is covered by the GNU General Public License.
 #See the file COPYING for more details.
-# Copyright (C) 2011, 2012  Rui Batista <ruiandrebatista@gmail.com>
+# Copyright (C) 2011-2017 Rui Batista, NV Access Limited
 
 """ Braille Display driver for the BrailleNote notetakers in terminal mode.
 USB, serial and bluetooth communications are supported.
@@ -11,12 +11,12 @@
 from collections import OrderedDict
 import itertools
 import serial
-import wx
 import braille
 import brailleInput
 import hwPortUtils
 import inputCore
 from logHandler import log
+import hwIo
 
 BLUETOOTH_NAMES = ("Braillenote",)
 BLUETOOTH_ADDRS = (
@@ -29,7 +29,6 @@
 
 BAUD_RATE = 38400
 TIMEOUT = 0.1
-READ_INTERVAL = 50
 
 # Tags sent by the braillenote
 # Combinations of dots 1...6
@@ -87,6 +86,7 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
 	name = "brailleNote"
 	# Translators: Names of braille displays
 	description = _("HumanWare BrailleNote")
+	isThreadSafe = True
 
 	@classmethod
 	def check(cls):
@@ -139,7 +139,6 @@ def getPossiblePorts(cls):
 	def __init__(self, port="auto"):
 		super(BrailleDisplayDriver, self).__init__()
 		self._serial = None
-		self._buffer = ""
 		if port == "auto":
 			portsToTry = itertools.chain(self._getUSBPorts(), self._getBluetoothPorts())
 		elif port == "usb":
@@ -148,76 +147,51 @@ def __init__(self, port="auto"):
 			portsToTry = self._getBluetoothPorts()
 		else:
 			portsToTry = (port,)
-		found = False
 		for port in portsToTry:
 			log.debug("Checking port %s for a BrailleNote", port)
 			try:
-				self._serial = serial.Serial(port, baudrate=BAUD_RATE, timeout=TIMEOUT, writeTimeout=TIMEOUT, parity=serial.PARITY_NONE)
-			except serial.SerialException:
+				self._serial = hwIo.Serial(port, baudrate=BAUD_RATE, timeout=TIMEOUT, writeTimeout=TIMEOUT, parity=serial.PARITY_NONE, onReceive=self._onReceive)
+			except EnvironmentError:
 				continue
 			# Check for cell information
 			if self._describe():
 				log.debug("BrailleNote found on %s with %d cells", port, self.numCells)
-				found = True
 				break
 			else:
 				self._serial.close()
-		if not found:
+		else:
 			raise RuntimeError("Can't find a braillenote device (port = %s)" % port)
-		# start reading keys
-		self._readTimer = wx.PyTimer(self._readKeys)
-		self._readTimer.Start(READ_INTERVAL)
 
 	def terminate(self):
 		try:
 			super(BrailleDisplayDriver, self).terminate()
-			self._readTimer.Stop()
-			self._readTimer = None
 		finally:
-			self._closeComPort()
-
-	def _closeComPort(self):
-		if self._readTimer is not None:
-			self._readTimer.Stop()
-			self._readTimer = None
-		if self._serial is not None:
-			log.debug("Closing port %s", self._serial.port)
 			self._serial.close()
 			self._serial = None
 
 	def _describe(self):
-		log.debug("Writing sdescribe tag")
+		self.numCells = 0
+		log.debug("Writing describe tag")
 		self._serial.write(DESCRIBE_TAG)
-		# This seems always able to read the three bytes, but if someone complain it might be better to retry
-		packet = self._serial.read(3)
-		log.debug("Read %d bytes", len(packet))
-		if len(packet) != 3 or packet[0] != chr(STATUS_TAG):
-			log.debug("Not a braillenote")
-			return False
-		self._numCells = ord(packet[2])
-		return True
+		self._serial.waitForRead(TIMEOUT)
+		# If a valid response was received, _onReceive will have set numCells.
+		if self.numCells:
+			return True
+		log.debug("Not a braillenote")
+		return False
 
-	def _get_numCells(self):
-		return self._numCells
-
-	def _readKeys(self):
-		try:
-			while self._serial is not None and self._serial.inWaiting():
-				command, arg = self._readPacket()
-				if command:
-					self._dispatch(command, arg)
-		except serial.SerialException:
-			self._closeComPort()
-			# Reraise to be logged
-			raise
+	def _onReceive(self, command):
+		command = ord(command)
+		if command == STATUS_TAG:
+			arg = self._serial.read(2)
+			self.numCells = ord(arg[1])
+			return
+		arg = self._serial.read(1)
+		if not arg:
+			log.debugWarning("Timeout reading argument for command 0x%X" % command)
+			return
+		self._dispatch(command, ord(arg))
 
-	def _readPacket(self):
-		self._buffer += self._serial.read(2 - len(self._buffer))
-		if len(self._buffer) < 2:
-			return None, None
-		command, arg = ord(self._buffer[0]), ord(self._buffer[1])
-		self._buffer = ""
-		return command, arg
 	def _dispatch(self, command, arg):
 		space = False
 		if command == THUNB_KEYS_TAG :
@@ -241,16 +215,9 @@ def _dispatch(self, command, arg):
 			pass
 
 	def display(self, cells):
-		# if the serial port is not open don't even try to write
-		if self._serial is None:
-			return
 		# ESCAPE must be quoted because it is a control character
 		cells = [chr(cell).replace(ESCAPE, ESCAPE * 2) for cell in cells]
-		try:
-			self._serial.write(DISPLAY_TAG + "".join(cells))
-		except serial.SerialException, e:
-			self._closeComPort()
-			raise
+		self._serial.write(DISPLAY_TAG + "".join(cells))
 
 	gestureMap = inputCore.GlobalGestureMap({
 		"globalCommands.GlobalCommands": {


Test patch: