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.

914 lines
24 KiB

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