示例#1
0
class Tunnel_DAC(Instrument):
    def __init__(self, name, serial=None, channel='A0', numdacs=3, delay=1e-3):
        '''
            discover and initialize Tunnel_DAC hardware
            
            Input:
                serial - serial number of the FTDI converter
                channel - 2 character channel id the DAC is connected to;
                    the first byte identifies the channel (A..D for current devices)
                    the second byte identifies the bit within that channel (0..7)
                numdacs - number of DACs daisy-chained on that line
                delay - communications delay assumed between PC and the USB converter
        '''
        logging.info(__name__ + ': Initializing instrument Tunnel_DAC')
        Instrument.__init__(self, name, tags=['physical'])

        self._conn = Ftdi()
        # VIDs and PIDs of converters used
        vps = [
            (0x0403, 0x6011),  # FTDI UM4232H 4ch
            (0x0403, 0x6014)  # FTDI UM232H 1ch
        ]
        # explicitly clear device cache of UsbTools
        #UsbTools.USBDEVICES = []
        # find all devices and obtain serial numbers
        devs = self._conn.find_all(vps)
        # filter list by serial number if provided
        if (serial != None):
            devs = [dev for dev in devs if dev[2] == serial]
        if (len(devs) == 0):
            logging.error(__name__ + ': failed to find matching FTDI devices.')
        elif (len(devs) > 1):
            logging.error(
                __name__ +
                ': more than one converter found and no serial number given.')
            logging.info(__name__ + ': available devices are: %s.' %
                         str([dev[2] for dev in devs]))
        vid, pid, self._serial, channels, description = devs[0]
        # parse channel string
        if (len(channel) != 2):
            logging.error(
                __name__ +
                ': channel identifier must be a string of length 2. ex. A0, D5.'
            )
        self._channel = 1 + ord(channel[0]) - ord('A')
        self._bit = ord(channel[1]) - ord('0')
        if ((self._channel < 1) or (self._channel > channels)):
            logging.error(__name__ +
                          ': channel %c is not supported by this device.' %
                          (chr(ord('A') + self._channel - 1)))
        if ((self._bit < 0) or (self._bit > 7)):
            logging.error(__name__ +
                          ': subchannel must be between 0 and 7, not %d.' %
                          self._bit)

        # open device
        self._conn.open(vid, pid, interface=self._channel, serial=self._serial)
        logging.info(__name__ +
                     ': using converter with serial #%s' % self._serial)
        self._conn.set_bitmode(0xFF, Ftdi.BITMODE_BITBANG)
        # 80k generates bit durations of 12.5us, 80 is magic :(
        # magic?: 4 from incorrect BITBANG handling of pyftdi, 2.5 from 120MHz instead of 48MHz clock of H devices
        # original matlab code uses 19kS/s
        self._conn.set_baudrate(19000 / 80)

        # house keeping
        self._numdacs = numdacs
        self._sleeptime = (
            10. + 16. * self._numdacs
        ) * 12.5e-6 + delay  # 1st term from hardware parameters, 2nd term from USB
        self._minval = -5000.
        self._maxval = 5000.
        self._resolution = 16  # DAC resolution in bits
        self._voltages = [0.] * numdacs

        self.add_parameter('voltage',
                           type=types.FloatType,
                           flags=Instrument.FLAG_SET,
                           channels=(1, self._numdacs),
                           minval=self._minval,
                           maxval=self._maxval,
                           units='mV',
                           format='%.02f')  # tags=['sweep']
        self.add_function('set_voltages')
        self.add_function('commit')

    def _encode(self, data, channel=0, bits_per_item=16, big_endian=True):
        '''
            convert binary data into line symbols
            
            the tunnel electronic DAC logic box triggers on rising 
            signal edges and samples data after 18us. we use a line
            code with three bits of duration 12us, where a logical 1
            is encoded as B"110" and a logical 0 is encoded as B"100".
            
            the line data is returned as a byte string with three bytes/symbol.
            
            Input:
                data - a vector of data entities (usually a string or list of integers)
                channel - a number in [0, 7] specifying the output bit on the USB-to-UART chip
                bits_per_item - number of bits to extract from each element of the data vector
        '''
        # build line code for the requested channel
        line_1 = chr(1 << channel)
        line_0 = chr(0)
        line_code = [
            ''.join([line_0, line_1, line_1]),
            ''.join([line_0, line_1, line_0])
        ]
        # do actual encoding
        result = []
        result.append(10 * line_0)
        for item in data:
            for bit in (range(bits_per_item - 1, -1, -1)
                        if big_endian else range(0, bits_per_item)):
                result.append(line_code[1 if (item & (1 << bit)) else 0])
        result.append(10 * line_0)
        return ''.join(result)

    def commit(self):
        '''
            send updated parameter values to the physical DACs via USB
        '''
        # normalize, scale, clip voltages
        voltages = [
            -1 + 2 * (x - self._minval) / (self._maxval - self._minval)
            for x in self._voltages
        ]
        voltages = [
            max(
                -2**(self._resolution - 1),
                min(2**(self._resolution - 1) - 1,
                    int(2**(self._resolution - 1) * x))) for x in voltages
        ]
        # encode and send
        data = self._encode(reversed(voltages), self._bit, self._resolution)
        self._conn.write_data(data)
        # wait for the FTDI fifo to clock the data to the DACs
        sleep(self._sleeptime)

    def do_set_voltage(self, value, channel):
        '''
            immediately update voltage on channel ch
            parameter checking is done by qtlab
        '''
        self._voltages[channel - 1] = value
        self.commit()

    def set_voltages(self, valuedict):
        '''
            update voltages on several channels simultaneously
            todo: update instrument panel display
        '''
        for channel, value in valuedict.iteritems():
            # bounds checking & clipping
            if ((channel < 1) or (channel > self._numdacs)):
                logging.error(__name__ +
                              ': channel %d out of range.' % channel)
                continue
            value = float(value)
            if ((value < self._minval) or (value >= self._maxval)):
                logging.error(__name__ +
                              ': value %f out of range. clipping.' % value)
                value = max(self._minval,
                            min(self._maxval,
                                value))  # does not handle maxval correctly
            self._voltages[channel - 1] = value
        self.commit()
示例#2
0
class Tunnel_DAC(Instrument):

    def __init__(self, name, serial = None, channel = 'A0', numdacs=3, delay = 1e-3):
        '''
            discover and initialize Tunnel_DAC hardware
            
            Input:
                serial - serial number of the FTDI converter
                channel - 2 character channel id the DAC is connected to;
                    the first byte identifies the channel (A..D for current devices)
                    the second byte identifies the bit within that channel (0..7)
                numdacs - number of DACs daisy-chained on that line
                delay - communications delay assumed between PC and the USB converter
        '''
        logging.info(__name__+ ': Initializing instrument Tunnel_DAC')
        Instrument.__init__(self, name, tags=['physical'])
        
        self._conn = Ftdi()
        # VIDs and PIDs of converters used
        vps = [
            (0x0403, 0x6011), # FTDI UM4232H 4ch
            (0x0403, 0x6014)  # FTDI UM232H 1ch
        ]
        # explicitly clear device cache of UsbTools
        #UsbTools.USBDEVICES = []
        # find all devices and obtain serial numbers
        devs = self._conn.find_all(vps)
        # filter list by serial number if provided
        if(serial != None):
            devs = [dev for dev in devs if dev[2] == serial]
        if(len(devs) == 0):
            logging.error(__name__ + ': failed to find matching FTDI devices.')
        elif(len(devs) > 1):
            logging.error(__name__ + ': more than one converter found and no serial number given.')
            logging.info(__name__ + ': available devices are: %s.'%str([dev[2] for dev in devs]))
        vid, pid, self._serial, channels, description = devs[0]
        # parse channel string
        if(len(channel) != 2):
            logging.error(__name__ + ': channel identifier must be a string of length 2. ex. A0, D5.')
        self._channel = 1 + ord(channel[0]) - ord('A')
        self._bit = ord(channel[1]) - ord('0')
        if((self._channel < 1) or (self._channel > channels)):
            logging.error(__name__ + ': channel %c is not supported by this device.'%(chr(ord('A')+self._channel-1)))
        if((self._bit < 0) or (self._bit > 7)):
            logging.error(__name__ + ': subchannel must be between 0 and 7, not %d.'%self._bit)

        # open device
        self._conn.open(vid, pid, interface = self._channel, serial = self._serial)
        logging.info(__name__ + ': using converter with serial #%s'%self._serial)
        self._conn.set_bitmode(0xFF, Ftdi.BITMODE_BITBANG)
        # 80k generates bit durations of 12.5us, 80 is magic :(
        # magic?: 4 from incorrect BITBANG handling of pyftdi, 2.5 from 120MHz instead of 48MHz clock of H devices
        # original matlab code uses 19kS/s
        self._conn.set_baudrate(19000/80)

        # house keeping        
        self._numdacs = numdacs
        self._sleeptime = (10. + 16.*self._numdacs)*12.5e-6 + delay # 1st term from hardware parameters, 2nd term from USB  
        self._minval = -5000.
        self._maxval = 5000.
        self._resolution = 16 # DAC resolution in bits
        self._voltages = [0.]*numdacs
        
        self.add_parameter('voltage', type=types.FloatType, flags=Instrument.FLAG_SET, channels=(1, self._numdacs),
                           minval = self._minval, maxval = self._maxval, units='mV', format = '%.02f') # tags=['sweep']
        self.add_function('set_voltages')
        self.add_function('commit')


    def _encode(self, data, channel = 0, bits_per_item = 16, big_endian = True):
        '''
            convert binary data into line symbols
            
            the tunnel electronic DAC logic box triggers on rising 
            signal edges and samples data after 18us. we use a line
            code with three bits of duration 12us, where a logical 1
            is encoded as B"110" and a logical 0 is encoded as B"100".
            
            the line data is returned as a byte string with three bytes/symbol.
            
            Input:
                data - a vector of data entities (usually a string or list of integers)
                channel - a number in [0, 7] specifying the output bit on the USB-to-UART chip
                bits_per_item - number of bits to extract from each element of the data vector
        '''
        # build line code for the requested channel
        line_1 = chr(1<<channel)
        line_0 = chr(0)
        line_code = [''.join([line_0, line_1, line_1]), ''.join([line_0, line_1, line_0])]
        # do actual encoding
        result = []
        result.append(10*line_0)
        for item in data:
            for bit in (range(bits_per_item-1, -1, -1) if big_endian else range(0, bits_per_item)):
                result.append(line_code[1 if(item & (1<<bit)) else 0])
        result.append(10*line_0)
        return ''.join(result)


    def commit(self):
        '''
            send updated parameter values to the physical DACs via USB
        '''
        # normalize, scale, clip voltages
        voltages = [-1+2*(x-self._minval)/(self._maxval-self._minval) for x in self._voltages]
        voltages = [max(-2**(self._resolution-1), min(2**(self._resolution-1)-1, int(2**(self._resolution-1)*x))) for x in voltages]
        # encode and send
        data = self._encode(reversed(voltages), self._bit, self._resolution)
        self._conn.write_data(data)
        # wait for the FTDI fifo to clock the data to the DACs
        sleep(self._sleeptime)


    def do_set_voltage(self, value, channel):
        '''
            immediately update voltage on channel ch
            parameter checking is done by qtlab
        '''
        self._voltages[channel-1] = value
        self.commit()


    def set_voltages(self, valuedict):
        '''
            update voltages on several channels simultaneously
            todo: update instrument panel display
        '''
        for channel, value in valuedict.iteritems():
            # bounds checking & clipping
            if((channel < 1) or (channel > self._numdacs)):
                logging.error(__name__ + ': channel %d out of range.'%channel)
                continue
            value = float(value)
            if((value < self._minval) or (value >= self._maxval)):
                logging.error(__name__ + ': value %f out of range. clipping.'%value)
                value = max(self._minval, min(self._maxval, value)) # does not handle maxval correctly
            self._voltages[channel-1] = value
        self.commit()
示例#3
0
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)
示例#4
0
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)