| @@ -1,4 +1,4 @@ | |||||
| import ad2 | |||||
| from ad2 import AD2 | |||||
| import devices | import devices | ||||
| import util | import util | ||||
| import messages | import messages | ||||
| @@ -8,167 +8,10 @@ import time | |||||
| import threading | import threading | ||||
| from .event import event | from .event import event | ||||
| from .devices import USBDevice | |||||
| from .util import CommError, NoDeviceError | from .util import CommError, NoDeviceError | ||||
| from .messages import Message, ExpanderMessage, RFMessage, LRRMessage | from .messages import Message, ExpanderMessage, RFMessage, LRRMessage | ||||
| from .zonetracking import Zonetracker | 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): | class AD2(object): | ||||
| """ | """ | ||||
| High-level wrapper around AD2 devices. | High-level wrapper around AD2 devices. | ||||
| @@ -142,23 +142,84 @@ class USBDevice(Device): | |||||
| BAUDRATE = 115200 | BAUDRATE = 115200 | ||||
| """Default baudrate for AD2USB devices.""" | """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 all FTDI devices matching our vendor and product IDs. | ||||
| :returns: list of devices | :returns: list of devices | ||||
| :raises: CommError | :raises: CommError | ||||
| """ | """ | ||||
| devices = [] | |||||
| cls.__devices = [] | |||||
| try: | 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: | except (usb.core.USBError, FtdiError), err: | ||||
| raise CommError('Error enumerating AD2USB devices: {0}'.format(str(err)), 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 | @property | ||||
| def interface(self): | def interface(self): | ||||
| @@ -267,7 +328,10 @@ class USBDevice(Device): | |||||
| self._device.set_baudrate(baudrate) | 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: | except (usb.core.USBError, FtdiError), err: | ||||
| raise NoDeviceError('Error opening device: {0}'.format(str(err)), err) | raise NoDeviceError('Error opening device: {0}'.format(str(err)), err) | ||||
| @@ -395,6 +459,71 @@ class USBDevice(Device): | |||||
| return ret | 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): | class SerialDevice(Device): | ||||
| """ | """ | ||||
| @@ -3,70 +3,12 @@ import time | |||||
| from unittest import TestCase | from unittest import TestCase | ||||
| from mock import Mock, MagicMock, patch | from mock import Mock, MagicMock, patch | ||||
| from ..ad2 import AD2Factory, AD2 | |||||
| from ..ad2 import AD2 | |||||
| from ..devices import USBDevice | from ..devices import USBDevice | ||||
| from ..messages import Message, RFMessage, LRRMessage, ExpanderMessage | from ..messages import Message, RFMessage, LRRMessage, ExpanderMessage | ||||
| from ..event.event import Event, EventHandler | from ..event.event import Event, EventHandler | ||||
| from ..zonetracking import Zonetracker | 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): | class TestAD2(TestCase): | ||||
| def setUp(self): | def setUp(self): | ||||
| self._panicked = False | self._panicked = False | ||||
| @@ -4,6 +4,7 @@ from serial import Serial, SerialException | |||||
| from pyftdi.pyftdi.ftdi import Ftdi, FtdiError | from pyftdi.pyftdi.ftdi import Ftdi, FtdiError | ||||
| from usb.core import USBError, Device as USBCoreDevice | from usb.core import USBError, Device as USBCoreDevice | ||||
| import socket | import socket | ||||
| import time | |||||
| from OpenSSL import SSL, crypto | from OpenSSL import SSL, crypto | ||||
| from ..devices import USBDevice, SerialDevice, SocketDevice | from ..devices import USBDevice, SerialDevice, SocketDevice | ||||
| from ..util import NoDeviceError, CommError, TimeoutError | 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.bus = 0 | ||||
| self._device._device.usb_dev.address = 0 | self._device._device.usb_dev.address = 0 | ||||
| self._attached = False | |||||
| self._detached = False | |||||
| def tearDown(self): | def tearDown(self): | ||||
| self._device.close() | 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): | def test_find_all(self): | ||||
| with patch.object(USBDevice, 'find_all', return_value=[]) as mock: | with patch.object(USBDevice, 'find_all', return_value=[]) as mock: | ||||
| devices = USBDevice.find_all() | devices = USBDevice.find_all() | ||||