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.

1196 lines
33 KiB

  1. """
  2. This module contains different types of devices belonging to the `AlarmDecoder`_ (AD2) family.
  3. * :py:class:`USBDevice`: Interfaces with the `AD2USB`_ device.
  4. * :py:class:`SerialDevice`: Interfaces with the `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_.
  5. * :py:class:`SocketDevice`: Interfaces with devices exposed through `ser2sock`_ or another IP to Serial solution.
  6. Also supports SSL if using `ser2sock`_.
  7. .. _ser2sock: http://github.com/nutechsoftware/ser2sock
  8. .. _AlarmDecoder: http://www.alarmdecoder.com
  9. .. _AD2USB: http://www.alarmdecoder.com
  10. .. _AD2SERIAL: http://www.alarmdecoder.com
  11. .. _AD2PI: http://www.alarmdecoder.com
  12. .. moduleauthor:: Scott Petersen <scott@nutech.com>
  13. """
  14. import time
  15. import threading
  16. import serial
  17. import serial.tools.list_ports
  18. import socket
  19. from .util import CommError, TimeoutError, NoDeviceError, InvalidMessageError
  20. from .event import event
  21. try:
  22. from pyftdi.pyftdi.ftdi import Ftdi, FtdiError
  23. import usb.core
  24. import usb.util
  25. have_pyftdi = True
  26. except ImportError:
  27. have_pyftdi = False
  28. try:
  29. from OpenSSL import SSL, crypto
  30. have_openssl = True
  31. except ImportError:
  32. class SSL:
  33. class Error(BaseException):
  34. pass
  35. class WantReadError(BaseException):
  36. pass
  37. class SysCallError(BaseException):
  38. pass
  39. have_openssl = False
  40. class Device(object):
  41. """
  42. Base class for all `AlarmDecoder`_ (AD2) device types.
  43. """
  44. # Generic device events
  45. on_open = event.Event("This event is called when the device has been opened.\n\n**Callback definition:** *def callback(device)*")
  46. on_close = event.Event("This event is called when the device has been closed.\n\n**Callback definition:** def callback(device)*")
  47. on_read = event.Event("This event is called when a line has been read from the device.\n\n**Callback definition:** def callback(device, data)*")
  48. on_write = event.Event("This event is called when data has been written to the device.\n\n**Callback definition:** def callback(device, data)*")
  49. def __init__(self):
  50. """
  51. Constructor
  52. """
  53. self._id = ''
  54. self._buffer = b''
  55. self._device = None
  56. self._running = False
  57. self._read_thread = None
  58. def __enter__(self):
  59. """
  60. Support for context manager __enter__.
  61. """
  62. return self
  63. def __exit__(self, exc_type, exc_value, traceback):
  64. """
  65. Support for context manager __exit__.
  66. """
  67. self.close()
  68. return False
  69. @property
  70. def id(self):
  71. """
  72. Retrieve the device ID.
  73. :returns: identification string for the device
  74. """
  75. return self._id
  76. @id.setter
  77. def id(self, value):
  78. """
  79. Sets the device ID.
  80. :param value: device identification string
  81. :type value: string
  82. """
  83. self._id = value
  84. def is_reader_alive(self):
  85. """
  86. Indicates whether or not the reader thread is alive.
  87. :returns: whether or not the reader thread is alive
  88. """
  89. return self._read_thread.is_alive()
  90. def stop_reader(self):
  91. """
  92. Stops the reader thread.
  93. """
  94. self._read_thread.stop()
  95. def close(self):
  96. """
  97. Closes the device.
  98. """
  99. try:
  100. self._running = False
  101. self._read_thread.stop()
  102. self._device.close()
  103. except Exception:
  104. pass
  105. self.on_close()
  106. class ReadThread(threading.Thread):
  107. """
  108. Reader thread which processes messages from the device.
  109. """
  110. READ_TIMEOUT = 10
  111. """Timeout for the reader thread."""
  112. def __init__(self, device):
  113. """
  114. Constructor
  115. :param device: device used by the reader thread
  116. :type device: :py:class:`~alarmdecoder.devices.Device`
  117. """
  118. threading.Thread.__init__(self)
  119. self._device = device
  120. self._running = False
  121. def stop(self):
  122. """
  123. Stops the running thread.
  124. """
  125. self._running = False
  126. def run(self):
  127. """
  128. The actual read process.
  129. """
  130. self._running = True
  131. while self._running:
  132. try:
  133. self._device.read_line(timeout=self.READ_TIMEOUT)
  134. except TimeoutError:
  135. pass
  136. except InvalidMessageError:
  137. pass
  138. except SSL.WantReadError:
  139. pass
  140. except CommError:
  141. self._device.close()
  142. except Exception:
  143. self._device.close()
  144. self._running = False
  145. raise
  146. class USBDevice(Device):
  147. """
  148. `AD2USB`_ device utilizing PyFTDI's interface.
  149. """
  150. # Constants
  151. PRODUCT_IDS = ((0x0403, 0x6001), (0x0403, 0x6015))
  152. """List of Vendor and Product IDs used to recognize `AD2USB`_ devices."""
  153. DEFAULT_VENDOR_ID = PRODUCT_IDS[0][0]
  154. """Default Vendor ID used to recognize `AD2USB`_ devices."""
  155. DEFAULT_PRODUCT_ID = PRODUCT_IDS[0][1]
  156. """Default Product ID used to recognize `AD2USB`_ devices."""
  157. # Deprecated constants
  158. FTDI_VENDOR_ID = DEFAULT_VENDOR_ID
  159. """DEPRECATED: Vendor ID used to recognize `AD2USB`_ devices."""
  160. FTDI_PRODUCT_ID = DEFAULT_PRODUCT_ID
  161. """DEPRECATED: Product ID used to recognize `AD2USB`_ devices."""
  162. BAUDRATE = 115200
  163. """Default baudrate for `AD2USB`_ devices."""
  164. __devices = []
  165. __detect_thread = None
  166. @classmethod
  167. def find_all(cls, vid=None, pid=None):
  168. """
  169. Returns all FTDI devices matching our vendor and product IDs.
  170. :returns: list of devices
  171. :raises: :py:class:`~alarmdecoder.util.CommError`
  172. """
  173. if not have_pyftdi:
  174. raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')
  175. cls.__devices = []
  176. query = cls.PRODUCT_IDS
  177. if vid and pid:
  178. query = [(vid, pid)]
  179. try:
  180. cls.__devices = Ftdi.find_all(query, nocache=True)
  181. except (usb.core.USBError, FtdiError) as err:
  182. raise CommError('Error enumerating AD2USB devices: {0}'.format(str(err)), err)
  183. return cls.__devices
  184. @classmethod
  185. def devices(cls):
  186. """
  187. Returns a cached list of `AD2USB`_ devices located on the system.
  188. :returns: cached list of devices found
  189. """
  190. return cls.__devices
  191. @classmethod
  192. def find(cls, device=None):
  193. """
  194. Factory method that returns the requested :py:class:`USBDevice` device, or the
  195. first device.
  196. :param device: Tuple describing the USB device to open, as returned
  197. by find_all().
  198. :type device: tuple
  199. :returns: :py:class:`USBDevice` object utilizing the specified device
  200. :raises: :py:class:`~alarmdecoder.util.NoDeviceError`
  201. """
  202. if not have_pyftdi:
  203. raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')
  204. cls.find_all()
  205. if len(cls.__devices) == 0:
  206. raise NoDeviceError('No AD2USB devices present.')
  207. if device is None:
  208. device = cls.__devices[0]
  209. vendor, product, sernum, ifcount, description = device
  210. return USBDevice(interface=sernum, vid=vendor, pid=product)
  211. @classmethod
  212. def start_detection(cls, on_attached=None, on_detached=None):
  213. """
  214. Starts the device detection thread.
  215. :param on_attached: function to be called when a device is attached **Callback definition:** *def callback(thread, device)*
  216. :type on_attached: function
  217. :param on_detached: function to be called when a device is detached **Callback definition:** *def callback(thread, device)*
  218. :type on_detached: function
  219. """
  220. if not have_pyftdi:
  221. raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')
  222. cls.__detect_thread = USBDevice.DetectThread(on_attached, on_detached)
  223. try:
  224. cls.find_all()
  225. except CommError:
  226. pass
  227. cls.__detect_thread.start()
  228. @classmethod
  229. def stop_detection(cls):
  230. """
  231. Stops the device detection thread.
  232. """
  233. if not have_pyftdi:
  234. raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')
  235. try:
  236. cls.__detect_thread.stop()
  237. except Exception:
  238. pass
  239. @property
  240. def interface(self):
  241. """
  242. Retrieves the interface used to connect to the device.
  243. :returns: the interface used to connect to the device
  244. """
  245. return self._interface
  246. @interface.setter
  247. def interface(self, value):
  248. """
  249. Sets the interface used to connect to the device.
  250. :param value: may specify either the serial number or the device index
  251. :type value: string or int
  252. """
  253. self._interface = value
  254. if isinstance(value, int):
  255. self._device_number = value
  256. else:
  257. self._serial_number = value
  258. @property
  259. def serial_number(self):
  260. """
  261. Retrieves the serial number of the device.
  262. :returns: serial number of the device
  263. """
  264. return self._serial_number
  265. @serial_number.setter
  266. def serial_number(self, value):
  267. """
  268. Sets the serial number of the device.
  269. :param value: serial number of the device
  270. :type value: string
  271. """
  272. self._serial_number = value
  273. @property
  274. def description(self):
  275. """
  276. Retrieves the description of the device.
  277. :returns: description of the device
  278. """
  279. return self._description
  280. @description.setter
  281. def description(self, value):
  282. """
  283. Sets the description of the device.
  284. :param value: description of the device
  285. :type value: string
  286. """
  287. self._description = value
  288. def __init__(self, interface=0, vid=None, pid=None):
  289. """
  290. Constructor
  291. :param interface: May specify either the serial number or the device
  292. index.
  293. :type interface: string or int
  294. """
  295. if not have_pyftdi:
  296. raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')
  297. Device.__init__(self)
  298. self._device = Ftdi()
  299. self._interface = 0
  300. self._device_number = 0
  301. self._serial_number = None
  302. self._vendor_id = USBDevice.DEFAULT_VENDOR_ID
  303. if vid:
  304. self._vendor_id = vid
  305. self._product_id = USBDevice.DEFAULT_PRODUCT_ID
  306. if pid:
  307. self._product_id = pid
  308. self._endpoint = 0
  309. self._description = None
  310. self.interface = interface
  311. def open(self, baudrate=BAUDRATE, no_reader_thread=False):
  312. """
  313. Opens the device.
  314. :param baudrate: baudrate to use
  315. :type baudrate: int
  316. :param no_reader_thread: whether or not to automatically start the
  317. reader thread.
  318. :type no_reader_thread: bool
  319. :raises: :py:class:`~alarmdecoder.util.NoDeviceError`
  320. """
  321. # Set up defaults
  322. if baudrate is None:
  323. baudrate = USBDevice.BAUDRATE
  324. self._read_thread = Device.ReadThread(self)
  325. # Open the device and start up the thread.
  326. try:
  327. self._device.open(self._vendor_id,
  328. self._product_id,
  329. self._endpoint,
  330. self._device_number,
  331. self._serial_number,
  332. self._description)
  333. self._device.set_baudrate(baudrate)
  334. if not self._serial_number:
  335. self._serial_number = self._get_serial_number()
  336. self._id = self._serial_number
  337. except (usb.core.USBError, FtdiError) as err:
  338. raise NoDeviceError('Error opening device: {0}'.format(str(err)), err)
  339. except KeyError as err:
  340. raise NoDeviceError('Unsupported device. ({0:04x}:{1:04x}) You probably need a newer version of pyftdi.'.format(err[0][0], err[0][1]))
  341. else:
  342. self._running = True
  343. self.on_open()
  344. if not no_reader_thread:
  345. self._read_thread.start()
  346. return self
  347. def close(self):
  348. """
  349. Closes the device.
  350. """
  351. try:
  352. Device.close(self)
  353. # HACK: Probably should fork pyftdi and make this call in .close()
  354. self._device.usb_dev.attach_kernel_driver(self._device_number)
  355. except Exception:
  356. pass
  357. def fileno(self):
  358. raise NotImplementedError('USB devices do not support fileno()')
  359. def write(self, data):
  360. """
  361. Writes data to the device.
  362. :param data: data to write
  363. :type data: string
  364. :raises: :py:class:`~alarmdecoder.util.CommError`
  365. """
  366. try:
  367. self._device.write_data(data)
  368. self.on_write(data=data)
  369. except FtdiError as err:
  370. raise CommError('Error writing to device: {0}'.format(str(err)), err)
  371. def read(self):
  372. """
  373. Reads a single character from the device.
  374. :returns: character read from the device
  375. :raises: :py:class:`~alarmdecoder.util.CommError`
  376. """
  377. ret = None
  378. try:
  379. ret = self._device.read_data(1)
  380. except (usb.core.USBError, FtdiError) as err:
  381. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  382. return ret
  383. def read_line(self, timeout=0.0, purge_buffer=False):
  384. """
  385. Reads a line from the device.
  386. :param timeout: read timeout
  387. :type timeout: float
  388. :param purge_buffer: Indicates whether to purge the buffer prior to
  389. reading.
  390. :type purge_buffer: bool
  391. :returns: line that was read
  392. :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError`
  393. """
  394. def timeout_event():
  395. """Handles read timeout event"""
  396. timeout_event.reading = False
  397. timeout_event.reading = True
  398. if purge_buffer:
  399. self._buffer = b''
  400. got_line, ret = False, None
  401. timer = threading.Timer(timeout, timeout_event)
  402. if timeout > 0:
  403. timer.start()
  404. try:
  405. while timeout_event.reading:
  406. buf = self._device.read_data(1)
  407. if buf != b'':
  408. self._buffer += buf
  409. if buf == b"\n":
  410. self._buffer = self._buffer.rstrip(b"\r\n")
  411. if len(self._buffer) > 0:
  412. got_line = True
  413. break
  414. else:
  415. time.sleep(0.01)
  416. except (usb.core.USBError, FtdiError) as err:
  417. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  418. else:
  419. if got_line:
  420. ret, self._buffer = self._buffer, b''
  421. self.on_read(data=ret)
  422. else:
  423. raise TimeoutError('Timeout while waiting for line terminator.')
  424. finally:
  425. timer.cancel()
  426. return ret
  427. def _get_serial_number(self):
  428. """
  429. Retrieves the FTDI device serial number.
  430. :returns: string containing the device serial number
  431. """
  432. return usb.util.get_string(self._device.usb_dev, 64, self._device.usb_dev.iSerialNumber)
  433. class DetectThread(threading.Thread):
  434. """
  435. Thread that handles detection of added/removed devices.
  436. """
  437. on_attached = event.Event("This event is called when an `AD2USB`_ device has been detected.\n\n**Callback definition:** def callback(thread, device*")
  438. on_detached = event.Event("This event is called when an `AD2USB`_ device has been removed.\n\n**Callback definition:** def callback(thread, device*")
  439. def __init__(self, on_attached=None, on_detached=None):
  440. """
  441. Constructor
  442. :param on_attached: Function to call when a device is attached **Callback definition:** *def callback(thread, device)*
  443. :type on_attached: function
  444. :param on_detached: Function to call when a device is detached **Callback definition:** *def callback(thread, device)*
  445. :type on_detached: function
  446. """
  447. threading.Thread.__init__(self)
  448. if on_attached:
  449. self.on_attached += on_attached
  450. if on_detached:
  451. self.on_detached += on_detached
  452. self._running = False
  453. def stop(self):
  454. """
  455. Stops the thread.
  456. """
  457. self._running = False
  458. def run(self):
  459. """
  460. The actual detection process.
  461. """
  462. self._running = True
  463. last_devices = set()
  464. while self._running:
  465. try:
  466. current_devices = set(USBDevice.find_all())
  467. for dev in current_devices.difference(last_devices):
  468. self.on_attached(device=dev)
  469. for dev in last_devices.difference(current_devices):
  470. self.on_detached(device=dev)
  471. last_devices = current_devices
  472. except CommError:
  473. pass
  474. time.sleep(0.25)
  475. class SerialDevice(Device):
  476. """
  477. `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_ device utilizing the PySerial interface.
  478. """
  479. # Constants
  480. BAUDRATE = 19200
  481. """Default baudrate for Serial devices."""
  482. @staticmethod
  483. def find_all(pattern=None):
  484. """
  485. Returns all serial ports present.
  486. :param pattern: pattern to search for when retrieving serial ports
  487. :type pattern: string
  488. :returns: list of devices
  489. :raises: :py:class:`~alarmdecoder.util.CommError`
  490. """
  491. devices = []
  492. try:
  493. if pattern:
  494. devices = serial.tools.list_ports.grep(pattern)
  495. else:
  496. devices = serial.tools.list_ports.comports()
  497. except serial.SerialException as err:
  498. raise CommError('Error enumerating serial devices: {0}'.format(str(err)), err)
  499. return devices
  500. @property
  501. def interface(self):
  502. """
  503. Retrieves the interface used to connect to the device.
  504. :returns: interface used to connect to the device
  505. """
  506. return self._port
  507. @interface.setter
  508. def interface(self, value):
  509. """
  510. Sets the interface used to connect to the device.
  511. :param value: name of the serial device
  512. :type value: string
  513. """
  514. self._port = value
  515. def __init__(self, interface=None):
  516. """
  517. Constructor
  518. :param interface: device to open
  519. :type interface: string
  520. """
  521. Device.__init__(self)
  522. self._port = interface
  523. self._id = interface
  524. # Timeout = non-blocking to match pyftdi.
  525. self._device = serial.Serial(timeout=0, writeTimeout=0)
  526. def open(self, baudrate=BAUDRATE, no_reader_thread=False):
  527. """
  528. Opens the device.
  529. :param baudrate: baudrate to use with the device
  530. :type baudrate: int
  531. :param no_reader_thread: whether or not to automatically start the
  532. reader thread.
  533. :type no_reader_thread: bool
  534. :raises: :py:class:`~alarmdecoder.util.NoDeviceError`
  535. """
  536. # Set up the defaults
  537. if baudrate is None:
  538. baudrate = SerialDevice.BAUDRATE
  539. if self._port is None:
  540. raise NoDeviceError('No device interface specified.')
  541. self._read_thread = Device.ReadThread(self)
  542. # Open the device and start up the reader thread.
  543. try:
  544. self._device.port = self._port
  545. self._device.open()
  546. # NOTE: Setting the baudrate before opening the
  547. # port caused issues with Moschip 7840/7820
  548. # USB Serial Driver converter. (mos7840)
  549. #
  550. # Moving it to this point seems to resolve
  551. # all issues with it.
  552. self._device.baudrate = baudrate
  553. except (serial.SerialException, ValueError, OSError) as err:
  554. raise NoDeviceError('Error opening device on {0}.'.format(self._port), err)
  555. else:
  556. self._running = True
  557. self.on_open()
  558. if not no_reader_thread:
  559. self._read_thread.start()
  560. return self
  561. def close(self):
  562. """
  563. Closes the device.
  564. """
  565. try:
  566. Device.close(self)
  567. except Exception:
  568. pass
  569. def fileno(self):
  570. return self._device.fileno()
  571. def write(self, data):
  572. """
  573. Writes data to the device.
  574. :param data: data to write
  575. :type data: string
  576. :raises: py:class:`~alarmdecoder.util.CommError`
  577. """
  578. try:
  579. self._device.write(data)
  580. except serial.SerialTimeoutException:
  581. pass
  582. except serial.SerialException as err:
  583. raise CommError('Error writing to device.', err)
  584. else:
  585. self.on_write(data=data)
  586. def read(self):
  587. """
  588. Reads a single character from the device.
  589. :returns: character read from the device
  590. :raises: :py:class:`~alarmdecoder.util.CommError`
  591. """
  592. ret = None
  593. try:
  594. ret = self._device.read(1)
  595. except serial.SerialException as err:
  596. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  597. return ret
  598. def read_line(self, timeout=0.0, purge_buffer=False):
  599. """
  600. Reads a line from the device.
  601. :param timeout: read timeout
  602. :type timeout: float
  603. :param purge_buffer: Indicates whether to purge the buffer prior to
  604. reading.
  605. :type purge_buffer: bool
  606. :returns: line that was read
  607. :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError`
  608. """
  609. def timeout_event():
  610. """Handles read timeout event"""
  611. timeout_event.reading = False
  612. timeout_event.reading = True
  613. if purge_buffer:
  614. self._buffer = b''
  615. got_line, ret = False, None
  616. timer = threading.Timer(timeout, timeout_event)
  617. if timeout > 0:
  618. timer.start()
  619. try:
  620. while timeout_event.reading:
  621. buf = self._device.read(1)
  622. # NOTE: AD2SERIAL apparently sends down \xFF on boot.
  623. if buf != b'' and buf != b"\xff":
  624. self._buffer += buf
  625. if buf == b"\n":
  626. self._buffer = self._buffer.rstrip(b"\r\n")
  627. if len(self._buffer) > 0:
  628. got_line = True
  629. break
  630. else:
  631. time.sleep(0.01)
  632. except (OSError, serial.SerialException) as err:
  633. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  634. else:
  635. if got_line:
  636. ret, self._buffer = self._buffer, b''
  637. self.on_read(data=ret)
  638. else:
  639. raise TimeoutError('Timeout while waiting for line terminator.')
  640. finally:
  641. timer.cancel()
  642. return ret
  643. class SocketDevice(Device):
  644. """
  645. Device that supports communication with an `AlarmDecoder`_ (AD2) that is
  646. exposed via `ser2sock`_ or another Serial to IP interface.
  647. """
  648. @property
  649. def interface(self):
  650. """
  651. Retrieves the interface used to connect to the device.
  652. :returns: interface used to connect to the device
  653. """
  654. return (self._host, self._port)
  655. @interface.setter
  656. def interface(self, value):
  657. """
  658. Sets the interface used to connect to the device.
  659. :param value: Tuple containing the host and port to use
  660. :type value: tuple
  661. """
  662. self._host, self._port = value
  663. @property
  664. def ssl(self):
  665. """
  666. Retrieves whether or not the device is using SSL.
  667. :returns: whether or not the device is using SSL
  668. """
  669. return self._use_ssl
  670. @ssl.setter
  671. def ssl(self, value):
  672. """
  673. Sets whether or not SSL communication is in use.
  674. :param value: Whether or not SSL communication is in use
  675. :type value: bool
  676. """
  677. self._use_ssl = value
  678. @property
  679. def ssl_certificate(self):
  680. """
  681. Retrieves the SSL client certificate path used for authentication.
  682. :returns: path to the certificate path or :py:class:`OpenSSL.crypto.X509`
  683. """
  684. return self._ssl_certificate
  685. @ssl_certificate.setter
  686. def ssl_certificate(self, value):
  687. """
  688. Sets the SSL client certificate to use for authentication.
  689. :param value: path to the SSL certificate or :py:class:`OpenSSL.crypto.X509`
  690. :type value: string or :py:class:`OpenSSL.crypto.X509`
  691. """
  692. self._ssl_certificate = value
  693. @property
  694. def ssl_key(self):
  695. """
  696. Retrieves the SSL client certificate key used for authentication.
  697. :returns: jpath to the SSL key or :py:class:`OpenSSL.crypto.PKey`
  698. """
  699. return self._ssl_key
  700. @ssl_key.setter
  701. def ssl_key(self, value):
  702. """
  703. Sets the SSL client certificate key to use for authentication.
  704. :param value: path to the SSL key or :py:class:`OpenSSL.crypto.PKey`
  705. :type value: string or :py:class:`OpenSSL.crypto.PKey`
  706. """
  707. self._ssl_key = value
  708. @property
  709. def ssl_ca(self):
  710. """
  711. Retrieves the SSL Certificate Authority certificate used for
  712. authentication.
  713. :returns: path to the CA certificate or :py:class:`OpenSSL.crypto.X509`
  714. """
  715. return self._ssl_ca
  716. @ssl_ca.setter
  717. def ssl_ca(self, value):
  718. """
  719. Sets the SSL Certificate Authority certificate used for authentication.
  720. :param value: path to the SSL CA certificate or :py:class:`OpenSSL.crypto.X509`
  721. :type value: string or :py:class:`OpenSSL.crypto.X509`
  722. """
  723. self._ssl_ca = value
  724. def __init__(self, interface=("localhost", 10000)):
  725. """
  726. Constructor
  727. :param interface: Tuple containing the hostname and port of our target
  728. :type interface: tuple
  729. """
  730. Device.__init__(self)
  731. self._host, self._port = interface
  732. self._use_ssl = False
  733. self._ssl_certificate = None
  734. self._ssl_key = None
  735. self._ssl_ca = None
  736. def open(self, baudrate=None, no_reader_thread=False):
  737. """
  738. Opens the device.
  739. :param baudrate: baudrate to use
  740. :type baudrate: int
  741. :param no_reader_thread: whether or not to automatically open the reader
  742. thread.
  743. :type no_reader_thread: bool
  744. :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.CommError`
  745. """
  746. try:
  747. self._read_thread = Device.ReadThread(self)
  748. self._device = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  749. if self._use_ssl:
  750. self._init_ssl()
  751. self._device.connect((self._host, self._port))
  752. #self._device.setblocking(1)
  753. if self._use_ssl:
  754. while True:
  755. try:
  756. self._device.do_handshake()
  757. break
  758. except SSL.WantReadError:
  759. pass
  760. self._id = '{0}:{1}'.format(self._host, self._port)
  761. except socket.error as err:
  762. raise NoDeviceError('Error opening device at {0}:{1}'.format(self._host, self._port), err)
  763. else:
  764. self._running = True
  765. self.on_open()
  766. if not no_reader_thread:
  767. self._read_thread.start()
  768. return self
  769. def close(self):
  770. """
  771. Closes the device.
  772. """
  773. try:
  774. # TODO: Find a way to speed up this shutdown.
  775. if self.ssl:
  776. self._device.shutdown()
  777. else:
  778. # Make sure that it closes immediately.
  779. self._device.shutdown(socket.SHUT_RDWR)
  780. except Exception:
  781. pass
  782. Device.close(self)
  783. def fileno(self):
  784. return self._device.fileno()
  785. def write(self, data):
  786. """
  787. Writes data to the device.
  788. :param data: data to write
  789. :type data: string
  790. :returns: number of bytes sent
  791. :raises: :py:class:`~alarmdecoder.util.CommError`
  792. """
  793. data_sent = None
  794. try:
  795. data_sent = self._device.send(data)
  796. if data_sent == 0:
  797. raise CommError('Error writing to device.')
  798. self.on_write(data=data)
  799. except (SSL.Error, socket.error) as err:
  800. raise CommError('Error writing to device.', err)
  801. return data_sent
  802. def read(self):
  803. """
  804. Reads a single character from the device.
  805. :returns: character read from the device
  806. :raises: :py:class:`~alarmdecoder.util.CommError`
  807. """
  808. data = None
  809. try:
  810. data = self._device.recv(1)
  811. except socket.error as err:
  812. raise CommError('Error while reading from device: {0}'.format(str(err)), err)
  813. return data
  814. def read_line(self, timeout=0.0, purge_buffer=False):
  815. """
  816. Reads a line from the device.
  817. :param timeout: read timeout
  818. :type timeout: float
  819. :param purge_buffer: Indicates whether to purge the buffer prior to
  820. reading.
  821. :type purge_buffer: bool
  822. :returns: line that was read
  823. :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError`
  824. """
  825. def timeout_event():
  826. """Handles read timeout event"""
  827. timeout_event.reading = False
  828. timeout_event.reading = True
  829. if purge_buffer:
  830. self._buffer = b''
  831. got_line, ret = False, None
  832. timer = threading.Timer(timeout, timeout_event)
  833. if timeout > 0:
  834. timer.start()
  835. try:
  836. while timeout_event.reading:
  837. buf = self._device.recv(1)
  838. if buf != b'':
  839. self._buffer += buf
  840. if buf == b"\n":
  841. self._buffer = self._buffer.rstrip(b"\r\n")
  842. if len(self._buffer) > 0:
  843. got_line = True
  844. break
  845. else:
  846. time.sleep(0.01)
  847. except socket.error as err:
  848. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  849. except SSL.SysCallError as err:
  850. errno, msg = err
  851. raise CommError('SSL error while reading from device: {0} ({1})'.format(msg, errno))
  852. except Exception:
  853. raise
  854. else:
  855. if got_line:
  856. ret, self._buffer = self._buffer, b''
  857. self.on_read(data=ret)
  858. else:
  859. raise TimeoutError('Timeout while waiting for line terminator.')
  860. finally:
  861. timer.cancel()
  862. return ret
  863. def _init_ssl(self):
  864. """
  865. Initializes our device as an SSL connection.
  866. :raises: :py:class:`~alarmdecoder.util.CommError`
  867. """
  868. if not have_openssl:
  869. raise ImportError('SSL sockets have been disabled due to missing requirement: pyopenssl.')
  870. try:
  871. ctx = SSL.Context(SSL.TLSv1_METHOD)
  872. if isinstance(self.ssl_key, crypto.PKey):
  873. ctx.use_privatekey(self.ssl_key)
  874. else:
  875. ctx.use_privatekey_file(self.ssl_key)
  876. if isinstance(self.ssl_certificate, crypto.X509):
  877. ctx.use_certificate(self.ssl_certificate)
  878. else:
  879. ctx.use_certificate_file(self.ssl_certificate)
  880. if isinstance(self.ssl_ca, crypto.X509):
  881. store = ctx.get_cert_store()
  882. store.add_cert(self.ssl_ca)
  883. else:
  884. ctx.load_verify_locations(self.ssl_ca, None)
  885. ctx.set_verify(SSL.VERIFY_PEER, self._verify_ssl_callback)
  886. self._device = SSL.Connection(ctx, self._device)
  887. except SSL.Error as err:
  888. raise CommError('Error setting up SSL connection.', err)
  889. def _verify_ssl_callback(self, connection, x509, errnum, errdepth, ok):
  890. """
  891. SSL verification callback.
  892. """
  893. return ok