Repository: nvaccess/nvda

Issue:
Error in Freedom Scientific focus braille detection after standby/resume of PC
<!--
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples
-->

### Steps to reproduce:

1. Connect a Freedom Scientific braille display.
2. Send PC to standby.
3. After a while, reopen lid or wake PC up. Windows will come up with a login prompt or such to enter a PIN (in my case). NVDA's default logon screen version detects the focus.
4. Log in.
5. Braille display updates with info of last opened app.
6. After one or two focus changes, the display stops updating, and after a moment, the Windows "USB device disconnected" sound is heard, followed by the reconnect sound.
7. At the same time, an NVDA error sound is heard, and right after that, the display starts working as normal. The error log is:

ERROR - braille.executor (14:02:22.349):
Error displaying cells. Disabling display
Traceback (most recent call last):
  File "braille.pyc", line 2085, in executor
  File "brailleDisplayDrivers\freedomScientific.pyc", line 474, in display
  File "brailleDisplayDrivers\freedomScientific.pyc", line 279, in _sendPacket
  File "hwIo.pyc", line 101, in write
WindowsError: [Error 22] Das Gerät erkennt den Befehl nicht.
ERROR - braille.BrailleHandler.handleDisplayUnavailable (14:02:22.380):
Braille display unavailable. Disabling
Traceback (most recent call last):
  File "braille.pyc", line 2085, in executor
  File "brailleDisplayDrivers\freedomScientific.pyc", line 474, in display
  File "brailleDisplayDrivers\freedomScientific.pyc", line 279, in _sendPacket
  File "hwIo.pyc", line 101, in write
WindowsError: [Error 22] Das Gerät erkennt den Befehl nicht.
DEBUG - braille.BrailleHandler.setDisplayByName (14:02:22.407):
Switching braille display from freedomScientific to noBraille
INFO - braille.BrailleHandler.setDisplayByName (14:02:22.411):
Loaded braille display driver noBraille, current display has 0 cells.
DEBUG - brailleDisplayDrivers.freedomScientific.BrailleDisplayDriver._onReceive (14:02:22.423):
Got packet of type '\x01' with args: '\x00' '\x00' '\x00'
DEBUG - brailleDisplayDrivers.freedomScientific.BrailleDisplayDriver._onReceive (14:02:22.424):
Got packet of type '\x80' with args: '0' '\x00' '\x00'
DEBUG - brailleDisplayDrivers.freedomScientific.BrailleDisplayDriver._handlePacket (14:02:22.424):
Device info: manufacturer: FREEDOM SCIENTIFIC model: Focus 40, version: 5.81-34
INFO - brailleDisplayDrivers.freedomScientific.BrailleDisplayDriver.__init__ (14:02:22.424):
Found Focus 40 connected via custom (\\?\usb#vid_0f4e&pid_0114#1234567#{a5dcbf10-6530-11d2-901f-00c04fb951ed})
DEBUG - brailleDisplayDrivers.freedomScientific.BrailleDisplayDriver._configureDisplay (14:02:22.424):
Activating extended keys on freedom Scientific display. Display name: Focus 40, firmware version: 5.81-34.
DEBUG - braille.BrailleHandler.setDisplayByName (14:02:22.424):
Switching braille display from noBraille to freedomScientific
INFO - braille.BrailleHandler.setDisplayByName (14:02:22.428):
Loaded braille display driver freedomScientific, current display has 40 cells.

### Actual behavior:

See step 7 above. Interruption in braille display service.

### Expected behavior:

No interruption.

### System configuration
#### NVDA installed/portable/running from source:

Installed.

#### NVDA version:

version alpha-17832,7dd122b7

#### Windows version:

19h1.

#### Name and version of other software in use when reproducing the issue:

Freedom Scientific Focus 40 Blue 5th Generation with latest USB driver from FS.

#### Other information about your system:

Dell XPS15--9560.

### Other questions
#### Does the issue still occur after restarting your PC?

No, does not happen when initially logging in, only when waking up from standby.

#### Have you tried any other versions of NVDA? If so, please report their behaviors.

19.2beta2, same result. Earlier versions don't have the native driver from #7727 yet.

CC @bramd 


Discussion:
@MarcoZehe Thanks for the issue report. I'll try to reproduce this.

Braille handling between the NVDA version that runs on the sign in screen and the normal version is still a bit of a mess. NVDA doesn't explicitly close/free the display when you switch versions, so often braille doesn't even work on secure screens. It might be that the Freedom Scientific driver allows two applications to open it at once and the display gets messages from both leading to a disconnect or firmware crash.
Hmm, the two file handles that are used for Bulk communication are opened in exclusive mode. Therefore, it slightly amazes me that your system is able to pick up the Focus at the login prompt while there is a connection with the NVDA that's running under your user account.

Could you please describe the behavior when:

1. From a running desktop, you press ctrl+alt+del? Is the display picked up in that case? What happens if you switch back?
2. Set the default braille display to no braille, save your configuration and copy it to the system configuration. After that, NVDA shouldn't not communicate with the Focus at the logon screen. How is the behavior when you're coming out of stand by in that case?
1. After pressing CTRL+Alt+Del, the braille display comes up, with the English braille table and other indicators that the factory profile is now active. My standard in my user config is German.
2. If I escape out of this, the display briefly winks out, then comes back with German braille. And no errors, handoff seems to work fine between these two.
3. If choosing the Lock link after pressing CTRL+Alt+Del, and then unlocking the machine again, handoff also works fine.

Should I still try to set to NoBraille and such? Because it seems that this scenario I observe only applies to the moment when the machine returns from standby, meaning a real deep sleep. Normal handoff between these scenarios seems to work fine.
Interestingly, on my second machine, which has a Handy Tech ActiLino connected, which also uses auto detection, when in the lock screen, the display is not active. It is only active when logged in.
@MarcoZehe: Please compare the braille settings between both nvda.ini (userConfig and systemConfig) by opening them in Notepad++. The userConfig is located in AppData\Roaming\nvda and the systemConfig in the program folder where NVDA was installed.
Sorry it took me a while to get back to this. I still see this behavior in the latest alpha builds.

There is no nvda.ini in the program Files (x86)\NVDA folder, I only have one nvda.ini in my AppData/Roaming/nvda folder. I also don't remember ever consciously having removed the one from the Program Files folder.
@MarcoZehe: I meant the "nvda.ini" in the folder "C:\Program Files (x86)\NVDA\systemConfig". This folder stores the configuration for the logon/secure screen.
There's no nvda.ini there, either. Only an addonstate.picle file.
@MarcoZehe: If NVDA is installed, please press the button "Use currently saved settings on the logon and other secure screens (requires administrator privileges)" in the General NVDA Settings and then reboot the PC.
I have a customer with a 4th generation focus 80 , firmware 5.7.1 who seems to suffer from the same issue.

@jcsteh: Could you reproduce this with your Focus 5th gen?
I can't reproduce this here with my 5th gen. However, I too see output from the secure desktop copy of NVDA (e.g. when I press control+alt+delete), so apparently, exclusive mode is not so exclusive with this driver. Also:

1. In the main copy of NVDA, open the Python console.
2. Enter this command:
    `wx.CallLater(5000, lambda: braille.handler.message("hi"))`
3. Quickly and immediately press control+alt+delete.
    - Observe: When the control+alt_+delete screen comes up, you will see its output on the braille display.
4. Wait a few seconds.
    - Observe: "hi" appears on the display.
5. Press tab.
    - Observe: The next control focused in the control+alt+delete screen is reported on the display.
6. Wait a few more seconds (until the "hi" message expires).
    - Observe: "secure Desktop" appears on the display.

So it seems the display quite happily takes interspersed output from both copies. It doesn't seem to cause a driver crash on my system, but it's certainly conceivable that there is a race condition at play here.
This is strange. the hwIo.Bulk class doesn't specify any sharing parameters.

I wonder what 
`bdDetect.getConnectedUsbDevicesForDriver("freedomScientific")` returns 
for you. May be it has multiple matches.


```
>>> list(bdDetect.getConnectedUsbDevicesForDriver("freedomScientific"))
[DeviceMatch(type='custom', id='VID_0F4E&PID_0114', port='\\\\?\\usb#vid_0f4e&pid_0114#1234567#{a5dcbf10-6530-11d2-901f-00c04fb951ed}', deviceInfo={'hardwareID': 'USB\\VID_0F4E&PID_0114&REV_0581', 'usbID': 'VID_0F4E&PID_0114', 'devicePath': '\\\\?\\usb#vid_0f4e&pid_0114#1234567#{a5dcbf10-6530-11d2-901f-00c04fb951ed}'})]
```

So only 1 match.
cc: @bramd 
@MarcoZehe I guess you are still having this issue in NVDA 2020.1 Is this correct?
Correct, still happening.
I'd like to add that I'm seeing this issue as well on NVDA 2021.3 - are there any steps I can take to try and debug this? (There doesn't seem to be a `bdDetect` available in the Python console these days).

If it helps, I'm using a F40B
> [...] (There doesn't seem to be a `bdDetect` available in the Python console these days).


NVDA's python console behaves much like the standard Python's REPL, and while only very limited subset of modules is available in its namespace by default, you can happily do: `import bdDetect`.

Resolving patch:
diff --git a/source/brailleDisplayDrivers/freedomScientific.py b/source/brailleDisplayDrivers/freedomScientific.py
--- a/source/brailleDisplayDrivers/freedomScientific.py
+++ b/source/brailleDisplayDrivers/freedomScientific.py
@@ -208,7 +208,8 @@ def __init__(self, port="auto"):
 						epIn=1,
 						epOut=0,
 						onReceive=self._onReceive,
-						onReceiveSize=56
+						onReceiveSize=56,
+						onReadError=self._handleReadError
 					)
 				else:
 					self._dev = hwIo.Serial(
@@ -245,6 +246,7 @@ def __init__(self, port="auto"):
 			"globalCommands", "GlobalCommands", "braille_scrollBack")
 		self.gestureMap.add("br(freedomScientific):topRouting%d" % self.numCells,
 			"globalCommands", "GlobalCommands", "braille_scrollForward")
+		self._restarting = False
 
 	def terminate(self):
 		try:
@@ -317,6 +319,17 @@ def _onReceive(self, data: bytes):
 
 		self._handlePacket(packetType, arg1, arg2, arg3, payload)
 
+	def _handleReadError(self, error: int) -> bool:
+		if error == 995:  # Broken I/O pipe, terminate and allow restart
+			if not self._restarting:
+				# Will not cause a data race since this driver runs on one thread
+				self._restarting = True
+				log.info("Freedom Scientific display implicitly disconnected by suspend, reinitializing")
+				self.terminate()
+				self.__init__()
+			return True
+		return False
+
 	def _handlePacket(
 			self, packetType: bytes, arg1: bytes, arg2: bytes, arg3: bytes, payload: bytes
 	):
diff --git a/source/hwIo/base.py b/source/hwIo/base.py
--- a/source/hwIo/base.py
+++ b/source/hwIo/base.py
@@ -39,7 +39,8 @@ def __init__(
 			fileHandle: Union[ctypes.wintypes.HANDLE],
 			onReceive: Callable[[bytes], None],
 			writeFileHandle: Optional[ctypes.wintypes.HANDLE] = None,
-			onReceiveSize: int = 1
+			onReceiveSize: int = 1,
+			onReadError: Optional[Callable[[int], bool]] = None
 	):
 		"""Constructor.
 		@param fileHandle: A handle to an open I/O device opened for overlapped I/O.
@@ -48,11 +49,15 @@ def __init__(
 		@param onReceive: A callable taking the received data as its only argument.
 		@param writeFileHandle: A handle to an open output device opened for overlapped I/O.
 		@param onReceiveSize: The size (in bytes) of the data with which to call C{onReceive}.
+		@param onReadError: If provided, a callback that takes the error code for a failed read
+		  and returns True if the I/O loop should exit cleanly or False if an
+		  exception should be thrown
 		"""
 		self._file = fileHandle
 		self._onReceive = onReceive
 		self._writeFile = writeFileHandle if writeFileHandle is not None else fileHandle
 		self._readSize = onReceiveSize
+		self._onReadError = onReadError
 		self._readBuf = ctypes.create_string_buffer(onReceiveSize)
 		self._readOl = OVERLAPPED()
 		self._recvEvt = winKernel.createEvent()
@@ -141,7 +146,11 @@ def _ioDone(self, error, numberOfBytes: int, overlapped):
 			self._ioDone = None
 			return
 		elif error != 0:
-			raise ctypes.WinError(error)
+			if not self._onReadError or not self._onReadError(error):
+				raise ctypes.WinError(error)
+			else:
+				self._ioDone = None
+				return
 		self._notifyReceive(self._readBuf[:numberOfBytes])
 		winKernel.kernel32.SetEvent(self._recvEvt)
 		self._asyncRead()
@@ -244,13 +253,17 @@ class Bulk(IoBase):
 	def __init__(
 			self, path: str, epIn: int, epOut: int,
 			onReceive: Callable[[bytes], None],
-			onReceiveSize: int = 1
+			onReceiveSize: int = 1,
+			onReadError: Optional[Callable[[int], bool]] = None
 	):
 		"""Constructor.
 		@param path: The device path.
 		@param epIn: The endpoint to read data from.
 		@param epOut: The endpoint to write data to.
 		@param onReceive: A callable taking a received input report as its only argument.
+		@param onReadError: An optional callable that handles read errors.
+		  It takes an error code and returns True if the error has been handled,
+		  allowing the read loop to exit cleanly, or False if an exception should be thrown.
 		"""
 		if _isDebug():
 			log.debug("Opening device %s" % path)
@@ -269,7 +282,8 @@ def __init__(
 				log.debug("Open write handle failed: %s" % ctypes.WinError())
 			raise ctypes.WinError()
 		super(Bulk, self).__init__(readHandle, onReceive,
-			writeFileHandle=writeHandle, onReceiveSize=onReceiveSize)
+		                           writeFileHandle=writeHandle, onReceiveSize=onReceiveSize,
+		                           onReadError=onReadError)
 
 	def close(self):
 		super(Bulk, self).close()


Test patch: