Browse Source

Broke out the devices into their own files for easier management.

pyserial_fix
Scott Petersen 7 years ago
parent
commit
f234038494
7 changed files with 1297 additions and 1297 deletions
  1. +0
    -1297
      alarmdecoder/devices.py
  2. +6
    -0
      alarmdecoder/devices/__init__.py
  3. +139
    -0
      alarmdecoder/devices/base_device.py
  4. +268
    -0
      alarmdecoder/devices/serial_device.py
  5. +388
    -0
      alarmdecoder/devices/socket_device.py
  6. +482
    -0
      alarmdecoder/devices/usb_device.py
  7. +14
    -0
      alarmdecoder/util.py

+ 0
- 1297
alarmdecoder/devices.py
File diff suppressed because it is too large
View File


+ 6
- 0
alarmdecoder/devices/__init__.py View File

@@ -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']

+ 139
- 0
alarmdecoder/devices/base_device.py View File

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

+ 268
- 0
alarmdecoder/devices/serial_device.py View File

@@ -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()

+ 388
- 0
alarmdecoder/devices/socket_device.py View File

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

+ 482
- 0
alarmdecoder/devices/usb_device.py View File

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

+ 14
- 0
alarmdecoder/util.py View File

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


Loading…
Cancel
Save