@@ -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. | ||||