| @@ -1,4 +1,4 @@ | |||
| import ad2 | |||
| from ad2 import AD2 | |||
| import devices | |||
| import util | |||
| import messages | |||
| @@ -8,167 +8,10 @@ import time | |||
| import threading | |||
| from .event import event | |||
| from .devices import USBDevice | |||
| from .util import CommError, NoDeviceError | |||
| from .messages import Message, ExpanderMessage, RFMessage, LRRMessage | |||
| from .zonetracking import Zonetracker | |||
| class AD2Factory(object): | |||
| """ | |||
| Factory for creation of AD2USB devices as well as provides attach/detach events." | |||
| """ | |||
| # Factory events | |||
| on_attached = event.Event('Called when an AD2USB device has been detected.') | |||
| on_detached = event.Event('Called when an AD2USB device has been removed.') | |||
| __devices = [] | |||
| @classmethod | |||
| def find_all(cls): | |||
| """ | |||
| Returns all AD2USB devices located on the system. | |||
| :returns: list of devices found | |||
| :raises: CommError | |||
| """ | |||
| cls.__devices = USBDevice.find_all() | |||
| 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 create(cls, device=None): | |||
| """ | |||
| Factory method that returns the requested AD2USB device, or the first device. | |||
| :param device: Tuple describing the USB device to open, as returned by find_all(). | |||
| :type device: tuple | |||
| :returns: AD2USB object utilizing the specified device. | |||
| :raises: NoDeviceError | |||
| """ | |||
| 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 | |||
| device = USBDevice(interface=sernum) | |||
| return AD2(device) | |||
| def __init__(self, attached_event=None, detached_event=None): | |||
| """ | |||
| Constructor | |||
| :param attached_event: Event to trigger when a device is attached. | |||
| :type attached_event: function | |||
| :param detached_event: Event to trigger when a device is detached. | |||
| :type detached_event: function | |||
| """ | |||
| self._detect_thread = AD2Factory.DetectThread(self) | |||
| if attached_event: | |||
| self.on_attached += attached_event | |||
| if detached_event: | |||
| self.on_detached += detached_event | |||
| AD2Factory.find_all() | |||
| self.start() | |||
| def close(self): | |||
| """ | |||
| Clean up and shut down. | |||
| """ | |||
| self.stop() | |||
| def start(self): | |||
| """ | |||
| Starts the detection thread, if not already running. | |||
| """ | |||
| if not self._detect_thread.is_alive(): | |||
| self._detect_thread.start() | |||
| def stop(self): | |||
| """ | |||
| Stops the detection thread. | |||
| """ | |||
| self._detect_thread.stop() | |||
| def get_device(self, device=None): | |||
| """ | |||
| Factory method that returns the requested AD2USB device, or the first device. | |||
| :param device: Tuple describing the USB device to open, as returned by find_all(). | |||
| :type device: tuple | |||
| """ | |||
| return AD2Factory.create(device) | |||
| class DetectThread(threading.Thread): | |||
| """ | |||
| Thread that handles detection of added/removed devices. | |||
| """ | |||
| def __init__(self, factory): | |||
| """ | |||
| Constructor | |||
| :param factory: AD2Factory object to use with the thread. | |||
| :type factory: AD2Factory | |||
| """ | |||
| threading.Thread.__init__(self) | |||
| self._factory = factory | |||
| 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: | |||
| AD2Factory.find_all() | |||
| current_devices = set(AD2Factory.devices()) | |||
| new_devices = [d for d in current_devices if d not in last_devices] | |||
| removed_devices = [d for d in last_devices if d not in current_devices] | |||
| last_devices = current_devices | |||
| for d in new_devices: | |||
| self._factory.on_attached(device=d) | |||
| for d in removed_devices: | |||
| self._factory.on_detached(device=d) | |||
| except CommError, err: | |||
| pass | |||
| time.sleep(0.25) | |||
| class AD2(object): | |||
| """ | |||
| High-level wrapper around AD2 devices. | |||
| @@ -142,23 +142,84 @@ class USBDevice(Device): | |||
| BAUDRATE = 115200 | |||
| """Default baudrate for AD2USB devices.""" | |||
| @staticmethod | |||
| def find_all(): | |||
| __devices = [] | |||
| @classmethod | |||
| def find_all(cls, vid=FTDI_VENDOR_ID, pid=FTDI_PRODUCT_ID): | |||
| """ | |||
| Returns all FTDI devices matching our vendor and product IDs. | |||
| :returns: list of devices | |||
| :raises: CommError | |||
| """ | |||
| devices = [] | |||
| cls.__devices = [] | |||
| try: | |||
| devices = Ftdi.find_all([(USBDevice.FTDI_VENDOR_ID, USBDevice.FTDI_PRODUCT_ID)], nocache=True) | |||
| cls.__devices = Ftdi.find_all([(vid, pid)], nocache=True) | |||
| except (usb.core.USBError, FtdiError), err: | |||
| raise CommError('Error enumerating AD2USB devices: {0}'.format(str(err)), err) | |||
| return devices | |||
| 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 USBDevice device, or the first device. | |||
| :param device: Tuple describing the USB device to open, as returned by find_all(). | |||
| :type device: tuple | |||
| :returns: USBDevice object utilizing the specified device. | |||
| :raises: NoDeviceError | |||
| """ | |||
| 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) | |||
| @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. | |||
| :type on_attached: function | |||
| :param on_detached: function to be called when a device is detached. | |||
| :type on_detached: function | |||
| """ | |||
| cls.__detect_thread = USBDevice.DetectThread(on_attached, on_detached) | |||
| cls.find_all() | |||
| cls.__detect_thread.start() | |||
| @classmethod | |||
| def stop_detection(cls): | |||
| """ | |||
| Stops the device detection thread. | |||
| """ | |||
| try: | |||
| cls.__detect_thread.stop() | |||
| except: | |||
| pass | |||
| @property | |||
| def interface(self): | |||
| @@ -267,7 +328,10 @@ class USBDevice(Device): | |||
| self._device.set_baudrate(baudrate) | |||
| self._id = 'USB {0}:{1}'.format(self._device.usb_dev.bus, self._device.usb_dev.address) | |||
| if not self._serial_number: | |||
| self._serial_number = self._get_serial_number() | |||
| self._id = self._serial_number | |||
| except (usb.core.USBError, FtdiError), err: | |||
| raise NoDeviceError('Error opening device: {0}'.format(str(err)), err) | |||
| @@ -395,6 +459,71 @@ class USBDevice(Device): | |||
| return ret | |||
| 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('Called when an AD2USB device has been detected.') | |||
| on_detached = event.Event('Called when an AD2USB device has been removed.') | |||
| def __init__(self, on_attached=None, on_detached=None): | |||
| """ | |||
| Constructor | |||
| :param factory: AD2Factory object to use with the thread. | |||
| :type factory: AD2Factory | |||
| """ | |||
| 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()) | |||
| new_devices = [d for d in current_devices if d not in last_devices] | |||
| removed_devices = [d for d in last_devices if d not in current_devices] | |||
| last_devices = current_devices | |||
| for d in new_devices: | |||
| self.on_attached(device=d) | |||
| for d in removed_devices: | |||
| self.on_detached(device=d) | |||
| except CommError, err: | |||
| pass | |||
| time.sleep(0.25) | |||
| class SerialDevice(Device): | |||
| """ | |||
| @@ -3,70 +3,12 @@ import time | |||
| from unittest import TestCase | |||
| from mock import Mock, MagicMock, patch | |||
| from ..ad2 import AD2Factory, AD2 | |||
| from ..ad2 import AD2 | |||
| from ..devices import USBDevice | |||
| from ..messages import Message, RFMessage, LRRMessage, ExpanderMessage | |||
| from ..event.event import Event, EventHandler | |||
| from ..zonetracking import Zonetracker | |||
| class TestAD2Factory(TestCase): | |||
| def setUp(self): | |||
| self._attached = False | |||
| self._detached = False | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2', 1, 'AD2')]): | |||
| self._factory = AD2Factory() | |||
| def tearDown(self): | |||
| self._factory.stop() | |||
| def attached_event(self, sender, *args, **kwargs): | |||
| self._attached = True | |||
| def detached_event(self, sender, *args, **kwargs): | |||
| self._detached = True | |||
| def test_find_all(self): | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2', 1, 'AD2')]): | |||
| devices = AD2Factory.find_all() | |||
| self.assertEquals(devices[0][2], 'AD2') | |||
| def test_create_default_param(self): | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2', 1, 'AD2')]): | |||
| device = AD2Factory.create() | |||
| self.assertEquals(device._device.interface, 'AD2') | |||
| def test_create_with_param(self): | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2-1', 1, 'AD2'), (0, 0, 'AD2-2', 1, 'AD2')]): | |||
| device = AD2Factory.create((0, 0, 'AD2-1', 1, 'AD2')) | |||
| self.assertEquals(device._device.interface, 'AD2-1') | |||
| device = AD2Factory.create((0, 0, 'AD2-2', 1, 'AD2')) | |||
| self.assertEquals(device._device.interface, 'AD2-2') | |||
| def test_events(self): | |||
| self.assertEquals(self._attached, False) | |||
| self.assertEquals(self._detached, False) | |||
| # this is ugly, but it works. | |||
| self._factory.stop() | |||
| self._factory._detect_thread = AD2Factory.DetectThread(self._factory) | |||
| self._factory.on_attached += self.attached_event | |||
| self._factory.on_detached += self.detached_event | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2-1', 1, 'AD2'), (0, 0, 'AD2-2', 1, 'AD2')]): | |||
| self._factory.start() | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2-2', 1, 'AD2')]): | |||
| AD2Factory.find_all() | |||
| time.sleep(1) | |||
| self._factory.stop() | |||
| self.assertEquals(self._attached, True) | |||
| self.assertEquals(self._detached, True) | |||
| class TestAD2(TestCase): | |||
| def setUp(self): | |||
| self._panicked = False | |||
| @@ -4,6 +4,7 @@ from serial import Serial, SerialException | |||
| from pyftdi.pyftdi.ftdi import Ftdi, FtdiError | |||
| from usb.core import USBError, Device as USBCoreDevice | |||
| import socket | |||
| import time | |||
| from OpenSSL import SSL, crypto | |||
| from ..devices import USBDevice, SerialDevice, SocketDevice | |||
| from ..util import NoDeviceError, CommError, TimeoutError | |||
| @@ -17,9 +18,48 @@ class TestUSBDevice(TestCase): | |||
| self._device._device.usb_dev.bus = 0 | |||
| self._device._device.usb_dev.address = 0 | |||
| self._attached = False | |||
| self._detached = False | |||
| def tearDown(self): | |||
| self._device.close() | |||
| def attached_event(self, sender, *args, **kwargs): | |||
| self._attached = True | |||
| def detached_event(self, sender, *args, **kwargs): | |||
| self._detached = True | |||
| def test_find_default_param(self): | |||
| with patch.object(Ftdi, 'find_all', return_value=[(0, 0, 'AD2', 1, 'AD2')]): | |||
| device = USBDevice.find() | |||
| self.assertEquals(device.interface, 'AD2') | |||
| def test_find_with_param(self): | |||
| with patch.object(Ftdi, 'find_all', return_value=[(0, 0, 'AD2-1', 1, 'AD2'), (0, 0, 'AD2-2', 1, 'AD2')]): | |||
| device = USBDevice.find((0, 0, 'AD2-1', 1, 'AD2')) | |||
| self.assertEquals(device.interface, 'AD2-1') | |||
| device = USBDevice.find((0, 0, 'AD2-2', 1, 'AD2')) | |||
| self.assertEquals(device.interface, 'AD2-2') | |||
| def test_events(self): | |||
| self.assertEquals(self._attached, False) | |||
| self.assertEquals(self._detached, False) | |||
| # this is ugly, but it works. | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2-1', 1, 'AD2'), (0, 0, 'AD2-2', 1, 'AD2')]): | |||
| USBDevice.start_detection(on_attached=self.attached_event, on_detached=self.detached_event) | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2-2', 1, 'AD2')]): | |||
| USBDevice.find_all() | |||
| time.sleep(1) | |||
| USBDevice.stop_detection() | |||
| self.assertEquals(self._attached, True) | |||
| self.assertEquals(self._detached, True) | |||
| def test_find_all(self): | |||
| with patch.object(USBDevice, 'find_all', return_value=[]) as mock: | |||
| devices = USBDevice.find_all() | |||