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.

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