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.

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