A clone of: https://github.com/nutechsoftware/alarmdecoder This is requires as they dropped support for older firmware releases w/o building in backward compatibility code, and they had previously hardcoded pyserial to a python2 only version.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

263 lines
7.1 KiB

  1. """
  2. This module contains the :py:class:`SerialDevice` interface for the `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_.
  3. .. _AD2USB: http://www.alarmdecoder.com
  4. .. _AD2SERIAL: http://www.alarmdecoder.com
  5. .. _AD2PI: http://www.alarmdecoder.com
  6. .. moduleauthor:: Scott Petersen <scott@nutech.com>
  7. """
  8. import threading
  9. import serial
  10. import serial.tools.list_ports
  11. import select
  12. import sys
  13. from .base_device import Device
  14. from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack, filter_ad2prot_byte
  15. class SerialDevice(Device):
  16. """
  17. `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_ device utilizing the PySerial interface.
  18. """
  19. # Constants
  20. BAUDRATE = 19200
  21. """Default baudrate for Serial devices."""
  22. @staticmethod
  23. def find_all(pattern=None):
  24. """
  25. Returns all serial ports present.
  26. :param pattern: pattern to search for when retrieving serial ports
  27. :type pattern: string
  28. :returns: list of devices
  29. :raises: :py:class:`~alarmdecoder.util.CommError`
  30. """
  31. devices = []
  32. try:
  33. if pattern:
  34. devices = serial.tools.list_ports.grep(pattern)
  35. else:
  36. devices = serial.tools.list_ports.comports()
  37. except serial.SerialException as err:
  38. raise CommError('Error enumerating serial devices: {0}'.format(str(err)), err)
  39. return devices
  40. @property
  41. def interface(self):
  42. """
  43. Retrieves the interface used to connect to the device.
  44. :returns: interface used to connect to the device
  45. """
  46. return self._port
  47. @interface.setter
  48. def interface(self, value):
  49. """
  50. Sets the interface used to connect to the device.
  51. :param value: name of the serial device
  52. :type value: string
  53. """
  54. self._port = value
  55. def __init__(self, interface=None):
  56. """
  57. Constructor
  58. :param interface: device to open
  59. :type interface: string
  60. """
  61. Device.__init__(self)
  62. self._port = interface
  63. self._id = interface
  64. # Timeout = non-blocking to match pyftdi.
  65. self._device = serial.Serial(timeout=0, writeTimeout=0)
  66. def open(self, baudrate=BAUDRATE, no_reader_thread=False):
  67. """
  68. Opens the device.
  69. :param baudrate: baudrate to use with the device
  70. :type baudrate: int
  71. :param no_reader_thread: whether or not to automatically start the
  72. reader thread.
  73. :type no_reader_thread: bool
  74. :raises: :py:class:`~alarmdecoder.util.NoDeviceError`
  75. """
  76. # Set up the defaults
  77. if baudrate is None:
  78. baudrate = SerialDevice.BAUDRATE
  79. if self._port is None:
  80. raise NoDeviceError('No device interface specified.')
  81. self._read_thread = Device.ReadThread(self)
  82. # Open the device and start up the reader thread.
  83. try:
  84. self._device.port = self._port
  85. self._device.open()
  86. # NOTE: Setting the baudrate before opening the
  87. # port caused issues with Moschip 7840/7820
  88. # USB Serial Driver converter. (mos7840)
  89. #
  90. # Moving it to this point seems to resolve
  91. # all issues with it.
  92. self._device.baudrate = baudrate
  93. except (serial.SerialException, ValueError, OSError) as err:
  94. raise NoDeviceError('Error opening device on {0}.'.format(self._port), err)
  95. else:
  96. self._running = True
  97. self.on_open()
  98. if not no_reader_thread:
  99. self._read_thread.start()
  100. return self
  101. def close(self):
  102. """
  103. Closes the device.
  104. """
  105. try:
  106. Device.close(self)
  107. except Exception:
  108. pass
  109. def fileno(self):
  110. """
  111. Returns the file number associated with the device
  112. :returns: int
  113. """
  114. return self._device.fileno()
  115. def write(self, data):
  116. """
  117. Writes data to the device.
  118. :param data: data to write
  119. :type data: string
  120. :raises: py:class:`~alarmdecoder.util.CommError`
  121. """
  122. try:
  123. # Hack to support unicode under Python 2.x
  124. if isinstance(data, str) or (sys.version_info < (3,) and isinstance(data, unicode)):
  125. data = data.encode('utf-8')
  126. self._device.write(data)
  127. except serial.SerialTimeoutException:
  128. pass
  129. except serial.SerialException as err:
  130. raise CommError('Error writing to device.', err)
  131. else:
  132. self.on_write(data=data)
  133. def read(self):
  134. """
  135. Reads a single character from the device.
  136. :returns: character read from the device
  137. :raises: :py:class:`~alarmdecoder.util.CommError`
  138. """
  139. data = b''
  140. try:
  141. read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5)
  142. if len(read_ready) != 0:
  143. data = filter_ad2prot_byte(self._device.read(1))
  144. except serial.SerialException as err:
  145. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  146. return data
  147. def read_line(self, timeout=0.0, purge_buffer=False):
  148. """
  149. Reads a line from the device.
  150. :param timeout: read timeout
  151. :type timeout: float
  152. :param purge_buffer: Indicates whether to purge the buffer prior to
  153. reading.
  154. :type purge_buffer: bool
  155. :returns: line that was read
  156. :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError`
  157. """
  158. def timeout_event():
  159. """Handles read timeout event"""
  160. timeout_event.reading = False
  161. timeout_event.reading = True
  162. if purge_buffer:
  163. self._buffer = b''
  164. got_line, ret = False, None
  165. timer = threading.Timer(timeout, timeout_event)
  166. if timeout > 0:
  167. timer.start()
  168. try:
  169. while timeout_event.reading:
  170. read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5)
  171. if len(read_ready) == 0:
  172. continue
  173. buf = filter_ad2prot_byte(self._device.read(1))
  174. if buf != b'':
  175. self._buffer += buf[0]
  176. if buf == b"\n":
  177. self._buffer = self._buffer.rstrip(b"\r\n")
  178. if len(self._buffer) > 0:
  179. got_line = True
  180. break
  181. except (OSError, serial.SerialException) as err:
  182. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  183. else:
  184. if got_line:
  185. ret, self._buffer = self._buffer, b''
  186. self.on_read(data=ret)
  187. else:
  188. raise TimeoutError('Timeout while waiting for line terminator.')
  189. finally:
  190. timer.cancel()
  191. return ret
  192. def purge(self):
  193. """
  194. Purges read/write buffers.
  195. """
  196. self._device.flushInput()
  197. self._device.flushOutput()