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.

917 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._serial_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 serial number and endpoint number to use.
  140. :type value: tuple
  141. """
  142. self._serial_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=(None, 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._serial_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._device_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: 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 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: CommError
  233. """
  234. try:
  235. self._device.write_data(data)
  236. self.on_write(data)
  237. except FtdiError, err:
  238. raise 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: CommError
  244. """
  245. ret = None
  246. try:
  247. ret = self._device.read_data(1)
  248. except (usb.core.USBError, FtdiError), err:
  249. raise 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: CommError, 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. if timer:
  289. timer.cancel()
  290. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  291. else:
  292. if got_line:
  293. ret = self._buffer
  294. self._buffer = ''
  295. self.on_read(ret)
  296. if timer:
  297. if timer.is_alive():
  298. timer.cancel()
  299. else:
  300. raise TimeoutError('Timeout while waiting for line terminator.')
  301. return ret
  302. class SerialDevice(Device):
  303. """
  304. AD2USB or AD2SERIAL device exposed with the pyserial interface.
  305. """
  306. # Constants
  307. BAUDRATE = 19200
  308. """Default baudrate for Serial devices."""
  309. @staticmethod
  310. def find_all(pattern=None):
  311. """
  312. Returns all serial ports present.
  313. :param pattern: Pattern to search for when retrieving serial ports.
  314. :type pattern: str
  315. :returns: list of devices
  316. :raises: CommError
  317. """
  318. devices = []
  319. try:
  320. if pattern:
  321. devices = serial.tools.list_ports.grep(pattern)
  322. else:
  323. devices = serial.tools.list_ports.comports()
  324. except SerialException, err:
  325. raise CommError('Error enumerating serial devices: {0}'.format(str(err)), err)
  326. return devices
  327. @property
  328. def interface(self):
  329. """
  330. Retrieves the interface used to connect to the device.
  331. :returns: the interface used to connect to the device.
  332. """
  333. return self._port
  334. @interface.setter
  335. def interface(self, value):
  336. """
  337. Sets the interface used to connect to the device.
  338. :param value: The name of the serial device.
  339. :type value: string
  340. """
  341. self._port = value
  342. def __init__(self, interface=None):
  343. """
  344. Constructor
  345. :param interface: The device to open.
  346. :type interface: str
  347. """
  348. Device.__init__(self)
  349. self._port = interface
  350. self._id = interface
  351. self._device = serial.Serial(timeout=0, writeTimeout=0) # Timeout = non-blocking to match pyftdi.
  352. def open(self, baudrate=BAUDRATE, no_reader_thread=False):
  353. """
  354. Opens the device.
  355. :param baudrate: The baudrate to use with the device.
  356. :type baudrate: int
  357. :param no_reader_thread: Whether or not to automatically start the reader thread.
  358. :type no_reader_thread: bool
  359. :raises: NoDeviceError
  360. """
  361. # Set up the defaults
  362. if baudrate is None:
  363. baudrate = SerialDevice.BAUDRATE
  364. if self._port is None:
  365. raise NoDeviceError('No device interface specified.')
  366. self._device.port = self._port
  367. # Open the device and start up the reader thread.
  368. try:
  369. self._device.open()
  370. self._device.baudrate = baudrate # NOTE: Setting the baudrate before opening the
  371. # port caused issues with Moschip 7840/7820
  372. # USB Serial Driver converter. (mos7840)
  373. #
  374. # Moving it to this point seems to resolve
  375. # all issues with it.
  376. except (serial.SerialException, ValueError), err:
  377. raise NoDeviceError('Error opening device on port {0}.'.format(self._port), err)
  378. else:
  379. self._running = True
  380. self.on_open()
  381. if not no_reader_thread:
  382. self._read_thread.start()
  383. def close(self):
  384. """
  385. Closes the device.
  386. """
  387. try:
  388. Device.close(self)
  389. except:
  390. pass
  391. def write(self, data):
  392. """
  393. Writes data to the device.
  394. :param data: The data to write.
  395. :type data: str
  396. :raises: CommError
  397. """
  398. try:
  399. self._device.write(data)
  400. except serial.SerialTimeoutException, err:
  401. pass
  402. except serial.SerialException, err:
  403. raise CommError('Error writing to device.', err)
  404. else:
  405. self.on_write(data)
  406. def read(self):
  407. """
  408. Reads a single character from the device.
  409. :returns: The character read from the device.
  410. :raises: CommError
  411. """
  412. ret = None
  413. try:
  414. ret = self._device.read(1)
  415. except serial.SerialException, err:
  416. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  417. return ret
  418. def read_line(self, timeout=0.0, purge_buffer=False):
  419. """
  420. Reads a line from the device.
  421. :param timeout: The read timeout.
  422. :type timeout: float
  423. :param purge_buffer: Indicates whether to purge the buffer prior to reading.
  424. :type purge_buffer: bool
  425. :returns: The line read.
  426. :raises: CommError, TimeoutError
  427. """
  428. def timeout_event():
  429. timeout_event.reading = False
  430. timeout_event.reading = True
  431. got_line = False
  432. ret = None
  433. timer = None
  434. if timeout > 0:
  435. timer = threading.Timer(timeout, timeout_event)
  436. timer.start()
  437. try:
  438. while timeout_event.reading:
  439. buf = self._device.read(1)
  440. if buf != '' and buf != "\xff": # AD2SERIAL specifically apparently sends down \xFF on boot.
  441. self._buffer += buf
  442. if buf == "\n":
  443. if len(self._buffer) > 1:
  444. if self._buffer[-2] == "\r":
  445. self._buffer = self._buffer[:-2]
  446. # ignore if we just got \r\n with nothing else in the buffer.
  447. if len(self._buffer) != 0:
  448. got_line = True
  449. break
  450. else:
  451. self._buffer = self._buffer[:-1]
  452. except (OSError, serial.SerialException), err:
  453. if timer:
  454. timer.cancel()
  455. raise 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 TimeoutError('Timeout while waiting for line terminator.')
  466. return ret
  467. class SocketDevice(Device):
  468. """
  469. Device that supports communication with an AD2 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: NoDeviceError, CommError
  568. """
  569. try:
  570. self._device = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  571. if self._use_ssl:
  572. self._init_ssl()
  573. self._device.connect((self._host, self._port))
  574. self._id = '{0}:{1}'.format(self._host, self._port)
  575. except socket.error, err:
  576. raise NoDeviceError('Error opening device at {0}:{1}'.format(self._host, self._port), err)
  577. else:
  578. self._running = True
  579. self.on_open()
  580. if not no_reader_thread:
  581. self._read_thread.start()
  582. def close(self):
  583. """
  584. Closes the device.
  585. """
  586. try:
  587. if self.ssl:
  588. self._device.shutdown()
  589. else:
  590. self._device.shutdown(socket.SHUT_RDWR) # Make sure that it closes immediately.
  591. Device.close(self)
  592. except Exception, ex:
  593. pass
  594. def write(self, data):
  595. """
  596. Writes data to the device.
  597. :param data: The data to write.
  598. :type data: str
  599. :returns: The number of bytes sent.
  600. :raises: CommError
  601. """
  602. data_sent = None
  603. try:
  604. data_sent = self._device.send(data)
  605. if data_sent == 0:
  606. raise CommError('Error writing to device.')
  607. self.on_write(data)
  608. except (SSL.Error, socket.error), err:
  609. raise CommError('Error writing to device.', err)
  610. return data_sent
  611. def read(self):
  612. """
  613. Reads a single character from the device.
  614. :returns: The character read from the device.
  615. :raises: CommError
  616. """
  617. data = None
  618. try:
  619. data = self._device.recv(1)
  620. except socket.error, err:
  621. raise CommError('Error while reading from device: {0}'.format(str(err)), err)
  622. return data
  623. def read_line(self, timeout=0.0, purge_buffer=False):
  624. """
  625. Reads a line from the device.
  626. :param timeout: The read timeout.
  627. :type timeout: float
  628. :param purge_buffer: Indicates whether to purge the buffer prior to reading.
  629. :type purge_buffer: bool
  630. :returns: The line read from the device.
  631. :raises: CommError, TimeoutError
  632. """
  633. if purge_buffer:
  634. self._buffer = ''
  635. def timeout_event():
  636. timeout_event.reading = False
  637. timeout_event.reading = True
  638. got_line = False
  639. ret = None
  640. timer = None
  641. if timeout > 0:
  642. timer = threading.Timer(timeout, timeout_event)
  643. timer.start()
  644. try:
  645. while timeout_event.reading:
  646. buf = self._device.recv(1)
  647. if buf != '':
  648. self._buffer += buf
  649. if buf == "\n":
  650. if len(self._buffer) > 1:
  651. if self._buffer[-2] == "\r":
  652. self._buffer = self._buffer[:-2]
  653. # ignore if we just got \r\n with nothing else in the buffer.
  654. if len(self._buffer) != 0:
  655. got_line = True
  656. break
  657. else:
  658. self._buffer = self._buffer[:-1]
  659. except socket.error, err:
  660. if timer:
  661. timer.cancel()
  662. raise CommError('Error reading from device: {0}'.format(str(err)), err)
  663. else:
  664. if got_line:
  665. ret = self._buffer
  666. self._buffer = ''
  667. self.on_read(ret)
  668. if timer:
  669. if timer.is_alive():
  670. timer.cancel()
  671. else:
  672. raise TimeoutError('Timeout while waiting for line terminator.')
  673. return ret
  674. def _init_ssl(self):
  675. try:
  676. ctx = SSL.Context(SSL.TLSv1_METHOD)
  677. if isinstance(self.ssl_key, crypto.PKey):
  678. ctx.use_privatekey(self.ssl_key)
  679. else:
  680. ctx.use_privatekey_file(self.ssl_key)
  681. if isinstance(self.ssl_certificate, crypto.X509):
  682. ctx.use_certificate(self.ssl_certificate)
  683. else:
  684. ctx.use_certificate_file(self.ssl_certificate)
  685. if isinstance(self.ssl_ca, crypto.X509):
  686. store = ctx.get_cert_store()
  687. store.add_cert(self.ssl_ca)
  688. else:
  689. ctx.load_verify_locations(self.ssl_ca, None)
  690. ctx.set_verify(SSL.VERIFY_PEER, self._verify_ssl_callback)
  691. self._device = SSL.Connection(ctx, self._device)
  692. except SSL.Error, err:
  693. raise CommError('Error setting up SSL connection.', err)
  694. def _verify_ssl_callback(self, connection, x509, errnum, errdepth, ok):
  695. return ok