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.

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