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.

922 lines
24 KiB

  1. """
  2. Contains different types of devices belonging to the AD2 family.
  3. .. moduleauthor:: Scott Petersen <scott@nutech.com>
  4. """
  5. import usb.core, usb.util
  6. import time
  7. import threading
  8. import serial, serial.tools.list_ports
  9. import socket
  10. from OpenSSL import SSL, crypto
  11. from pyftdi.pyftdi.ftdi import *
  12. from pyftdi.pyftdi.usbtools import *
  13. from .util import CommError, TimeoutError, NoDeviceError
  14. from .event import event
  15. class Device(object):
  16. """
  17. Generic parent device to all AD2 products.
  18. """
  19. # Generic device events
  20. on_open = event.Event('Called when the device has been opened')
  21. on_close = event.Event('Called when the device has been closed')
  22. on_read = event.Event('Called when a line has been read from the device')
  23. on_write = event.Event('Called when data has been written to the device')
  24. def __init__(self):
  25. """
  26. Constructor
  27. """
  28. self._id = ''
  29. self._buffer = ''
  30. self._device = None
  31. self._running = False
  32. self._read_thread = Device.ReadThread(self)
  33. @property
  34. def id(self):
  35. """
  36. Retrieve the device ID.
  37. :returns: The identification string for the device.
  38. """
  39. return self._id
  40. @id.setter
  41. def id(self, value):
  42. """
  43. Sets the device ID.
  44. :param value: The device identification.
  45. :type value: str
  46. """
  47. self._id = value
  48. def is_reader_alive(self):
  49. """
  50. Indicates whether or not the reader thread is alive.
  51. :returns: Whether or not the reader thread is alive.
  52. """
  53. return self._read_thread.is_alive()
  54. def stop_reader(self):
  55. """
  56. Stops the reader thread.
  57. """
  58. self._read_thread.stop()
  59. def close(self):
  60. """
  61. Closes the device.
  62. """
  63. try:
  64. self._running = False
  65. self._read_thread.stop()
  66. self._device.close()
  67. except:
  68. pass
  69. self.on_close()
  70. class ReadThread(threading.Thread):
  71. """
  72. Reader thread which processes messages from the device.
  73. """
  74. READ_TIMEOUT = 10
  75. """Timeout for the reader thread."""
  76. def __init__(self, device):
  77. """
  78. Constructor
  79. :param device: The device used by the reader thread.
  80. :type device: devices.Device
  81. """
  82. threading.Thread.__init__(self)
  83. self._device = device
  84. self._running = False
  85. def stop(self):
  86. """
  87. Stops the running thread.
  88. """
  89. self._running = False
  90. def run(self):
  91. """
  92. The actual read process.
  93. """
  94. self._running = True
  95. while self._running:
  96. try:
  97. self._device.read_line(timeout=self.READ_TIMEOUT)
  98. except TimeoutError, err:
  99. pass
  100. except Exception, err:
  101. self._running = False
  102. #raise err
  103. time.sleep(0.01)
  104. class USBDevice(Device):
  105. """
  106. AD2USB device exposed with PyFTDI's interface.
  107. """
  108. # Constants
  109. FTDI_VENDOR_ID = 0x0403
  110. """Vendor ID used to recognize AD2USB devices."""
  111. FTDI_PRODUCT_ID = 0x6001
  112. """Product ID used to recognize AD2USB devices."""
  113. BAUDRATE = 115200
  114. """Default baudrate for AD2USB devices."""
  115. @staticmethod
  116. def find_all():
  117. """
  118. Returns all FTDI devices matching our vendor and product IDs.
  119. :returns: list of devices
  120. :raises: CommError
  121. """
  122. devices = []
  123. try:
  124. devices = Ftdi.find_all([(USBDevice.FTDI_VENDOR_ID, USBDevice.FTDI_PRODUCT_ID)], nocache=True)
  125. except (usb.core.USBError, FtdiError), err:
  126. raise CommError('Error enumerating AD2USB devices: {0}'.format(str(err)), err)
  127. return devices
  128. @property
  129. def interface(self):
  130. """
  131. Retrieves the interface used to connect to the device.
  132. :returns: the interface used to connect to the device.
  133. """
  134. return self._interface
  135. @interface.setter
  136. def interface(self, value):
  137. """
  138. Sets the interface used to connect to the device.
  139. :param value: May specify either the serial number or the device index.
  140. :type value: str or int
  141. """
  142. self._interface = value
  143. if isinstance(value, int):
  144. self._device_number = value
  145. else:
  146. self._serial_number = value
  147. @property
  148. def serial_number(self):
  149. """
  150. Retrieves the serial number of the device.
  151. :returns: The serial number of the device.
  152. """
  153. return self._serial_number
  154. @serial_number.setter
  155. def serial_number(self, value):
  156. """
  157. Sets the serial number of the device.
  158. :param value: The serial number of the device.
  159. :type value: string
  160. """
  161. self._serial_number = value
  162. @property
  163. def description(self):
  164. """
  165. Retrieves the description of the device.
  166. :returns: The description of the device.
  167. """
  168. return self._description
  169. @description.setter
  170. def description(self, value):
  171. """
  172. Sets the description of the device.
  173. :param value: The description of the device.
  174. :type value: string
  175. """
  176. self._description = value
  177. def __init__(self, interface=0):
  178. """
  179. Constructor
  180. :param interface: May specify either the serial number or the device index.
  181. :type interface: str or int
  182. """
  183. Device.__init__(self)
  184. self._device = Ftdi()
  185. self._device_number = 0
  186. self._serial_number = None
  187. self.interface = interface
  188. self._vendor_id = USBDevice.FTDI_VENDOR_ID
  189. self._product_id = USBDevice.FTDI_PRODUCT_ID
  190. self._endpoint = 0
  191. self._description = None
  192. def open(self, baudrate=BAUDRATE, no_reader_thread=False):
  193. """
  194. Opens the device.
  195. :param baudrate: The baudrate to use.
  196. :type baudrate: int
  197. :param no_reader_thread: Whether or not to automatically start the reader thread.
  198. :type no_reader_thread: bool
  199. :raises: NoDeviceError
  200. """
  201. # Set up defaults
  202. if baudrate is None:
  203. baudrate = USBDevice.BAUDRATE
  204. # Open the device and start up the thread.
  205. try:
  206. self._device.open(self._vendor_id,
  207. self._product_id,
  208. self._endpoint,
  209. self._device_number,
  210. self._serial_number,
  211. self._description)
  212. self._device.set_baudrate(baudrate)
  213. self._id = 'USB {0}:{1}'.format(self._device.usb_dev.bus, self._device.usb_dev.address)
  214. except (usb.core.USBError, FtdiError), err:
  215. raise NoDeviceError('Error opening device: {0}'.format(str(err)), err)
  216. else:
  217. self._running = True
  218. if not no_reader_thread:
  219. self._read_thread.start()
  220. self.on_open()
  221. def close(self):
  222. """
  223. Closes the device.
  224. """
  225. try:
  226. Device.close(self)
  227. # HACK: Probably should fork pyftdi and make this call in .close().
  228. self._device.usb_dev.attach_kernel_driver(self._device_number)
  229. except:
  230. pass
  231. def write(self, data):
  232. """
  233. Writes data to the device.
  234. :param data: Data to write
  235. :type data: str
  236. :raises: CommError
  237. """
  238. try:
  239. self._device.write_data(data)
  240. self.on_write(data)
  241. except FtdiError, err:
  242. raise CommError('Error writing to device: {0}'.format(str(err)), err)
  243. def read(self):
  244. """
  245. Reads a single character from the device.
  246. :returns: The character read from the device.
  247. :raises: CommError
  248. """
  249. ret = None
  250. try:
  251. ret = self._device.read_data(1)
  252. except (usb.core.USBError, FtdiError), err:
  253. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  254. return ret
  255. def read_line(self, timeout=0.0, purge_buffer=False):
  256. """
  257. Reads a line from the device.
  258. :param timeout: Read timeout
  259. :type timeout: float
  260. :param purge_buffer: Indicates whether to purge the buffer prior to reading.
  261. :type purge_buffer: bool
  262. :returns: The line that was read.
  263. :raises: CommError, TimeoutError
  264. """
  265. if purge_buffer:
  266. self._buffer = ''
  267. def timeout_event():
  268. timeout_event.reading = False
  269. timeout_event.reading = True
  270. got_line = False
  271. ret = None
  272. timer = None
  273. if timeout > 0:
  274. timer = threading.Timer(timeout, timeout_event)
  275. timer.start()
  276. try:
  277. while timeout_event.reading:
  278. buf = self._device.read_data(1)
  279. if buf != '':
  280. self._buffer += buf
  281. if buf == "\n":
  282. if len(self._buffer) > 1:
  283. if self._buffer[-2] == "\r":
  284. self._buffer = self._buffer[:-2]
  285. # ignore if we just got \r\n with nothing else in the buffer.
  286. if len(self._buffer) != 0:
  287. got_line = True
  288. break
  289. else:
  290. self._buffer = self._buffer[:-1]
  291. except (usb.core.USBError, FtdiError), err:
  292. if timer:
  293. timer.cancel()
  294. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  295. else:
  296. if got_line:
  297. ret = self._buffer
  298. self._buffer = ''
  299. self.on_read(ret)
  300. if timer:
  301. if timer.is_alive():
  302. timer.cancel()
  303. else:
  304. raise TimeoutError('Timeout while waiting for line terminator.')
  305. return ret
  306. class SerialDevice(Device):
  307. """
  308. AD2USB or AD2SERIAL device exposed with the pyserial interface.
  309. """
  310. # Constants
  311. BAUDRATE = 19200
  312. """Default baudrate for Serial devices."""
  313. @staticmethod
  314. def find_all(pattern=None):
  315. """
  316. Returns all serial ports present.
  317. :param pattern: Pattern to search for when retrieving serial ports.
  318. :type pattern: str
  319. :returns: list of devices
  320. :raises: CommError
  321. """
  322. devices = []
  323. try:
  324. if pattern:
  325. devices = serial.tools.list_ports.grep(pattern)
  326. else:
  327. devices = serial.tools.list_ports.comports()
  328. except SerialException, err:
  329. raise CommError('Error enumerating serial devices: {0}'.format(str(err)), err)
  330. return devices
  331. @property
  332. def interface(self):
  333. """
  334. Retrieves the interface used to connect to the device.
  335. :returns: the interface used to connect to the device.
  336. """
  337. return self._port
  338. @interface.setter
  339. def interface(self, value):
  340. """
  341. Sets the interface used to connect to the device.
  342. :param value: The name of the serial device.
  343. :type value: string
  344. """
  345. self._port = value
  346. def __init__(self, interface=None):
  347. """
  348. Constructor
  349. :param interface: The device to open.
  350. :type interface: str
  351. """
  352. Device.__init__(self)
  353. self._port = interface
  354. self._id = interface
  355. self._device = serial.Serial(timeout=0, writeTimeout=0) # Timeout = non-blocking to match pyftdi.
  356. def open(self, baudrate=BAUDRATE, no_reader_thread=False):
  357. """
  358. Opens the device.
  359. :param baudrate: The baudrate to use with the device.
  360. :type baudrate: int
  361. :param no_reader_thread: Whether or not to automatically start the reader thread.
  362. :type no_reader_thread: bool
  363. :raises: NoDeviceError
  364. """
  365. # Set up the defaults
  366. if baudrate is None:
  367. baudrate = SerialDevice.BAUDRATE
  368. if self._port is None:
  369. raise NoDeviceError('No device interface specified.')
  370. self._device.port = self._port
  371. # Open the device and start up the reader thread.
  372. try:
  373. self._device.open()
  374. self._device.baudrate = baudrate # NOTE: Setting the baudrate before opening the
  375. # port caused issues with Moschip 7840/7820
  376. # USB Serial Driver converter. (mos7840)
  377. #
  378. # Moving it to this point seems to resolve
  379. # all issues with it.
  380. except (serial.SerialException, ValueError), err:
  381. raise NoDeviceError('Error opening device on port {0}.'.format(self._port), err)
  382. else:
  383. self._running = True
  384. self.on_open()
  385. if not no_reader_thread:
  386. self._read_thread.start()
  387. def close(self):
  388. """
  389. Closes the device.
  390. """
  391. try:
  392. Device.close(self)
  393. except:
  394. pass
  395. def write(self, data):
  396. """
  397. Writes data to the device.
  398. :param data: The data to write.
  399. :type data: str
  400. :raises: CommError
  401. """
  402. try:
  403. self._device.write(data)
  404. except serial.SerialTimeoutException, err:
  405. pass
  406. except serial.SerialException, err:
  407. raise CommError('Error writing to device.', err)
  408. else:
  409. self.on_write(data)
  410. def read(self):
  411. """
  412. Reads a single character from the device.
  413. :returns: The character read from the device.
  414. :raises: CommError
  415. """
  416. ret = None
  417. try:
  418. ret = self._device.read(1)
  419. except serial.SerialException, err:
  420. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  421. return ret
  422. def read_line(self, timeout=0.0, purge_buffer=False):
  423. """
  424. Reads a line from the device.
  425. :param timeout: The read timeout.
  426. :type timeout: float
  427. :param purge_buffer: Indicates whether to purge the buffer prior to reading.
  428. :type purge_buffer: bool
  429. :returns: The line read.
  430. :raises: CommError, TimeoutError
  431. """
  432. def timeout_event():
  433. timeout_event.reading = False
  434. timeout_event.reading = True
  435. got_line = False
  436. ret = None
  437. timer = None
  438. if timeout > 0:
  439. timer = threading.Timer(timeout, timeout_event)
  440. timer.start()
  441. try:
  442. while timeout_event.reading:
  443. buf = self._device.read(1)
  444. if buf != '' and buf != "\xff": # AD2SERIAL specifically apparently sends down \xFF on boot.
  445. self._buffer += buf
  446. if buf == "\n":
  447. if len(self._buffer) > 1:
  448. if self._buffer[-2] == "\r":
  449. self._buffer = self._buffer[:-2]
  450. # ignore if we just got \r\n with nothing else in the buffer.
  451. if len(self._buffer) != 0:
  452. got_line = True
  453. break
  454. else:
  455. self._buffer = self._buffer[:-1]
  456. except (OSError, serial.SerialException), err:
  457. if timer:
  458. timer.cancel()
  459. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  460. else:
  461. if got_line:
  462. ret = self._buffer
  463. self._buffer = ''
  464. self.on_read(ret)
  465. if timer:
  466. if timer.is_alive():
  467. timer.cancel()
  468. else:
  469. raise TimeoutError('Timeout while waiting for line terminator.')
  470. return ret
  471. class SocketDevice(Device):
  472. """
  473. Device that supports communication with an AD2 that is exposed via ser2sock or another
  474. Serial to IP interface.
  475. """
  476. @property
  477. def interface(self):
  478. """
  479. Retrieves the interface used to connect to the device.
  480. :returns: the interface used to connect to the device.
  481. """
  482. return (self._host, self._port)
  483. @interface.setter
  484. def interface(self, value):
  485. """
  486. Sets the interface used to connect to the device.
  487. :param value: Tuple containing the host and port to use.
  488. :type value: tuple
  489. """
  490. self._host = value[0]
  491. self._port = value[1]
  492. @property
  493. def ssl(self):
  494. """
  495. Retrieves whether or not the device is using SSL.
  496. :returns: Whether or not the device is using SSL.
  497. """
  498. return self._use_ssl
  499. @ssl.setter
  500. def ssl(self, value):
  501. """
  502. Sets whether or not SSL communication is in use.
  503. :param value: Whether or not SSL communication is in use.
  504. :type value: bool
  505. """
  506. self._use_ssl = value
  507. @property
  508. def ssl_certificate(self):
  509. """
  510. Retrieves the SSL client certificate path used for authentication.
  511. :returns: The certificate path
  512. """
  513. return self._ssl_certificate
  514. @ssl_certificate.setter
  515. def ssl_certificate(self, value):
  516. """
  517. Sets the SSL client certificate to use for authentication.
  518. :param value: The path to the SSL certificate.
  519. :type value: str
  520. """
  521. self._ssl_certificate = value
  522. @property
  523. def ssl_key(self):
  524. """
  525. Retrieves the SSL client certificate key used for authentication.
  526. :returns: The key path
  527. """
  528. return self._ssl_key
  529. @ssl_key.setter
  530. def ssl_key(self, value):
  531. """
  532. Sets the SSL client certificate key to use for authentication.
  533. :param value: The path to the SSL key.
  534. :type value: str
  535. """
  536. self._ssl_key = value
  537. @property
  538. def ssl_ca(self):
  539. """
  540. Retrieves the SSL Certificate Authority certificate used for authentication.
  541. :returns: The CA path
  542. """
  543. return self._ssl_ca
  544. @ssl_ca.setter
  545. def ssl_ca(self, value):
  546. """
  547. Sets the SSL Certificate Authority certificate used for authentication.
  548. :param value: The path to the SSL CA certificate.
  549. :type value: str
  550. """
  551. self._ssl_ca = value
  552. def __init__(self, interface=("localhost", 10000)):
  553. """
  554. Constructor
  555. :param interface: Tuple containing the hostname and port of our target.
  556. :type interface: tuple
  557. """
  558. Device.__init__(self)
  559. self._host, self._port = interface
  560. self._use_ssl = False
  561. self._ssl_certificate = None
  562. self._ssl_key = None
  563. self._ssl_ca = None
  564. def open(self, baudrate=None, no_reader_thread=False):
  565. """
  566. Opens the device.
  567. :param baudrate: The baudrate to use
  568. :type baudrate: int
  569. :param no_reader_thread: Whether or not to automatically open the reader thread.
  570. :type no_reader_thread: bool
  571. :raises: NoDeviceError, CommError
  572. """
  573. try:
  574. self._device = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  575. if self._use_ssl:
  576. self._init_ssl()
  577. self._device.connect((self._host, self._port))
  578. self._id = '{0}:{1}'.format(self._host, self._port)
  579. except socket.error, err:
  580. raise NoDeviceError('Error opening device at {0}:{1}'.format(self._host, self._port), err)
  581. else:
  582. self._running = True
  583. self.on_open()
  584. if not no_reader_thread:
  585. self._read_thread.start()
  586. def close(self):
  587. """
  588. Closes the device.
  589. """
  590. try:
  591. if self.ssl:
  592. self._device.shutdown()
  593. else:
  594. self._device.shutdown(socket.SHUT_RDWR) # Make sure that it closes immediately.
  595. Device.close(self)
  596. except Exception, ex:
  597. pass
  598. def write(self, data):
  599. """
  600. Writes data to the device.
  601. :param data: The data to write.
  602. :type data: str
  603. :returns: The number of bytes sent.
  604. :raises: CommError
  605. """
  606. data_sent = None
  607. try:
  608. data_sent = self._device.send(data)
  609. if data_sent == 0:
  610. raise CommError('Error writing to device.')
  611. self.on_write(data)
  612. except (SSL.Error, socket.error), err:
  613. raise CommError('Error writing to device.', err)
  614. return data_sent
  615. def read(self):
  616. """
  617. Reads a single character from the device.
  618. :returns: The character read from the device.
  619. :raises: CommError
  620. """
  621. data = None
  622. try:
  623. data = self._device.recv(1)
  624. except socket.error, err:
  625. raise CommError('Error while reading from device: {0}'.format(str(err)), err)
  626. return data
  627. def read_line(self, timeout=0.0, purge_buffer=False):
  628. """
  629. Reads a line from the device.
  630. :param timeout: The read timeout.
  631. :type timeout: float
  632. :param purge_buffer: Indicates whether to purge the buffer prior to reading.
  633. :type purge_buffer: bool
  634. :returns: The line read from the device.
  635. :raises: CommError, TimeoutError
  636. """
  637. if purge_buffer:
  638. self._buffer = ''
  639. def timeout_event():
  640. timeout_event.reading = False
  641. timeout_event.reading = True
  642. got_line = False
  643. ret = None
  644. timer = None
  645. if timeout > 0:
  646. timer = threading.Timer(timeout, timeout_event)
  647. timer.start()
  648. try:
  649. while timeout_event.reading:
  650. buf = self._device.recv(1)
  651. if buf != '':
  652. self._buffer += buf
  653. if buf == "\n":
  654. if len(self._buffer) > 1:
  655. if self._buffer[-2] == "\r":
  656. self._buffer = self._buffer[:-2]
  657. # ignore if we just got \r\n with nothing else in the buffer.
  658. if len(self._buffer) != 0:
  659. got_line = True
  660. break
  661. else:
  662. self._buffer = self._buffer[:-1]
  663. except socket.error, err:
  664. if timer:
  665. timer.cancel()
  666. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  667. else:
  668. if got_line:
  669. ret = self._buffer
  670. self._buffer = ''
  671. self.on_read(ret)
  672. if timer:
  673. if timer.is_alive():
  674. timer.cancel()
  675. else:
  676. raise TimeoutError('Timeout while waiting for line terminator.')
  677. return ret
  678. def _init_ssl(self):
  679. try:
  680. ctx = SSL.Context(SSL.TLSv1_METHOD)
  681. if isinstance(self.ssl_key, crypto.PKey):
  682. ctx.use_privatekey(self.ssl_key)
  683. else:
  684. ctx.use_privatekey_file(self.ssl_key)
  685. if isinstance(self.ssl_certificate, crypto.X509):
  686. ctx.use_certificate(self.ssl_certificate)
  687. else:
  688. ctx.use_certificate_file(self.ssl_certificate)
  689. if isinstance(self.ssl_ca, crypto.X509):
  690. store = ctx.get_cert_store()
  691. store.add_cert(self.ssl_ca)
  692. else:
  693. ctx.load_verify_locations(self.ssl_ca, None)
  694. ctx.set_verify(SSL.VERIFY_PEER, self._verify_ssl_callback)
  695. self._device = SSL.Connection(ctx, self._device)
  696. except SSL.Error, err:
  697. raise CommError('Error setting up SSL connection.', err)
  698. def _verify_ssl_callback(self, connection, x509, errnum, errdepth, ok):
  699. return ok