hwIo.py 9.79 KB
#hwIo.py
#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) 2015-2016 NV Access Limited

"""Raw input/output for braille displays via serial and HID.
See the L{Serial} and L{Hid} classes.
Braille display drivers must be thread-safe to use this, as it utilises a background thread.
See L{braille.BrailleDisplayDriver.isThreadSafe}.
"""

import threading
import ctypes
from ctypes import byref
from ctypes.wintypes import DWORD, USHORT
import serial
from serial.win32 import MAXDWORD, OVERLAPPED, FILE_FLAG_OVERLAPPED, INVALID_HANDLE_VALUE, ERROR_IO_PENDING, COMMTIMEOUTS, CreateFile, SetCommTimeouts
import winKernel
import braille
from logHandler import log
import config

LPOVERLAPPED_COMPLETION_ROUTINE = ctypes.WINFUNCTYPE(None, DWORD, DWORD, serial.win32.LPOVERLAPPED)

def _isDebug():
	return config.conf["debugLog"]["hwIo"]

class IoBase(object):
	"""Base class for raw I/O.
	This watches for data of a specified size and calls a callback when it is received.
	"""

	def __init__(self, fileHandle, onReceive, onReceiveSize=1, writeSize=None):
		"""Constructr.
		@param fileHandle: A handle to an open I/O device opened for overlapped I/O.
		@param onReceive: A callable taking the received data as its only argument.
		@type onReceive: callable(str)
		@param onReceiveSize: The size (in bytes) of the data with which to call C{onReceive}.
		@type onReceiveSize: int
		@param writeSize: The size of the buffer for writes,
			C{None} to use the length of the data written.
		@param writeSize: int or None
		"""
		self._file = fileHandle
		self._onReceive = onReceive
		self._readSize = onReceiveSize
		self._writeSize = writeSize
		self._readBuf = ctypes.create_string_buffer(onReceiveSize)
		self._readOl = OVERLAPPED()
		self._recvEvt = threading.Event()
		self._ioDoneInst = LPOVERLAPPED_COMPLETION_ROUTINE(self._ioDone)
		self._writeOl = OVERLAPPED()
		# Do the initial read.
		@winKernel.PAPCFUNC
		def init(param):
			self._initApc = None
			self._asyncRead()
		# Ensure the APC stays alive until it runs.
		self._initApc = init
		braille._BgThread.queueApc(init)

	def waitForRead(self, timeout):
		"""Wait for a chunk of data to be received and processed.
		This will return after L{onReceive} has been called or when the timeout elapses.
		@param timeout: The maximum time to wait in seconds.
		@type timeout: int or float
		@return: C{True} if received data was processed before the timeout,
			C{False} if not.
		@rtype: bool
		"""
		if not self._recvEvt.wait(timeout):
			if _isDebug():
				log.debug("Wait timed out")
			return False
		self._recvEvt.clear()
		return True

	def write(self, data):
		if _isDebug():
			log.debug("Write: %r" % data)
		size = self._writeSize or len(data)
		buf = ctypes.create_string_buffer(size)
		buf.raw = data
		if not ctypes.windll.kernel32.WriteFile(self._file, data, size, None, byref(self._writeOl)):
			if ctypes.GetLastError() != ERROR_IO_PENDING:
				if _isDebug():
					log.debug("Write failed: %s" % ctypes.WinError())
				raise ctypes.WinError()
			bytes = DWORD()
			ctypes.windll.kernel32.GetOverlappedResult(self._file, byref(self._writeOl), byref(bytes), True)

	def close(self):
		if _isDebug():
			log.debug("Closing")
		self._onReceive = None
		ctypes.windll.kernel32.CancelIoEx(self._file, byref(self._readOl))

	def __del__(self):
		self.close()

	def _asyncRead(self):
		# Wait for _readSize bytes of data.
		# _ioDone will call onReceive once it is received.
		# onReceive can then optionally read additional bytes if it knows these are coming.
		ctypes.windll.kernel32.ReadFileEx(self._file, self._readBuf, self._readSize, byref(self._readOl), self._ioDoneInst)

	def _ioDone(self, error, bytes, overlapped):
		if not self._onReceive:
			# close has been called.
			self._ioDone = None
			return
		elif error != 0:
			raise ctypes.WinError(error)
		self._notifyReceive(self._readBuf[:bytes])
		self._recvEvt.set()
		self._asyncRead()

	def _notifyReceive(self, data):
		"""Called when data is received.
		The base implementation just calls the onReceive callback provided to the constructor.
		This can be extended to perform tasks before/after the callback.
		"""
		if _isDebug():
			log.debug("Read: %r" % data)
		try:
			self._onReceive(data)
		except:
			log.error("", exc_info=True)

class Serial(IoBase):
	"""Raw I/O for serial devices.
	This extends pyserial to call a callback when data is received.
	"""

	def __init__(self, *args, **kwargs):
		"""Constructor.
		Pass the arguments you would normally pass to L{serial.Serial}.
		There is also one additional keyword argument.
		@param onReceive: A callable taking a byte of received data as its only argument.
			This callable can then call C{read} to get additional data if desired.
		@type onReceive: callable(str)
		"""
		onReceive = kwargs.pop("onReceive")
		self._ser = None
		self.port = args[0] if len(args) >= 1 else kwargs["port"]
		if _isDebug():
			log.debug("Opening port %s" % self.port)
		try:
			self._ser = serial.Serial(*args, **kwargs)
		except Exception as e:
			if _isDebug():
				log.debug("Open failed: %s" % e)
			raise
		self._origTimeout = self._ser.timeout
		# We don't want a timeout while we're waiting for data.
		self._setTimeout(None)
		self.inWaiting = self._ser.inWaiting
		super(Serial, self).__init__(self._ser.hComPort, onReceive)

	def read(self, size=1):
		data = self._ser.read(size)
		if _isDebug():
			log.debug("Read: %r" % data)
		return data

	def write(self, data):
		if _isDebug():
			log.debug("Write: %r" % data)
		self._ser.write(data)

	def close(self):
		if not self._ser:
			return
		super(Serial, self).close()
		self._ser.close()

	def _notifyReceive(self, data):
		# Set the timeout for onReceive in case it does a sync read.
		self._setTimeout(self._origTimeout)
		super(Serial, self)._notifyReceive(data)
		self._setTimeout(None)

	def _setTimeout(self, timeout):
		# #6035: pyserial reconfigures all settings of the port when setting a timeout.
		# This can cause error 'Cannot configure port, some setting was wrong.'
		# Therefore, manually set the timeouts using the Win32 API.
		# Adapted from pyserial 3.1.1.
		timeouts = COMMTIMEOUTS()
		if timeout is not None:
			if timeout == 0:
				timeouts.ReadIntervalTimeout = win32.MAXDWORD
			else:
				timeouts.ReadTotalTimeoutConstant = max(int(timeout * 1000), 1)
		if timeout != 0 and self._ser._interCharTimeout is not None:
			timeouts.ReadIntervalTimeout = max(int(self._ser._interCharTimeout * 1000), 1)
		if self._ser._writeTimeout is not None:
			if self._ser._writeTimeout == 0:
				timeouts.WriteTotalTimeoutConstant = win32.MAXDWORD
			else:
				timeouts.WriteTotalTimeoutConstant = max(int(self._ser._writeTimeout * 1000), 1)
		SetCommTimeouts(self._ser.hComPort, ctypes.byref(timeouts))

class HIDP_CAPS (ctypes.Structure):
	_fields_ = (
		("Usage", USHORT),
		("UsagePage", USHORT),
		("InputReportByteLength", USHORT),
		("OutputReportByteLength", USHORT),
		("FeatureReportByteLength", USHORT),
		("Reserved", USHORT * 17),
		("NumberLinkCollectionNodes", USHORT),
		("NumberInputButtonCaps", USHORT),
		("NumberInputValueCaps", USHORT),
		("NumberInputDataIndices", USHORT),
		("NumberOutputButtonCaps", USHORT),
		("NumberOutputValueCaps", USHORT),
		("NumberOutputDataIndices", USHORT),
		("NumberFeatureButtonCaps", USHORT),
		("NumberFeatureValueCaps", USHORT),
		("NumberFeatureDataIndices", USHORT)
	)

class Hid(IoBase):
	"""Raw I/O for HID devices.
	"""

	def __init__(self, path, onReceive):
		"""Constructor.
		@param path: The device path.
			This can be retrieved using L{hwPortUtils.listHidDevices}.
		@type path: unicode
		@param onReceive: A callable taking a received input report as its only argument.
		@type onReceive: callable(str)
		"""
		if _isDebug():
			log.debug("Opening device %s" % path)
		handle = CreateFile(path, winKernel.GENERIC_READ | winKernel.GENERIC_WRITE,
			0, None, winKernel.OPEN_EXISTING, FILE_FLAG_OVERLAPPED, None)
		if handle == INVALID_HANDLE_VALUE:
			if _isDebug():
				log.debug("Open failed: %s" % ctypes.WinError())
			raise ctypes.WinError()
		pd = ctypes.c_void_p()
		if not ctypes.windll.hid.HidD_GetPreparsedData(handle, byref(pd)):
			raise ctypes.WinError()
		caps = HIDP_CAPS()
		ctypes.windll.hid.HidP_GetCaps(pd, byref(caps))
		ctypes.windll.hid.HidD_FreePreparsedData(pd)
		if _isDebug():
			log.debug("Report byte lengths: input %d, output %d, feature %d"
				% (caps.InputReportByteLength, caps.OutputReportByteLength,
					caps.FeatureReportByteLength))
		self._featureSize = caps.FeatureReportByteLength
		# Reading any less than caps.InputReportByteLength is an error.
		# On Windows 7, writing any less than caps.OutputReportByteLength is also an error.
		super(Hid, self).__init__(handle, onReceive,
			onReceiveSize=caps.InputReportByteLength,
			writeSize=caps.OutputReportByteLength)

	def getFeature(self, reportId):
		"""Get a feature report from this device.
		@param reportId: The report id.
		@type reportId: str
		@return: The report, including the report id.
		@rtype: str
		"""
		buf = ctypes.create_string_buffer(self._featureSize)
		buf[0] = reportId
		if not ctypes.windll.hid.HidD_GetFeature(self._file, buf, self._featureSize):
			if _isDebug():
				log.debug("Get feature %r failed: %s"
					% (reportId, ctypes.WinError()))
			raise ctypes.WinError()
		if _isDebug():
			log.debug("Get feature: %r" % buf.raw)
		return buf.raw

	def setFeature(self, report):
		"""Send a feature report to this device.
		@param report: The report, including its id.
		@type report: str
		"""
		length = len(report)
		buf = ctypes.create_string_buffer(length)
		buf.raw = report
		if _isDebug():
			log.debug("Set feature: %r" % report)
		if not ctypes.windll.hid.HidD_SetFeature(self._file, buf, length):
			if _isDebug():
				log.debug("Set feature failed: %s" % ctypes.WinError())
			raise ctypes.WinError()

	def close(self):
		super(Hid, self).close()
		winKernel.closeHandle(self._file)