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.

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