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.

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