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