| @@ -0,0 +1,6 @@ | |||
| from .base_device import Device | |||
| from .serial_device import SerialDevice | |||
| from .socket_device import SocketDevice | |||
| from .usb_device import USBDevice | |||
| __all__ = ['Device', 'SerialDevice', 'SocketDevice', 'USBDevice'] | |||
| @@ -0,0 +1,139 @@ | |||
| import threading | |||
| from ..util import CommError, TimeoutError, InvalidMessageError | |||
| from ..event import event | |||
| class Device(object): | |||
| """ | |||
| Base class for all `AlarmDecoder`_ (AD2) device types. | |||
| """ | |||
| # Generic device events | |||
| on_open = event.Event("This event is called when the device has been opened.\n\n**Callback definition:** *def callback(device)*") | |||
| on_close = event.Event("This event is called when the device has been closed.\n\n**Callback definition:** def callback(device)*") | |||
| on_read = event.Event("This event is called when a line has been read from the device.\n\n**Callback definition:** def callback(device, data)*") | |||
| on_write = event.Event("This event is called when data has been written to the device.\n\n**Callback definition:** def callback(device, data)*") | |||
| def __init__(self): | |||
| """ | |||
| Constructor | |||
| """ | |||
| self._id = '' | |||
| self._buffer = b'' | |||
| self._device = None | |||
| self._running = False | |||
| self._read_thread = None | |||
| def __enter__(self): | |||
| """ | |||
| Support for context manager __enter__. | |||
| """ | |||
| return self | |||
| def __exit__(self, exc_type, exc_value, traceback): | |||
| """ | |||
| Support for context manager __exit__. | |||
| """ | |||
| self.close() | |||
| return False | |||
| @property | |||
| def id(self): | |||
| """ | |||
| Retrieve the device ID. | |||
| :returns: identification string for the device | |||
| """ | |||
| return self._id | |||
| @id.setter | |||
| def id(self, value): | |||
| """ | |||
| Sets the device ID. | |||
| :param value: device identification string | |||
| :type value: string | |||
| """ | |||
| self._id = value | |||
| def is_reader_alive(self): | |||
| """ | |||
| Indicates whether or not the reader thread is alive. | |||
| :returns: whether or not the reader thread is alive | |||
| """ | |||
| return self._read_thread.is_alive() | |||
| def stop_reader(self): | |||
| """ | |||
| Stops the reader thread. | |||
| """ | |||
| self._read_thread.stop() | |||
| def close(self): | |||
| """ | |||
| Closes the device. | |||
| """ | |||
| try: | |||
| self._running = False | |||
| self._read_thread.stop() | |||
| self._device.close() | |||
| except Exception: | |||
| pass | |||
| self.on_close() | |||
| class ReadThread(threading.Thread): | |||
| """ | |||
| Reader thread which processes messages from the device. | |||
| """ | |||
| READ_TIMEOUT = 10 | |||
| """Timeout for the reader thread.""" | |||
| def __init__(self, device): | |||
| """ | |||
| Constructor | |||
| :param device: device used by the reader thread | |||
| :type device: :py:class:`~alarmdecoder.devices.Device` | |||
| """ | |||
| threading.Thread.__init__(self) | |||
| self._device = device | |||
| self._running = False | |||
| def stop(self): | |||
| """ | |||
| Stops the running thread. | |||
| """ | |||
| self._running = False | |||
| def run(self): | |||
| """ | |||
| The actual read process. | |||
| """ | |||
| self._running = True | |||
| while self._running: | |||
| try: | |||
| self._device.read_line(timeout=self.READ_TIMEOUT) | |||
| except TimeoutError: | |||
| pass | |||
| except InvalidMessageError: | |||
| pass | |||
| except SSL.WantReadError: | |||
| pass | |||
| except CommError as err: | |||
| self._device.close() | |||
| except Exception as err: | |||
| self._device.close() | |||
| self._running = False | |||
| raise | |||
| @@ -0,0 +1,268 @@ | |||
| import threading | |||
| import serial | |||
| import serial.tools.list_ports | |||
| import select | |||
| import sys | |||
| from .base_device import Device | |||
| from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack | |||
| class SerialDevice(Device): | |||
| """ | |||
| `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_ device utilizing the PySerial interface. | |||
| """ | |||
| # Constants | |||
| BAUDRATE = 19200 | |||
| """Default baudrate for Serial devices.""" | |||
| @staticmethod | |||
| def find_all(pattern=None): | |||
| """ | |||
| Returns all serial ports present. | |||
| :param pattern: pattern to search for when retrieving serial ports | |||
| :type pattern: string | |||
| :returns: list of devices | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| devices = [] | |||
| try: | |||
| if pattern: | |||
| devices = serial.tools.list_ports.grep(pattern) | |||
| else: | |||
| devices = serial.tools.list_ports.comports() | |||
| except serial.SerialException as err: | |||
| raise CommError('Error enumerating serial devices: {0}'.format(str(err)), err) | |||
| return devices | |||
| @property | |||
| def interface(self): | |||
| """ | |||
| Retrieves the interface used to connect to the device. | |||
| :returns: interface used to connect to the device | |||
| """ | |||
| return self._port | |||
| @interface.setter | |||
| def interface(self, value): | |||
| """ | |||
| Sets the interface used to connect to the device. | |||
| :param value: name of the serial device | |||
| :type value: string | |||
| """ | |||
| self._port = value | |||
| def __init__(self, interface=None): | |||
| """ | |||
| Constructor | |||
| :param interface: device to open | |||
| :type interface: string | |||
| """ | |||
| Device.__init__(self) | |||
| self._port = interface | |||
| self._id = interface | |||
| # Timeout = non-blocking to match pyftdi. | |||
| self._device = serial.Serial(timeout=0, writeTimeout=0) | |||
| def open(self, baudrate=BAUDRATE, no_reader_thread=False): | |||
| """ | |||
| Opens the device. | |||
| :param baudrate: baudrate to use with the device | |||
| :type baudrate: int | |||
| :param no_reader_thread: whether or not to automatically start the | |||
| reader thread. | |||
| :type no_reader_thread: bool | |||
| :raises: :py:class:`~alarmdecoder.util.NoDeviceError` | |||
| """ | |||
| # Set up the defaults | |||
| if baudrate is None: | |||
| baudrate = SerialDevice.BAUDRATE | |||
| if self._port is None: | |||
| raise NoDeviceError('No device interface specified.') | |||
| self._read_thread = Device.ReadThread(self) | |||
| # Open the device and start up the reader thread. | |||
| try: | |||
| self._device.port = self._port | |||
| self._device.open() | |||
| # NOTE: Setting the baudrate before opening the | |||
| # port caused issues with Moschip 7840/7820 | |||
| # USB Serial Driver converter. (mos7840) | |||
| # | |||
| # Moving it to this point seems to resolve | |||
| # all issues with it. | |||
| self._device.baudrate = baudrate | |||
| except (serial.SerialException, ValueError, OSError) as err: | |||
| raise NoDeviceError('Error opening device on {0}.'.format(self._port), err) | |||
| else: | |||
| self._running = True | |||
| self.on_open() | |||
| if not no_reader_thread: | |||
| self._read_thread.start() | |||
| return self | |||
| def close(self): | |||
| """ | |||
| Closes the device. | |||
| """ | |||
| try: | |||
| Device.close(self) | |||
| except Exception: | |||
| pass | |||
| def fileno(self): | |||
| """ | |||
| Returns the file number associated with the device | |||
| :returns: int | |||
| """ | |||
| return self._device.fileno() | |||
| def write(self, data): | |||
| """ | |||
| Writes data to the device. | |||
| :param data: data to write | |||
| :type data: string | |||
| :raises: py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| try: | |||
| # Hack to support unicode under Python 2.x | |||
| if isinstance(data, str) or (sys.version_info < (3,) and isinstance(data, unicode)): | |||
| data = data.encode('utf-8') | |||
| self._device.write(data) | |||
| except serial.SerialTimeoutException: | |||
| pass | |||
| except serial.SerialException as err: | |||
| raise CommError('Error writing to device.', err) | |||
| else: | |||
| self.on_write(data=data) | |||
| def read(self): | |||
| """ | |||
| Reads a single character from the device. | |||
| :returns: character read from the device | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| data = '' | |||
| try: | |||
| read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | |||
| if len(read_ready) != 0: | |||
| data = self._device.read(1) | |||
| except serial.SerialException as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| return data.decode('utf-8') | |||
| def read_line(self, timeout=0.0, purge_buffer=False): | |||
| """ | |||
| Reads a line from the device. | |||
| :param timeout: read timeout | |||
| :type timeout: float | |||
| :param purge_buffer: Indicates whether to purge the buffer prior to | |||
| reading. | |||
| :type purge_buffer: bool | |||
| :returns: line that was read | |||
| :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError` | |||
| """ | |||
| def timeout_event(): | |||
| """Handles read timeout event""" | |||
| timeout_event.reading = False | |||
| timeout_event.reading = True | |||
| if purge_buffer: | |||
| self._buffer = b'' | |||
| got_line, data = False, '' | |||
| timer = threading.Timer(timeout, timeout_event) | |||
| if timeout > 0: | |||
| timer.start() | |||
| leftovers = b'' | |||
| try: | |||
| while timeout_event.reading and not got_line: | |||
| read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | |||
| if len(read_ready) == 0: | |||
| continue | |||
| bytes_avail = 0 | |||
| if hasattr(self._device, "in_waiting"): | |||
| bytes_avail = self._device.in_waiting | |||
| else: | |||
| bytes_avail = self._device.inWaiting() | |||
| buf = self._device.read(bytes_avail) | |||
| for idx in range(len(buf)): | |||
| c = buf[idx] | |||
| ub = bytes_hack(c) | |||
| if sys.version_info > (3,): | |||
| ub = bytes([ub]) | |||
| # NOTE: AD2SERIAL and AD2PI apparently sends down \xFF on boot. | |||
| if ub != b'' and ub != b"\xff": | |||
| self._buffer += ub | |||
| if ub == b"\n": | |||
| self._buffer = self._buffer.strip(b"\r\n") | |||
| if len(self._buffer) > 0: | |||
| got_line = True | |||
| leftovers = buf[idx:] | |||
| break | |||
| except (OSError, serial.SerialException) as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| else: | |||
| if got_line: | |||
| data, self._buffer = self._buffer, leftovers | |||
| self.on_read(data=data) | |||
| else: | |||
| raise TimeoutError('Timeout while waiting for line terminator.') | |||
| finally: | |||
| timer.cancel() | |||
| return data.decode('utf-8') | |||
| def purge(self): | |||
| """ | |||
| Purges read/write buffers. | |||
| """ | |||
| self._device.flushInput() | |||
| self._device.flushOutput() | |||
| @@ -0,0 +1,388 @@ | |||
| import threading | |||
| import socket | |||
| import select | |||
| from .base_device import Device | |||
| from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack | |||
| try: | |||
| from OpenSSL import SSL, crypto | |||
| have_openssl = True | |||
| except ImportError: | |||
| class SSL: | |||
| class Error(BaseException): | |||
| pass | |||
| class WantReadError(BaseException): | |||
| pass | |||
| class SysCallError(BaseException): | |||
| pass | |||
| have_openssl = False | |||
| class SocketDevice(Device): | |||
| """ | |||
| Device that supports communication with an `AlarmDecoder`_ (AD2) that is | |||
| exposed via `ser2sock`_ or another Serial to IP interface. | |||
| """ | |||
| @property | |||
| def interface(self): | |||
| """ | |||
| Retrieves the interface used to connect to the device. | |||
| :returns: interface used to connect to the device | |||
| """ | |||
| return (self._host, self._port) | |||
| @interface.setter | |||
| def interface(self, value): | |||
| """ | |||
| Sets the interface used to connect to the device. | |||
| :param value: Tuple containing the host and port to use | |||
| :type value: tuple | |||
| """ | |||
| self._host, self._port = value | |||
| @property | |||
| def ssl(self): | |||
| """ | |||
| Retrieves whether or not the device is using SSL. | |||
| :returns: whether or not the device is using SSL | |||
| """ | |||
| return self._use_ssl | |||
| @ssl.setter | |||
| def ssl(self, value): | |||
| """ | |||
| Sets whether or not SSL communication is in use. | |||
| :param value: Whether or not SSL communication is in use | |||
| :type value: bool | |||
| """ | |||
| self._use_ssl = value | |||
| @property | |||
| def ssl_certificate(self): | |||
| """ | |||
| Retrieves the SSL client certificate path used for authentication. | |||
| :returns: path to the certificate path or :py:class:`OpenSSL.crypto.X509` | |||
| """ | |||
| return self._ssl_certificate | |||
| @ssl_certificate.setter | |||
| def ssl_certificate(self, value): | |||
| """ | |||
| Sets the SSL client certificate to use for authentication. | |||
| :param value: path to the SSL certificate or :py:class:`OpenSSL.crypto.X509` | |||
| :type value: string or :py:class:`OpenSSL.crypto.X509` | |||
| """ | |||
| self._ssl_certificate = value | |||
| @property | |||
| def ssl_key(self): | |||
| """ | |||
| Retrieves the SSL client certificate key used for authentication. | |||
| :returns: jpath to the SSL key or :py:class:`OpenSSL.crypto.PKey` | |||
| """ | |||
| return self._ssl_key | |||
| @ssl_key.setter | |||
| def ssl_key(self, value): | |||
| """ | |||
| Sets the SSL client certificate key to use for authentication. | |||
| :param value: path to the SSL key or :py:class:`OpenSSL.crypto.PKey` | |||
| :type value: string or :py:class:`OpenSSL.crypto.PKey` | |||
| """ | |||
| self._ssl_key = value | |||
| @property | |||
| def ssl_ca(self): | |||
| """ | |||
| Retrieves the SSL Certificate Authority certificate used for | |||
| authentication. | |||
| :returns: path to the CA certificate or :py:class:`OpenSSL.crypto.X509` | |||
| """ | |||
| return self._ssl_ca | |||
| @ssl_ca.setter | |||
| def ssl_ca(self, value): | |||
| """ | |||
| Sets the SSL Certificate Authority certificate used for authentication. | |||
| :param value: path to the SSL CA certificate or :py:class:`OpenSSL.crypto.X509` | |||
| :type value: string or :py:class:`OpenSSL.crypto.X509` | |||
| """ | |||
| self._ssl_ca = value | |||
| def __init__(self, interface=("localhost", 10000)): | |||
| """ | |||
| Constructor | |||
| :param interface: Tuple containing the hostname and port of our target | |||
| :type interface: tuple | |||
| """ | |||
| Device.__init__(self) | |||
| self._host, self._port = interface | |||
| self._use_ssl = False | |||
| self._ssl_certificate = None | |||
| self._ssl_key = None | |||
| self._ssl_ca = None | |||
| def open(self, baudrate=None, no_reader_thread=False): | |||
| """ | |||
| Opens the device. | |||
| :param baudrate: baudrate to use | |||
| :type baudrate: int | |||
| :param no_reader_thread: whether or not to automatically open the reader | |||
| thread. | |||
| :type no_reader_thread: bool | |||
| :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| try: | |||
| self._read_thread = Device.ReadThread(self) | |||
| self._device = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| if self._use_ssl: | |||
| self._init_ssl() | |||
| self._device.connect((self._host, self._port)) | |||
| if self._use_ssl: | |||
| while True: | |||
| try: | |||
| self._device.do_handshake() | |||
| break | |||
| except SSL.WantReadError: | |||
| pass | |||
| self._id = '{0}:{1}'.format(self._host, self._port) | |||
| except socket.error as err: | |||
| raise NoDeviceError('Error opening device at {0}:{1}'.format(self._host, self._port), err) | |||
| else: | |||
| self._running = True | |||
| self.on_open() | |||
| if not no_reader_thread: | |||
| self._read_thread.start() | |||
| return self | |||
| def close(self): | |||
| """ | |||
| Closes the device. | |||
| """ | |||
| try: | |||
| # TODO: Find a way to speed up this shutdown. | |||
| if self.ssl: | |||
| self._device.shutdown() | |||
| else: | |||
| # Make sure that it closes immediately. | |||
| self._device.shutdown(socket.SHUT_RDWR) | |||
| except Exception: | |||
| pass | |||
| Device.close(self) | |||
| def fileno(self): | |||
| """ | |||
| Returns the file number associated with the device | |||
| :returns: int | |||
| """ | |||
| return self._device.fileno() | |||
| def write(self, data): | |||
| """ | |||
| Writes data to the device. | |||
| :param data: data to write | |||
| :type data: string | |||
| :returns: number of bytes sent | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| data_sent = None | |||
| try: | |||
| if isinstance(data, str): | |||
| data = data.encode('utf-8') | |||
| data_sent = self._device.send(data) | |||
| if data_sent == 0: | |||
| raise CommError('Error writing to device.') | |||
| self.on_write(data=data) | |||
| except (SSL.Error, socket.error) as err: | |||
| raise CommError('Error writing to device.', err) | |||
| return data_sent | |||
| def read(self): | |||
| """ | |||
| Reads a single character from the device. | |||
| :returns: character read from the device | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| data = '' | |||
| try: | |||
| read_ready, _, _ = select.select([self._device], [], [], 0.5) | |||
| if len(read_ready) != 0: | |||
| data = self._device.recv(1) | |||
| except socket.error as err: | |||
| raise CommError('Error while reading from device: {0}'.format(str(err)), err) | |||
| return data.decode('utf-8') | |||
| def read_line(self, timeout=0.0, purge_buffer=False): | |||
| """ | |||
| Reads a line from the device. | |||
| :param timeout: read timeout | |||
| :type timeout: float | |||
| :param purge_buffer: Indicates whether to purge the buffer prior to | |||
| reading. | |||
| :type purge_buffer: bool | |||
| :returns: line that was read | |||
| :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError` | |||
| """ | |||
| def timeout_event(): | |||
| """Handles read timeout event""" | |||
| timeout_event.reading = False | |||
| timeout_event.reading = True | |||
| if purge_buffer: | |||
| self._buffer = b'' | |||
| got_line, ret = False, None | |||
| timer = threading.Timer(timeout, timeout_event) | |||
| if timeout > 0: | |||
| timer.start() | |||
| try: | |||
| while timeout_event.reading: | |||
| read_ready, _, _ = select.select([self._device], [], [], 0.5) | |||
| if len(read_ready) == 0: | |||
| continue | |||
| buf = self._device.recv(1) | |||
| if buf != b'' and buf != b"\xff": | |||
| ub = bytes_hack(buf) | |||
| self._buffer += ub | |||
| if ub == b"\n": | |||
| self._buffer = self._buffer.rstrip(b"\r\n") | |||
| if len(self._buffer) > 0: | |||
| got_line = True | |||
| break | |||
| except socket.error as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| except SSL.SysCallError as err: | |||
| errno, msg = err | |||
| raise CommError('SSL error while reading from device: {0} ({1})'.format(msg, errno)) | |||
| except Exception: | |||
| raise | |||
| else: | |||
| if got_line: | |||
| ret, self._buffer = self._buffer, b'' | |||
| self.on_read(data=ret) | |||
| else: | |||
| raise TimeoutError('Timeout while waiting for line terminator.') | |||
| finally: | |||
| timer.cancel() | |||
| return ret.decode('utf-8') | |||
| def purge(self): | |||
| """ | |||
| Purges read/write buffers. | |||
| """ | |||
| try: | |||
| self._device.setblocking(0) | |||
| while(self._device.recv(1)): | |||
| pass | |||
| except socket.error as err: | |||
| pass | |||
| finally: | |||
| self._device.setblocking(1) | |||
| def _init_ssl(self): | |||
| """ | |||
| Initializes our device as an SSL connection. | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| if not have_openssl: | |||
| raise ImportError('SSL sockets have been disabled due to missing requirement: pyopenssl.') | |||
| try: | |||
| ctx = SSL.Context(SSL.TLSv1_METHOD) | |||
| if isinstance(self.ssl_key, crypto.PKey): | |||
| ctx.use_privatekey(self.ssl_key) | |||
| else: | |||
| ctx.use_privatekey_file(self.ssl_key) | |||
| if isinstance(self.ssl_certificate, crypto.X509): | |||
| ctx.use_certificate(self.ssl_certificate) | |||
| else: | |||
| ctx.use_certificate_file(self.ssl_certificate) | |||
| if isinstance(self.ssl_ca, crypto.X509): | |||
| store = ctx.get_cert_store() | |||
| store.add_cert(self.ssl_ca) | |||
| else: | |||
| ctx.load_verify_locations(self.ssl_ca, None) | |||
| ctx.set_verify(SSL.VERIFY_PEER, self._verify_ssl_callback) | |||
| self._device = SSL.Connection(ctx, self._device) | |||
| except SSL.Error as err: | |||
| raise CommError('Error setting up SSL connection.', err) | |||
| def _verify_ssl_callback(self, connection, x509, errnum, errdepth, ok): | |||
| """ | |||
| SSL verification callback. | |||
| """ | |||
| return ok | |||
| @@ -0,0 +1,482 @@ | |||
| import time | |||
| import threading | |||
| from .base_device import Device | |||
| from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack | |||
| from ..event import event | |||
| have_pyftdi = False | |||
| try: | |||
| from pyftdi.pyftdi.ftdi import Ftdi, FtdiError | |||
| import usb.core | |||
| import usb.util | |||
| have_pyftdi = True | |||
| except ImportError: | |||
| try: | |||
| from pyftdi.ftdi import Ftdi, FtdiError | |||
| import usb.core | |||
| import usb.util | |||
| have_pyftdi = True | |||
| except ImportError: | |||
| have_pyftdi = False | |||
| class USBDevice(Device): | |||
| """ | |||
| `AD2USB`_ device utilizing PyFTDI's interface. | |||
| """ | |||
| # Constants | |||
| PRODUCT_IDS = ((0x0403, 0x6001), (0x0403, 0x6015)) | |||
| """List of Vendor and Product IDs used to recognize `AD2USB`_ devices.""" | |||
| DEFAULT_VENDOR_ID = PRODUCT_IDS[0][0] | |||
| """Default Vendor ID used to recognize `AD2USB`_ devices.""" | |||
| DEFAULT_PRODUCT_ID = PRODUCT_IDS[0][1] | |||
| """Default Product ID used to recognize `AD2USB`_ devices.""" | |||
| # Deprecated constants | |||
| FTDI_VENDOR_ID = DEFAULT_VENDOR_ID | |||
| """DEPRECATED: Vendor ID used to recognize `AD2USB`_ devices.""" | |||
| FTDI_PRODUCT_ID = DEFAULT_PRODUCT_ID | |||
| """DEPRECATED: Product ID used to recognize `AD2USB`_ devices.""" | |||
| BAUDRATE = 115200 | |||
| """Default baudrate for `AD2USB`_ devices.""" | |||
| __devices = [] | |||
| __detect_thread = None | |||
| @classmethod | |||
| def find_all(cls, vid=None, pid=None): | |||
| """ | |||
| Returns all FTDI devices matching our vendor and product IDs. | |||
| :returns: list of devices | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| if not have_pyftdi: | |||
| raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.') | |||
| cls.__devices = [] | |||
| query = cls.PRODUCT_IDS | |||
| if vid and pid: | |||
| query = [(vid, pid)] | |||
| try: | |||
| cls.__devices = Ftdi.find_all(query, nocache=True) | |||
| except (usb.core.USBError, FtdiError) as err: | |||
| raise CommError('Error enumerating AD2USB devices: {0}'.format(str(err)), err) | |||
| return cls.__devices | |||
| @classmethod | |||
| def devices(cls): | |||
| """ | |||
| Returns a cached list of `AD2USB`_ devices located on the system. | |||
| :returns: cached list of devices found | |||
| """ | |||
| return cls.__devices | |||
| @classmethod | |||
| def find(cls, device=None): | |||
| """ | |||
| Factory method that returns the requested :py:class:`USBDevice` device, or the | |||
| first device. | |||
| :param device: Tuple describing the USB device to open, as returned | |||
| by find_all(). | |||
| :type device: tuple | |||
| :returns: :py:class:`USBDevice` object utilizing the specified device | |||
| :raises: :py:class:`~alarmdecoder.util.NoDeviceError` | |||
| """ | |||
| if not have_pyftdi: | |||
| raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.') | |||
| cls.find_all() | |||
| if len(cls.__devices) == 0: | |||
| raise NoDeviceError('No AD2USB devices present.') | |||
| if device is None: | |||
| device = cls.__devices[0] | |||
| vendor, product, sernum, ifcount, description = device | |||
| return USBDevice(interface=sernum, vid=vendor, pid=product) | |||
| @classmethod | |||
| def start_detection(cls, on_attached=None, on_detached=None): | |||
| """ | |||
| Starts the device detection thread. | |||
| :param on_attached: function to be called when a device is attached **Callback definition:** *def callback(thread, device)* | |||
| :type on_attached: function | |||
| :param on_detached: function to be called when a device is detached **Callback definition:** *def callback(thread, device)* | |||
| :type on_detached: function | |||
| """ | |||
| if not have_pyftdi: | |||
| raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.') | |||
| cls.__detect_thread = USBDevice.DetectThread(on_attached, on_detached) | |||
| try: | |||
| cls.find_all() | |||
| except CommError: | |||
| pass | |||
| cls.__detect_thread.start() | |||
| @classmethod | |||
| def stop_detection(cls): | |||
| """ | |||
| Stops the device detection thread. | |||
| """ | |||
| if not have_pyftdi: | |||
| raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.') | |||
| try: | |||
| cls.__detect_thread.stop() | |||
| except Exception: | |||
| pass | |||
| @property | |||
| def interface(self): | |||
| """ | |||
| Retrieves the interface used to connect to the device. | |||
| :returns: the interface used to connect to the device | |||
| """ | |||
| return self._interface | |||
| @interface.setter | |||
| def interface(self, value): | |||
| """ | |||
| Sets the interface used to connect to the device. | |||
| :param value: may specify either the serial number or the device index | |||
| :type value: string or int | |||
| """ | |||
| self._interface = value | |||
| if isinstance(value, int): | |||
| self._device_number = value | |||
| else: | |||
| self._serial_number = value | |||
| @property | |||
| def serial_number(self): | |||
| """ | |||
| Retrieves the serial number of the device. | |||
| :returns: serial number of the device | |||
| """ | |||
| return self._serial_number | |||
| @serial_number.setter | |||
| def serial_number(self, value): | |||
| """ | |||
| Sets the serial number of the device. | |||
| :param value: serial number of the device | |||
| :type value: string | |||
| """ | |||
| self._serial_number = value | |||
| @property | |||
| def description(self): | |||
| """ | |||
| Retrieves the description of the device. | |||
| :returns: description of the device | |||
| """ | |||
| return self._description | |||
| @description.setter | |||
| def description(self, value): | |||
| """ | |||
| Sets the description of the device. | |||
| :param value: description of the device | |||
| :type value: string | |||
| """ | |||
| self._description = value | |||
| def __init__(self, interface=0, vid=None, pid=None): | |||
| """ | |||
| Constructor | |||
| :param interface: May specify either the serial number or the device | |||
| index. | |||
| :type interface: string or int | |||
| """ | |||
| if not have_pyftdi: | |||
| raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.') | |||
| Device.__init__(self) | |||
| self._device = Ftdi() | |||
| self._interface = 0 | |||
| self._device_number = 0 | |||
| self._serial_number = None | |||
| self._vendor_id = USBDevice.DEFAULT_VENDOR_ID | |||
| if vid: | |||
| self._vendor_id = vid | |||
| self._product_id = USBDevice.DEFAULT_PRODUCT_ID | |||
| if pid: | |||
| self._product_id = pid | |||
| self._endpoint = 0 | |||
| self._description = None | |||
| self.interface = interface | |||
| def open(self, baudrate=BAUDRATE, no_reader_thread=False): | |||
| """ | |||
| Opens the device. | |||
| :param baudrate: baudrate to use | |||
| :type baudrate: int | |||
| :param no_reader_thread: whether or not to automatically start the | |||
| reader thread. | |||
| :type no_reader_thread: bool | |||
| :raises: :py:class:`~alarmdecoder.util.NoDeviceError` | |||
| """ | |||
| # Set up defaults | |||
| if baudrate is None: | |||
| baudrate = USBDevice.BAUDRATE | |||
| self._read_thread = Device.ReadThread(self) | |||
| # Open the device and start up the thread. | |||
| try: | |||
| self._device.open(self._vendor_id, | |||
| self._product_id, | |||
| self._endpoint, | |||
| self._device_number, | |||
| self._serial_number, | |||
| self._description) | |||
| self._device.set_baudrate(baudrate) | |||
| if not self._serial_number: | |||
| self._serial_number = self._get_serial_number() | |||
| self._id = self._serial_number | |||
| except (usb.core.USBError, FtdiError) as err: | |||
| raise NoDeviceError('Error opening device: {0}'.format(str(err)), err) | |||
| except KeyError as err: | |||
| raise NoDeviceError('Unsupported device. ({0:04x}:{1:04x}) You probably need a newer version of pyftdi.'.format(err[0][0], err[0][1])) | |||
| else: | |||
| self._running = True | |||
| self.on_open() | |||
| if not no_reader_thread: | |||
| self._read_thread.start() | |||
| return self | |||
| def close(self): | |||
| """ | |||
| Closes the device. | |||
| """ | |||
| try: | |||
| Device.close(self) | |||
| # HACK: Probably should fork pyftdi and make this call in .close() | |||
| self._device.usb_dev.attach_kernel_driver(self._device_number) | |||
| except Exception: | |||
| pass | |||
| def fileno(self): | |||
| """ | |||
| File number not supported for USB devices. | |||
| :raises: NotImplementedError | |||
| """ | |||
| raise NotImplementedError('USB devices do not support fileno()') | |||
| def write(self, data): | |||
| """ | |||
| Writes data to the device. | |||
| :param data: data to write | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| try: | |||
| self._device.write_data(data) | |||
| self.on_write(data=data) | |||
| except FtdiError as err: | |||
| raise CommError('Error writing to device: {0}'.format(str(err)), err) | |||
| def read(self): | |||
| """ | |||
| Reads a single character from the device. | |||
| :returns: character read from the device | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| ret = None | |||
| try: | |||
| ret = self._device.read_data(1) | |||
| except (usb.core.USBError, FtdiError) as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| return ret | |||
| def read_line(self, timeout=0.0, purge_buffer=False): | |||
| """ | |||
| Reads a line from the device. | |||
| :param timeout: read timeout | |||
| :type timeout: float | |||
| :param purge_buffer: Indicates whether to purge the buffer prior to | |||
| reading. | |||
| :type purge_buffer: bool | |||
| :returns: line that was read | |||
| :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError` | |||
| """ | |||
| def timeout_event(): | |||
| """Handles read timeout event""" | |||
| timeout_event.reading = False | |||
| timeout_event.reading = True | |||
| if purge_buffer: | |||
| self._buffer = b'' | |||
| got_line, ret = False, None | |||
| timer = threading.Timer(timeout, timeout_event) | |||
| if timeout > 0: | |||
| timer.start() | |||
| try: | |||
| while timeout_event.reading: | |||
| buf = self._device.read_data(1) | |||
| if buf != b'': | |||
| ub = bytes_hack(buf) | |||
| self._buffer += ub | |||
| if ub == b"\n": | |||
| self._buffer = self._buffer.rstrip(b"\r\n") | |||
| if len(self._buffer) > 0: | |||
| got_line = True | |||
| break | |||
| else: | |||
| time.sleep(0.01) | |||
| except (usb.core.USBError, FtdiError) as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| else: | |||
| if got_line: | |||
| ret, self._buffer = self._buffer, b'' | |||
| self.on_read(data=ret) | |||
| else: | |||
| raise TimeoutError('Timeout while waiting for line terminator.') | |||
| finally: | |||
| timer.cancel() | |||
| return ret | |||
| def purge(self): | |||
| """ | |||
| Purges read/write buffers. | |||
| """ | |||
| self._device.purge_buffers() | |||
| def _get_serial_number(self): | |||
| """ | |||
| Retrieves the FTDI device serial number. | |||
| :returns: string containing the device serial number | |||
| """ | |||
| return usb.util.get_string(self._device.usb_dev, 64, self._device.usb_dev.iSerialNumber) | |||
| class DetectThread(threading.Thread): | |||
| """ | |||
| Thread that handles detection of added/removed devices. | |||
| """ | |||
| on_attached = event.Event("This event is called when an `AD2USB`_ device has been detected.\n\n**Callback definition:** def callback(thread, device*") | |||
| on_detached = event.Event("This event is called when an `AD2USB`_ device has been removed.\n\n**Callback definition:** def callback(thread, device*") | |||
| def __init__(self, on_attached=None, on_detached=None): | |||
| """ | |||
| Constructor | |||
| :param on_attached: Function to call when a device is attached **Callback definition:** *def callback(thread, device)* | |||
| :type on_attached: function | |||
| :param on_detached: Function to call when a device is detached **Callback definition:** *def callback(thread, device)* | |||
| :type on_detached: function | |||
| """ | |||
| threading.Thread.__init__(self) | |||
| if on_attached: | |||
| self.on_attached += on_attached | |||
| if on_detached: | |||
| self.on_detached += on_detached | |||
| self._running = False | |||
| def stop(self): | |||
| """ | |||
| Stops the thread. | |||
| """ | |||
| self._running = False | |||
| def run(self): | |||
| """ | |||
| The actual detection process. | |||
| """ | |||
| self._running = True | |||
| last_devices = set() | |||
| while self._running: | |||
| try: | |||
| current_devices = set(USBDevice.find_all()) | |||
| for dev in current_devices.difference(last_devices): | |||
| self.on_attached(device=dev) | |||
| for dev in last_devices.difference(current_devices): | |||
| self.on_detached(device=dev) | |||
| last_devices = current_devices | |||
| except CommError: | |||
| pass | |||
| time.sleep(0.25) | |||
| @@ -9,6 +9,7 @@ Provides utility classes for the `AlarmDecoder`_ (AD2) devices. | |||
| import time | |||
| import threading | |||
| import select | |||
| import sys | |||
| import alarmdecoder | |||
| from io import open | |||
| @@ -79,6 +80,19 @@ def bytes_available(device): | |||
| return bytes_avail | |||
| def bytes_hack(buf): | |||
| """ | |||
| Hacky workaround for old installs of the library on systems without python-future that were | |||
| keeping the 2to3 update from working after auto-update. | |||
| """ | |||
| ub = None | |||
| if sys.version_info > (3,): | |||
| ub = buf | |||
| else: | |||
| ub = bytes(buf) | |||
| return ub | |||
| def read_firmware_file(file_path): | |||
| """ | |||
| Reads a firmware file into a dequeue for processing. | |||