diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..68839a3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - "3.5" + - "3.4" + - "3.3" + - "3.2" + - "2.7" + - "pypy" + - "pypy3" +install: "pip install -e .[test]" +script: make test diff --git a/Makefile b/Makefile index 3c3d3e1..bbda07a 100644 --- a/Makefile +++ b/Makefile @@ -98,7 +98,7 @@ develop: tags @# These have to be done separately to avoid a cockup... $(PIP) install -U setuptools $(PIP) install -U pip - $(PIP) install -e . + $(PIP) install -e .[doc,test] test: $(COVERAGE) run -m $(PYTEST) tests -v diff --git a/coverage.cfg b/coverage.cfg new file mode 100644 index 0000000..54de190 --- /dev/null +++ b/coverage.cfg @@ -0,0 +1,20 @@ +[run] +branch = True +include = gpiozero/* +;omit = */bar.py,*/baz.py + +[report] +ignore_errors = True +show_missing = True +exclude_lines = + pragma: no cover + def __repr__ + if self\.debug + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + +[html] +directory = coverage + diff --git a/docs/api_exc.rst b/docs/api_exc.rst new file mode 100644 index 0000000..b0754ae --- /dev/null +++ b/docs/api_exc.rst @@ -0,0 +1,80 @@ +========== +Exceptions +========== + +.. currentmodule:: gpiozero + +The following exceptions are defined by GPIO Zero. Please note that multiple +inheritance is heavily used in the exception hierarchy to make testing for +exceptions easier. For example, to capture any exception generated by GPIO +Zero's code:: + + from gpiozero import * + + led = PWMLED(17) + try: + led.value = 2 + except GPIOZeroError: + print('A GPIO Zero error occurred') + +Since all GPIO Zero's exceptions descend from :exc:`GPIOZeroError`, this will +work. However, certain specific errors have multiple parents. For example, in +the case that an out of range value is passed to :attr:`OutputDevice.value` you +would expect a :exc:`ValueError` to be raised. In fact, a +:exc:`OutputDeviceBadValue` error will be raised. However, note that this +descends from both :exc:`GPIOZeroError` (indirectly) and from :exc:`ValueError` +so you can still do:: + + from gpiozero import * + + led = PWMLED(17) + try: + led.value = 2 + except ValueError: + print('Bad value specified') + + +.. autoexception:: GPIOZeroError + +.. autoexception:: CompositeDeviceError + +.. autoexception:: GPIODeviceError + +.. autoexception:: GPIODeviceClosed + +.. autoexception:: GPIOPinInUse + +.. autoexception:: GPIOPinMissing + +.. autoexception:: GPIOBadQueueLen + +.. autoexception:: InputDeviceError + +.. autoexception:: OutputDeviceError + +.. autoexception:: OutputDeviceBadValue + +.. autoexception:: PinError + +.. autoexception:: PinFixedFunction + +.. autoexception:: PinInvalidFunction + +.. autoexception:: PinInvalidState + +.. autoexception:: PinInvalidPull + +.. autoexception:: PinInvalidEdges + +.. autoexception:: PinSetInput + +.. autoexception:: PinFixedPull + +.. autoexception:: PinEdgeDetectUnsupported + +.. autoexception:: PinPWMError + +.. autoexception:: PinPWMUnsupported + +.. autoexception:: PinPWMFixedValue + diff --git a/docs/index.rst b/docs/index.rst index ec082e6..4f16c12 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,5 +13,6 @@ Table of Contents api_boards api_generic api_pins + api_exc changelog license diff --git a/gpiozero/__init__.py b/gpiozero/__init__.py index e78ade6..b8de050 100644 --- a/gpiozero/__init__.py +++ b/gpiozero/__init__.py @@ -5,7 +5,20 @@ from __future__ import ( division, ) -from .pins.exc import ( +from .pins import ( + Pin, +) +from .exc import ( + GPIOZeroError, + CompositeDeviceError, + GPIODeviceError, + GPIODeviceClosed, + GPIOPinInUse, + GPIOPinMissing, + GPIOBadQueueLen, + InputDeviceError, + OutputDeviceError, + OutputDeviceBadValue, PinError, PinFixedFunction, PinInvalidFunction, @@ -19,15 +32,6 @@ from .pins.exc import ( PinPWMUnsupported, PinPWMFixedValue, ) -from .pins import ( - Pin, -) -from .exc import ( - GPIODeviceClosed, - GPIODeviceError, - InputDeviceError, - OutputDeviceError, -) from .devices import ( GPIODevice, CompositeDevice, diff --git a/gpiozero/compat.py b/gpiozero/compat.py new file mode 100644 index 0000000..c09d3db --- /dev/null +++ b/gpiozero/compat.py @@ -0,0 +1,27 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + +import cmath + + +# Back-ported from python 3.5; see +# github.com/PythonCHB/close_pep/blob/master/is_close.py for original +# implementation +def isclose(a, b, rel_tol=1e-9, abs_tol=0.0): + if rel_tol < 0.0 or abs_tol < 0.0: + raise ValueError('error tolerances must be non-negative') + if a == b: # fast-path for exact equality + return True + if cmath.isinf(a) or cmath.isinf(b): + return False + diff = abs(b - a) + return ( + (diff <= abs(rel_tol * b)) or + (diff <= abs(rel_tol * a)) or + (diff <= abs_tol) + ) diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 8153912..78813ce 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -13,7 +13,12 @@ from threading import Thread, Event, RLock from collections import deque from types import FunctionType -from .exc import GPIODeviceError, GPIODeviceClosed, InputDeviceError +from .exc import ( + GPIOPinMissing, + GPIOPinInUse, + GPIODeviceClosed, + GPIOBadQueueLen, + ) # Get a pin implementation to use as the default; we prefer RPi.GPIO's here # as it supports PWM, and all Pi revisions. If no third-party libraries are @@ -203,7 +208,8 @@ class GPIODevice(ValuesMixin, GPIOBase): :param int pin: The GPIO pin (in BCM numbering) that the device is connected to. If - this is ``None`` a :exc:`GPIODeviceError` will be raised. + this is ``None``, :exc:`GPIOPinMissing` will be raised. If the pin is + already in use by another device, :exc:`GPIOPinInUse` will be raised. """ def __init__(self, pin=None): super(GPIODevice, self).__init__() @@ -212,12 +218,12 @@ class GPIODevice(ValuesMixin, GPIOBase): # value of pin until we've verified that it isn't already allocated self._pin = None if pin is None: - raise GPIODeviceError('No pin given') + raise GPIOPinMissing('No pin given') if isinstance(pin, int): pin = DefaultPin(pin) with _PINS_LOCK: if pin in _PINS: - raise GPIODeviceError( + raise GPIOPinInUse( 'pin %r is already in use by another gpiozero object' % pin ) _PINS.add(pin) @@ -342,7 +348,7 @@ class GPIOQueue(GPIOThread): assert isinstance(parent, GPIODevice) super(GPIOQueue, self).__init__(target=self.fill) if queue_len < 1: - raise InputDeviceError('queue_len must be at least one') + raise GPIOBadQueueLen('queue_len must be at least one') self.queue = deque(maxlen=queue_len) self.partial = partial self.sample_wait = sample_wait diff --git a/gpiozero/exc.py b/gpiozero/exc.py index b3b4e0e..f2de8c1 100644 --- a/gpiozero/exc.py +++ b/gpiozero/exc.py @@ -4,16 +4,72 @@ from __future__ import ( absolute_import, division, ) +str = type('') -class GPIODeviceError(Exception): - pass + +class GPIOZeroError(Exception): + "Base class for all exceptions in GPIO Zero" + +class CompositeDeviceError(GPIOZeroError): + "Base class for errors specific to the CompositeDevice hierarchy" + +class GPIODeviceError(GPIOZeroError): + "Base class for errors specific to the GPIODevice hierarchy" class GPIODeviceClosed(GPIODeviceError): - pass + "Error raised when an operation is attempted on a closed device" + +class GPIOPinInUse(GPIODeviceError): + "Error raised when attempting to use a pin already in use by another device" + +class GPIOPinMissing(GPIODeviceError, ValueError): + "Error raised when a pin number is not specified" + +class GPIOBadQueueLen(GPIODeviceError, ValueError): + "Error raised when non-positive queue length is specified" class InputDeviceError(GPIODeviceError): - pass + "Base class for errors specific to the InputDevice hierarchy" class OutputDeviceError(GPIODeviceError): - pass + "Base class for errors specified to the OutputDevice hierarchy" + +class OutputDeviceBadValue(OutputDeviceError, ValueError): + "Error raised when ``value`` is set to an invalid value" + +class PinError(GPIOZeroError): + "Base class for errors related to pin implementations" + +class PinFixedFunction(PinError, AttributeError): + "Error raised when attempting to change the function of a fixed type pin" + +class PinInvalidFunction(PinError, ValueError): + "Error raised when attempting to change the function of a pin to an invalid value" + +class PinInvalidState(PinError, ValueError): + "Error raised when attempting to assign an invalid state to a pin" + +class PinInvalidPull(PinError, ValueError): + "Error raised when attempting to assign an invalid pull-up to a pin" + +class PinInvalidEdges(PinError, ValueError): + "Error raised when attempting to assign an invalid edge detection to a pin" + +class PinSetInput(PinError, AttributeError): + "Error raised when attempting to set a read-only pin" + +class PinFixedPull(PinError, AttributeError): + "Error raised when attempting to set the pull of a pin with fixed pull-up" + +class PinEdgeDetectUnsupported(PinError, AttributeError): + "Error raised when attempting to use edge detection on unsupported pins" + +class PinPWMError(PinError): + "Base class for errors related to PWM implementations" + +class PinPWMUnsupported(PinPWMError, AttributeError): + "Error raised when attempting to activate PWM on unsupported pins" + +class PinPWMFixedValue(PinPWMError, AttributeError): + "Error raised when attempting to initialize PWM on an input pin" diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index c1968df..0bb9875 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -10,7 +10,7 @@ from time import sleep from threading import Lock from itertools import repeat, cycle, chain -from .exc import OutputDeviceError, GPIODeviceError, GPIODeviceClosed +from .exc import OutputDeviceBadValue, GPIOPinMissing, GPIODeviceClosed from .devices import GPIODevice, GPIOThread, CompositeDevice, SourceMixin @@ -24,7 +24,7 @@ class OutputDevice(SourceMixin, GPIODevice): :param int pin: The GPIO pin (in BCM numbering) that the device is connected to. If - this is ``None`` a :exc:`GPIODeviceError` will be raised. + this is ``None`` a :exc:`GPIOPinMissing` will be raised. :param bool active_high: If ``True`` (the default), the :meth:`on` method will set the GPIO to @@ -54,7 +54,7 @@ class OutputDevice(SourceMixin, GPIODevice): value = not value try: self.pin.state = bool(value) - except ValueError: + except AttributeError: self._check_open() raise @@ -269,7 +269,7 @@ class PWMOutputDevice(OutputDevice): def __init__(self, pin=None, active_high=True, initial_value=0, frequency=100): self._blink_thread = None if not 0 <= initial_value <= 1: - raise OutputDeviceError("initial_value must be between 0 and 1") + raise OutputDeviceBadValue("initial_value must be between 0 and 1") super(PWMOutputDevice, self).__init__(pin, active_high) try: # XXX need a way of setting these together @@ -299,7 +299,7 @@ class PWMOutputDevice(OutputDevice): if not self.active_high: value = 1 - value if not 0 <= value <= 1: - raise OutputDeviceError("PWM value must be between 0 and 1") + raise OutputDeviceBadValue("PWM value must be between 0 and 1") try: self.pin.state = value except AttributeError: @@ -505,7 +505,7 @@ class RGBLED(SourceMixin, CompositeDevice): self._leds = () self._blink_thread = None if not all([red, green, blue]): - raise OutputDeviceError('red, green, and blue pins must be provided') + raise GPIOPinMissing('red, green, and blue pins must be provided') super(RGBLED, self).__init__() self._leds = tuple(PWMLED(pin, active_high) for pin in (red, green, blue)) self.value = initial_value @@ -680,7 +680,7 @@ class Motor(SourceMixin, CompositeDevice): """ def __init__(self, forward=None, backward=None): if not all([forward, backward]): - raise OutputDeviceError( + raise GPIOPinMissing( 'forward and backward pins must be provided' ) super(Motor, self).__init__() @@ -722,7 +722,7 @@ class Motor(SourceMixin, CompositeDevice): @value.setter def value(self, value): if not -1 <= value <= 1: - raise OutputDeviceError("Motor value must be between -1 and 1") + raise OutputDeviceBadValue("Motor value must be between -1 and 1") if value > 0: self.forward(value) elif value < 0: diff --git a/gpiozero/pins/__init__.py b/gpiozero/pins/__init__.py index 6d9c1a4..d5e3389 100644 --- a/gpiozero/pins/__init__.py +++ b/gpiozero/pins/__init__.py @@ -6,7 +6,7 @@ from __future__ import ( ) str = type('') -from .exc import ( +from ..exc import ( PinFixedFunction, PinSetInput, PinFixedPull, diff --git a/gpiozero/pins/exc.py b/gpiozero/pins/exc.py deleted file mode 100644 index 5e27aba..0000000 --- a/gpiozero/pins/exc.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - - -class PinError(Exception): - "Base class for errors related to pin implementations" - -class PinFixedFunction(PinError, AttributeError): - "Error raised when attempting to change the function of a fixed type pin" - -class PinInvalidFunction(PinError, ValueError): - "Error raised when attempting to change the function of a pin to an invalid value" - -class PinInvalidState(PinError, ValueError): - "Error raised when attempting to assign an invalid state to a pin" - -class PinInvalidPull(PinError, ValueError): - "Error raised when attempting to assign an invalid pull-up to a pin" - -class PinInvalidEdges(PinError, ValueError): - "Error raised when attempting to assign an invalid edge detection to a pin" - -class PinSetInput(PinError, AttributeError): - "Error raised when attempting to set a read-only pin" - -class PinFixedPull(PinError, AttributeError): - "Error raised when attempting to set the pull of a pin with fixed pull-up" - -class PinEdgeDetectUnsupported(PinError, AttributeError): - "Error raised when attempting to use edge detection on unsupported pins" - -class PinPWMError(PinError): - "Base class for errors related to PWM implementations" - -class PinPWMUnsupported(PinPWMError, AttributeError): - "Error raised when attempting to activate PWM on unsupported pins" - -class PinPWMFixedValue(PinPWMError, AttributeError): - "Error raised when attempting to initialize PWM on an input pin" - diff --git a/gpiozero/pins/mock.py b/gpiozero/pins/mock.py new file mode 100644 index 0000000..aa6afa5 --- /dev/null +++ b/gpiozero/pins/mock.py @@ -0,0 +1,171 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +from collections import namedtuple +from time import time +try: + from math import isclose +except ImportError: + from ..compat import isclose + +from . import Pin, PINS_CLEANUP +from ..exc import PinSetInput, PinPWMUnsupported + + +PinState = namedtuple('PinState', ('timestamp', 'state')) + +class MockPin(Pin): + """ + A mock pin used primarily for testing. This class does *not* support PWM. + """ + + def __init__(self, number): + if not (0 <= number < 54): + raise ValueError('invalid pin %d specified (must be 0..53)' % number) + self._number = number + self._function = 'input' + self._state = False + self._pull = 'floating' + self._bounce = None + self._edges = 'both' + self._when_changed = None + self._last_change = time() + self.states = [PinState(0.0, False)] + + def __repr__(self): + return 'MOCK%d' % self._number + + @property + def number(self): + return self._number + + def close(self): + self.when_changed = None + self.function = 'input' + + def _get_function(self): + return self._function + + def _set_function(self, value): + assert value in ('input', 'output') + self._function = value + if value == 'input': + # Drive the input to the pull + self._set_pull(self._get_pull()) + + def _get_state(self): + return self._state + + def _set_state(self, value): + if self._function == 'input': + raise PinSetInput() + assert self._function == 'output' + assert 0 <= value <= 1 + if self._state != value: + t = time() + self._state = value + self.states.append(PinState(t - self._last_change, value)) + self._last_change = t + + def _get_frequency(self): + return None + + def _set_frequency(self, value): + raise PinPWMUnsupported() + + def _get_pull(self): + return self._pull + + def _set_pull(self, value): + assert self._function == 'input' + assert value in ('floating', 'up', 'down') + self._pull = value + if value == 'up': + self.drive_high() + elif value == 'down': + self.drive_low() + + def _get_bounce(self): + return self._bounce + + def _set_bounce(self, value): + # XXX Need to implement this + self._bounce = value + + def _get_edges(self): + return self._edges + + def _set_edges(self, value): + assert value in ('none', 'falling', 'rising', 'both') + self._edges = value + + def _get_when_changed(self): + return self._when_changed + + def _set_when_changed(self, value): + self._when_changed = value + + def drive_high(self): + assert self._function == 'input' + if not self._state: + t = time() + self._state = True + self.states.append(PinState(t - self._last_change, True)) + self._last_change = t + if self._edges in ('both', 'rising') and self._when_changed is not None: + self._when_changed() + + def drive_low(self): + assert self._function == 'input' + if self._state: + t = time() + self._state = False + self.states.append(PinState(t - self._last_change, False)) + self._last_change = t + if self._edges in ('both', 'falling') and self._when_changed is not None: + self._when_changed() + + def clear_states(self): + self._last_change = time() + self.states = [PinState(0.0, self.state)] + + def assert_states(self, expected_states): + # Tests that the pin went through the expected states (a list of values) + for actual, expected in zip(self.states, expected_states): + assert actual.state == expected + + def assert_states_and_times(self, expected_states): + # Tests that the pin went through the expected states at the expected + # times (times are compared with a tolerance of tens-of-milliseconds as + # that's about all we can reasonably expect in a non-realtime + # environment on a Pi 1) + for actual, expected in zip(self.states, expected_states): + assert isclose(actual.timestamp, expected[0], rel_tol=0.01, abs_tol=0.01) + assert isclose(actual.state, expected[1]) + + +class MockPWMPin(MockPin): + """ + This derivative of :class:`MockPin` adds PWM support. + """ + + def __init__(self, number): + super(MockPWMPin, self).__init__(number) + self._frequency = None + + def _get_frequency(self): + return self._frequency + + def _set_frequency(self, value): + if value is not None: + assert self._function == 'output' + self._frequency = value + if value is None: + self.state = False + diff --git a/gpiozero/pins/native.py b/gpiozero/pins/native.py index 6bd6da2..4b1de95 100644 --- a/gpiozero/pins/native.py +++ b/gpiozero/pins/native.py @@ -17,7 +17,7 @@ from threading import Thread, Event, Lock from collections import Counter from . import Pin, PINS_CLEANUP -from .exc import ( +from ..exc import ( PinInvalidPull, PinInvalidEdges, PinInvalidFunction, diff --git a/gpiozero/pins/rpigpio.py b/gpiozero/pins/rpigpio.py index cf6edfb..5c1304f 100644 --- a/gpiozero/pins/rpigpio.py +++ b/gpiozero/pins/rpigpio.py @@ -9,7 +9,7 @@ str = type('') from RPi import GPIO from . import Pin -from .exc import ( +from ..exc import ( PinInvalidFunction, PinSetInput, PinFixedPull, diff --git a/gpiozero/pins/rpio.py b/gpiozero/pins/rpio.py index 6e00c72..7ec7f6d 100644 --- a/gpiozero/pins/rpio.py +++ b/gpiozero/pins/rpio.py @@ -13,7 +13,7 @@ import RPIO import RPIO.PWM from . import Pin, PINS_CLEANUP -from .exc import ( +from ..exc import ( PinInvalidFunction, PinSetInput, PinFixedPull, diff --git a/setup.py b/setup.py index 17c6362..21e2bc3 100644 --- a/setup.py +++ b/setup.py @@ -49,8 +49,18 @@ __requires__ = [ ] __extra_requires__ = { + 'doc': ['sphinx'], + 'test': ['pytest', 'coverage', 'mock'], } +if sys.version_info[:2] == (3, 2): + # Particular versions are required for Python 3.2 compatibility + __extra_requires__['doc'].extend([ + 'Jinja2<2.7', + 'MarkupSafe<0.16', + ]) + __extra_requires__['test'][1] = 'coverage<4.0dev' + __entry_points__ = { } diff --git a/tests/test_boards.py b/tests/test_boards.py new file mode 100644 index 0000000..fa1c6d1 --- /dev/null +++ b/tests/test_boards.py @@ -0,0 +1,17 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +import pytest + +from gpiozero.pins.mock import MockPin +from gpiozero import * + + +# TODO boards tests! + diff --git a/tests/test_devices.py b/tests/test_devices.py new file mode 100644 index 0000000..9dff553 --- /dev/null +++ b/tests/test_devices.py @@ -0,0 +1,17 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +import pytest + +from gpiozero.pins.mock import MockPin +from gpiozero import * + + +# TODO devices tests! + diff --git a/tests/test_inputs.py b/tests/test_inputs.py new file mode 100644 index 0000000..88c664b --- /dev/null +++ b/tests/test_inputs.py @@ -0,0 +1,16 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +import pytest + +from gpiozero.pins.mock import MockPin +from gpiozero import * + + +# TODO input_devices tests! diff --git a/tests/test_mock_pin.py b/tests/test_mock_pin.py new file mode 100644 index 0000000..77b0c46 --- /dev/null +++ b/tests/test_mock_pin.py @@ -0,0 +1,74 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +from threading import Event + +import pytest + +from gpiozero.pins.mock import MockPin, MockPWMPin +from gpiozero import * + + +# Some rough tests to make sure our MockPin is up to snuff. This is just +# enough to get reasonable coverage but it's by no means comprehensive... + +def test_mock_pin_init(): + with pytest.raises(ValueError): + MockPin(60) + assert MockPin(2).number == 2 + +def test_mock_pin_frequency_unsupported(): + with pytest.raises(AttributeError): + pin = MockPin(3) + pin.frequency = 100 + +def test_mock_pin_frequency_supported(): + pin = MockPWMPin(3) + pin.function = 'output' + assert pin.frequency is None + pin.frequency = 100 + pin.state = 0.5 + pin.frequency = None + assert not pin.state + +def test_mock_pin_pull(): + pin = MockPin(4) + pin.function = 'input' + assert pin.pull == 'floating' + pin.pull = 'up' + assert pin.state + pin.pull = 'down' + assert not pin.state + +def test_mock_pin_edges(): + pin = MockPin(5) + assert pin.when_changed is None + fired = Event() + pin.function = 'input' + pin.edges = 'both' + assert pin.edges == 'both' + pin.drive_low() + assert not pin.state + def changed(): + fired.set() + pin.when_changed = changed + pin.drive_high() + assert pin.state + assert fired.wait(0) + fired.clear() + pin.edges = 'falling' + pin.drive_low() + assert not pin.state + assert fired.wait(0) + fired.clear() + pin.drive_high() + assert pin.state + assert not fired.wait(0) + assert pin.edges == 'falling' + diff --git a/tests/test_outputs.py b/tests/test_outputs.py new file mode 100644 index 0000000..2f3daa1 --- /dev/null +++ b/tests/test_outputs.py @@ -0,0 +1,460 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +try: + from math import isclose +except ImportError: + from gpiozero.compat import isclose + +import pytest +from time import sleep + +from gpiozero.pins.mock import MockPin, MockPWMPin +from gpiozero import * + + +def test_output_initial_values(): + pin = MockPin(2) + device = OutputDevice(pin, initial_value=False) + assert pin.function == 'output' + assert not pin.state + device.close() + device = OutputDevice(pin, initial_value=True) + assert pin.state + device.close() + state = pin.state + device = OutputDevice(pin, initial_value=None) + assert state == pin.state + +def test_output_write_active_high(): + pin = MockPin(2) + device = OutputDevice(pin) + device.on() + assert pin.state + device.off() + assert not pin.state + +def test_output_write_active_low(): + pin = MockPin(2) + device = OutputDevice(pin, active_high=False) + device.on() + assert not pin.state + device.off() + assert pin.state + +def test_output_write_closed(): + device = OutputDevice(MockPin(2)) + device.close() + with pytest.raises(GPIODeviceClosed): + device.on() + +def test_output_write_silly(): + pin = MockPin(2) + device = OutputDevice(pin) + pin.function = 'input' + with pytest.raises(AttributeError): + device.on() + +def test_output_value(): + pin = MockPin(2) + device = OutputDevice(pin) + assert not device.value + assert not pin.state + device.on() + assert device.value + assert pin.state + device.value = False + assert not device.value + assert not pin.state + +def test_output_digital_toggle(): + pin = MockPin(2) + device = DigitalOutputDevice(pin) + assert not device.value + assert not pin.state + device.toggle() + assert device.value + assert pin.state + device.toggle() + assert not device.value + assert not pin.state + +def test_output_blink_background(): + pin = MockPin(2) + device = DigitalOutputDevice(pin) + device.blink(0.1, 0.1, n=2) + device._blink_thread.join() # naughty, but ensures no arbitrary waits in the test + pin.assert_states_and_times([ + (0.0, False), + (0.0, True), + (0.1, False), + (0.1, True), + (0.1, False) + ]) + +def test_output_blink_foreground(): + pin = MockPin(2) + device = DigitalOutputDevice(pin) + device.blink(0.1, 0.1, n=2, background=False) + pin.assert_states_and_times([ + (0.0, False), + (0.0, True), + (0.1, False), + (0.1, True), + (0.1, False) + ]) + +def test_output_blink_interrupt_on(): + pin = MockPin(2) + device = DigitalOutputDevice(pin) + device.blink(1, 0.1) + sleep(0.2) + device.off() # should interrupt while on + pin.assert_states([False, True, False]) + +def test_output_blink_interrupt_off(): + pin = MockPin(2) + device = DigitalOutputDevice(pin) + device.blink(0.1, 1) + sleep(0.2) + device.off() # should interrupt while off + pin.assert_states([False, True, False]) + +def test_output_pwm_bad_initial_value(): + with pytest.raises(ValueError): + PWMOutputDevice(MockPin(2), initial_value=2) + +def test_output_pwm_not_supported(): + with pytest.raises(AttributeError): + PWMOutputDevice(MockPin(2)) + +def test_output_pwm_states(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin) + device.value = 0.1 + device.value = 0.2 + device.value = 0.0 + pin.assert_states([0.0, 0.1, 0.2, 0.0]) + +def test_output_pwm_read(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin, frequency=100) + assert device.frequency == 100 + device.value = 0.1 + assert isclose(device.value, 0.1) + assert isclose(pin.state, 0.1) + assert device.is_active + device.frequency = None + assert not device.value + assert not device.is_active + assert device.frequency is None + +def test_output_pwm_write(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin) + device.on() + device.off() + pin.assert_states([False, True, False]) + +def test_output_pwm_toggle(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin) + device.toggle() + device.value = 0.5 + device.value = 0.1 + device.toggle() + device.off() + pin.assert_states([False, True, 0.5, 0.1, 0.9, False]) + +def test_output_pwm_active_high_read(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin, active_high=False) + device.value = 0.1 + assert isclose(device.value, 0.1) + assert isclose(pin.state, 0.9) + device.frequency = None + assert device.value + +def test_output_pwm_bad_value(): + with pytest.raises(ValueError): + PWMOutputDevice(MockPWMPin(2)).value = 2 + +def test_output_pwm_write_closed(): + device = PWMOutputDevice(MockPWMPin(2)) + device.close() + with pytest.raises(GPIODeviceClosed): + device.on() + +def test_output_pwm_write_silly(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin) + pin.function = 'input' + with pytest.raises(AttributeError): + device.off() + +def test_output_pwm_blink_background(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin) + device.blink(0.1, 0.1, n=2) + device._blink_thread.join() + pin.assert_states_and_times([ + (0.0, 0), + (0.0, 1), + (0.1, 0), + (0.1, 1), + (0.1, 0) + ]) + +def test_output_pwm_blink_foreground(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin) + device.blink(0.1, 0.1, n=2, background=False) + pin.assert_states_and_times([ + (0.0, 0), + (0.0, 1), + (0.1, 0), + (0.1, 1), + (0.1, 0) + ]) + +def test_output_pwm_fade_background(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin) + device.blink(0, 0, 0.1, 0.1, n=2) + device._blink_thread.join() + pin.assert_states_and_times([ + (0.0, 0), + (0.02, 0.2), + (0.02, 0.4), + (0.02, 0.6), + (0.02, 0.8), + (0.02, 1), + (0.02, 0.8), + (0.02, 0.6), + (0.02, 0.4), + (0.02, 0.2), + (0.02, 0), + (0.02, 0.2), + (0.02, 0.4), + (0.02, 0.6), + (0.02, 0.8), + (0.02, 1), + (0.02, 0.8), + (0.02, 0.6), + (0.02, 0.4), + (0.02, 0.2), + (0.02, 0), + ]) + +def test_output_pwm_fade_foreground(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin) + device.blink(0, 0, 0.1, 0.1, n=2, background=False) + pin.assert_states_and_times([ + (0.0, 0), + (0.02, 0.2), + (0.02, 0.4), + (0.02, 0.6), + (0.02, 0.8), + (0.02, 1), + (0.02, 0.8), + (0.02, 0.6), + (0.02, 0.4), + (0.02, 0.2), + (0.02, 0), + (0.02, 0.2), + (0.02, 0.4), + (0.02, 0.6), + (0.02, 0.8), + (0.02, 1), + (0.02, 0.8), + (0.02, 0.6), + (0.02, 0.4), + (0.02, 0.2), + (0.02, 0), + ]) + +def test_output_pwm_blink_interrupt(): + pin = MockPWMPin(2) + device = PWMOutputDevice(pin) + device.blink(1, 0.1) + sleep(0.2) + device.off() # should interrupt while on + pin.assert_states([0, 1, 0]) + +def test_rgbled_missing_pins(): + with pytest.raises(ValueError): + RGBLED() + +def test_rgbled_initial_value(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + device = RGBLED(r, g, b, initial_value=(0.1, 0.2, 0)) + assert r.frequency + assert g.frequency + assert b.frequency + assert isclose(r.state, 0.1) + assert isclose(g.state, 0.2) + assert isclose(b.state, 0.0) + +def test_rgbled_value(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + device = RGBLED(r, g, b) + assert not device.is_active + assert device.value == (0, 0, 0) + device.on() + assert device.is_active + assert device.value == (1, 1, 1) + device.off() + assert not device.is_active + assert device.value == (0, 0, 0) + +def test_rgbled_toggle(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + device = RGBLED(r, g, b) + assert not device.is_active + assert device.value == (0, 0, 0) + device.toggle() + assert device.is_active + assert device.value == (1, 1, 1) + device.toggle() + assert not device.is_active + assert device.value == (0, 0, 0) + +def test_rgbled_blink_background(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + device = RGBLED(r, g, b) + device.blink(0.1, 0.1, n=2) + device._blink_thread.join() + expected = [ + (0.0, 0), + (0.0, 1), + (0.1, 0), + (0.1, 1), + (0.1, 0) + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +def test_rgbled_blink_foreground(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + device = RGBLED(r, g, b) + device.blink(0.1, 0.1, n=2, background=False) + expected = [ + (0.0, 0), + (0.0, 1), + (0.1, 0), + (0.1, 1), + (0.1, 0) + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +def test_rgbled_fade_background(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + device = RGBLED(r, g, b) + device.blink(0, 0, 0.1, 0.1, n=2) + device._blink_thread.join() + expected = [ + (0.0, 0), + (0.02, 0.2), + (0.02, 0.4), + (0.02, 0.6), + (0.02, 0.8), + (0.02, 1), + (0.02, 0.8), + (0.02, 0.6), + (0.02, 0.4), + (0.02, 0.2), + (0.02, 0), + (0.02, 0.2), + (0.02, 0.4), + (0.02, 0.6), + (0.02, 0.8), + (0.02, 1), + (0.02, 0.8), + (0.02, 0.6), + (0.02, 0.4), + (0.02, 0.2), + (0.02, 0), + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +def test_output_rgbled_blink_interrupt(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + device = RGBLED(r, g, b) + device.blink(1, 0.1) + sleep(0.2) + device.off() # should interrupt while on + r.assert_states([0, 1, 0]) + g.assert_states([0, 1, 0]) + b.assert_states([0, 1, 0]) + +def test_motor_missing_pins(): + with pytest.raises(ValueError): + Motor() + +def test_motor_pins(): + f = MockPWMPin(1) + b = MockPWMPin(2) + device = Motor(f, b) + assert device.forward_device.pin is f + assert device.backward_device.pin is b + +def test_motor_close(): + f = MockPWMPin(1) + b = MockPWMPin(2) + device = Motor(f, b) + device.close() + assert device.closed + assert device.forward_device.pin is None + assert device.backward_device.pin is None + +def test_motor_value(): + f = MockPWMPin(1) + b = MockPWMPin(2) + device = Motor(f, b) + device.value = -1 + assert device.is_active + assert device.value == -1 + assert b.state == 1 and f.state == 0 + device.value = 1 + assert device.is_active + assert device.value == 1 + assert b.state == 0 and f.state == 1 + device.value = 0.5 + assert device.is_active + assert device.value == 0.5 + assert b.state == 0 and f.state == 0.5 + device.value = 0 + assert not device.is_active + assert not device.value + assert b.state == 0 and f.state == 0 + +def test_motor_bad_value(): + f = MockPWMPin(1) + b = MockPWMPin(2) + device = Motor(f, b) + with pytest.raises(ValueError): + device.value = 2 + +def test_motor_reverse(): + f = MockPWMPin(1) + b = MockPWMPin(2) + device = Motor(f, b) + device.forward() + assert device.value == 1 + assert b.state == 0 and f.state == 1 + device.reverse() + assert device.value == -1 + assert b.state == 1 and f.state == 0 +