| @@ -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 time | ||||
| import threading | import threading | ||||
| import select | import select | ||||
| import sys | |||||
| import alarmdecoder | import alarmdecoder | ||||
| from io import open | from io import open | ||||
| @@ -79,6 +80,19 @@ def bytes_available(device): | |||||
| return bytes_avail | 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): | def read_firmware_file(file_path): | ||||
| """ | """ | ||||
| Reads a firmware file into a dequeue for processing. | Reads a firmware file into a dequeue for processing. | ||||