diff --git a/debian/control b/debian/control index b84aace..72f2eb4 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,8 @@ X-Python3-Version: >= 3.2 Package: python-gpiozero Architecture: all Section: python -Depends: ${misc:Depends}, ${python:Depends}, python-rpi.gpio, python-spidev +Depends: ${misc:Depends}, ${python:Depends}, python-rpi.gpio +Suggests: python-spidev, python-gpiozero-docs Description: Simple API for controlling devices attached to the GPIO pins. gpiozero builds on RPi.GPIO to provide a set of classes designed to simplify interaction with devices connected to the GPIO pins, from simple buttons and @@ -23,7 +24,8 @@ Description: Simple API for controlling devices attached to the GPIO pins. Package: python3-gpiozero Architecture: all Section: python -Depends: ${misc:Depends}, ${python3:Depends}, python3-rpi.gpio, python3-spidev +Depends: ${misc:Depends}, ${python3:Depends}, python3-rpi.gpio +Suggests: python3-spidev, python-gpiozero-docs Description: Simple API for controlling devices attached to the GPIO pins. gpiozero builds on RPi.GPIO to provide a set of classes designed to simplify interaction with devices connected to the GPIO pins, from simple buttons and diff --git a/docs/api_boards.rst b/docs/api_boards.rst index eb12754..f1fdb32 100644 --- a/docs/api_boards.rst +++ b/docs/api_boards.rst @@ -99,3 +99,10 @@ CamJam #3 Kit Robot :inherited-members: :members: +Energenie +========= + +.. autoclass:: Energenie + :inherited-members: + :members: + diff --git a/docs/api_pins.rst b/docs/api_pins.rst index e08b4ac..789d138 100644 --- a/docs/api_pins.rst +++ b/docs/api_pins.rst @@ -70,6 +70,14 @@ to utilize pins that are part of IO extender chips. For example:: It is potentially subject to change in future versions. We welcome any comments from testers! +.. warning:: + + The astute and mischievious reader may note that it is possible to mix pin + implementations, e.g. using ``RPiGPIOPin`` for one pin, and ``NativePin`` + for another. This is unsupported, and if it results in your script + crashing, your components failing, or your Raspberry Pi turning into an + actual raspberry pie, you have only yourself to blame. + RPiGPIOPin ========== diff --git a/docs/changelog.rst b/docs/changelog.rst index 30820e1..99b3c12 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,50 @@ Changelog .. currentmodule:: gpiozero +Release 1.2.0 (2016-04-??) +========================== + +* Added :class:`Energenie` class for controlling Energenie plugs (`#69`_) +* Added :class:`LineSensor` class for single line-sensors (`#109`_) +* Added :class:`DistanceSensor` class for HC-SR04 ultra-sonic sensors (`#114`_) +* Added :class:`SnowPi` class for the Ryanteck Snow-pi board (`#130`_) +* Fixed issues with installing GPIO Zero for python 3 on Raspbian Wheezy + releases (`#140`_) +* Added support for lots of ADC chips (MCP3xxx family) (`#162`_) - many thanks + to pcopa and lurch! +* Added support for pigpiod as a pin implementation with + :class:`~gpiozero.pins.pigpiod.PiGPIOPin` (`#180`_) +* Many refinements to the base classes mean more consistency in composite + devices and several bugs squashed (`#164`_, `#175`_, `#182`_, `#189`_, + `#193`_, `#229`_) +* GPIO Zero is now aware of what sort of Pi it's running on via :func:`pi_info` + and has a fairly extensive database of Pi information which it uses to + determine when users request impossible things (like pull-down on a pin with + a physical pull-up resistor) (`#222`_) +* The source/values system was enhanced to ensure normal usage doesn't stress + the CPU and lots of utilities were added (`#181`_, `#251`_) + +And I'll just add a note of thanks to the many people in the community who +contributed to this release: we've had some great PRs, suggestions, and bug +reports in this version - keep 'em coming! + +.. _#69: https://github.com/RPi-Distro/python-gpiozero/issues/69 +.. _#109: https://github.com/RPi-Distro/python-gpiozero/issues/109 +.. _#114: https://github.com/RPi-Distro/python-gpiozero/issues/114 +.. _#130: https://github.com/RPi-Distro/python-gpiozero/issues/130 +.. _#140: https://github.com/RPi-Distro/python-gpiozero/issues/140 +.. _#162: https://github.com/RPi-Distro/python-gpiozero/issues/162 +.. _#164: https://github.com/RPi-Distro/python-gpiozero/issues/164 +.. _#175: https://github.com/RPi-Distro/python-gpiozero/issues/175 +.. _#180: https://github.com/RPi-Distro/python-gpiozero/issues/180 +.. _#181: https://github.com/RPi-Distro/python-gpiozero/issues/181 +.. _#182: https://github.com/RPi-Distro/python-gpiozero/issues/182 +.. _#189: https://github.com/RPi-Distro/python-gpiozero/issues/189 +.. _#193: https://github.com/RPi-Distro/python-gpiozero/issues/193 +.. _#222: https://github.com/RPi-Distro/python-gpiozero/issues/222 +.. _#229: https://github.com/RPi-Distro/python-gpiozero/issues/229 +.. _#251: https://github.com/RPi-Distro/python-gpiozero/issues/251 + Release 1.1.0 (2016-02-08) ========================== diff --git a/gpiozero/boards.py b/gpiozero/boards.py index 24ec9d5..9bcffb3 100644 --- a/gpiozero/boards.py +++ b/gpiozero/boards.py @@ -539,13 +539,15 @@ class SnowPi(LEDBoard): _order=('top', 'middle', 'bottom')), right=LEDBoard( top=7, middle=8, bottom=9, pwm=pwm, - _order=('top', 'middle', 'bottom')) + _order=('top', 'middle', 'bottom')), + _order=('left', 'right') ), eyes=LEDBoard( left=23, right=24, pwm=pwm, _order=('left', 'right') ), - nose=25, pwm=pwm + nose=25, pwm=pwm, + _order=('eyes', 'nose', 'arms') ) @@ -629,9 +631,6 @@ class TrafficHat(TrafficLightsBuzzer): ) -RobotTuple = namedtuple('RobotTuple', ('left', 'right')) - - class Robot(SourceMixin, CompositeDevice): """ Extends :class:`CompositeDevice` to represent a generic dual-motor robot. @@ -657,48 +656,10 @@ class Robot(SourceMixin, CompositeDevice): """ def __init__(self, left=None, right=None): - if not all([left, right]): - raise GPIOPinMissing( - 'left and right motor pins must be provided' - ) - super(Robot, self).__init__() - self._left = Motor(*left) - self._right = Motor(*right) - - def close(self): - self._left.close() - self._right.close() - - @property - def closed(self): - return self._left.closed and self._right.closed - - @property - def left_motor(self): - """ - Returns the `Motor` device representing the robot's left motor. - """ - return self._left - - @property - def right_motor(self): - """ - Returns the `Motor` device representing the robot's right motor. - """ - return self._right - - @property - def value(self): - """ - Returns a tuple of two floating point values (-1 to 1) representing the - speeds of the robot's two motors (left and right). This property can - also be set to alter the speed of both motors. - """ - return RobotTuple(self._left.value, self._right.value) - - @value.setter - def value(self, value): - self._left.value, self._right.value = value + super(Robot, self).__init__( + left_motor=Motor(*left), + right_motor=Motor(*right), + _order=('left_motor', 'right_motor')) def forward(self, speed=1): """ @@ -708,8 +669,8 @@ class Robot(SourceMixin, CompositeDevice): Speed at which to drive the motors, as a value between 0 (stopped) and 1 (full speed). The default is 1. """ - self._left.forward(speed) - self._right.forward(speed) + self.left_motor.forward(speed) + self.right_motor.forward(speed) def backward(self, speed=1): """ @@ -719,8 +680,8 @@ class Robot(SourceMixin, CompositeDevice): Speed at which to drive the motors, as a value between 0 (stopped) and 1 (full speed). The default is 1. """ - self._left.backward(speed) - self._right.backward(speed) + self.left_motor.backward(speed) + self.right_motor.backward(speed) def left(self, speed=1): """ @@ -731,8 +692,8 @@ class Robot(SourceMixin, CompositeDevice): Speed at which to drive the motors, as a value between 0 (stopped) and 1 (full speed). The default is 1. """ - self._right.forward(speed) - self._left.backward(speed) + self.right_motor.forward(speed) + self.left_motor.backward(speed) def right(self, speed=1): """ @@ -743,8 +704,8 @@ class Robot(SourceMixin, CompositeDevice): Speed at which to drive the motors, as a value between 0 (stopped) and 1 (full speed). The default is 1. """ - self._left.forward(speed) - self._right.backward(speed) + self.left_motor.forward(speed) + self.right_motor.backward(speed) def reverse(self): """ @@ -753,15 +714,15 @@ class Robot(SourceMixin, CompositeDevice): robot is turning left at half-speed, it will turn right at half-speed. If the robot is currently stopped it will remain stopped. """ - self._left.value = -self._left.value - self._right.value = -self._right.value + self.left_motor.reverse() + self.right_motor.reverse() def stop(self): """ Stop the robot. """ - self._left.stop() - self._right.stop() + self.left_motor.stop() + self.right_motor.stop() class RyanteckRobot(Robot): @@ -779,7 +740,7 @@ class RyanteckRobot(Robot): """ def __init__(self): - super(RyanteckRobot, self).__init__(left=(17, 18), right=(22, 23)) + super(RyanteckRobot, self).__init__((17, 18), (22, 23)) class CamJamKitRobot(Robot): @@ -799,7 +760,7 @@ class CamJamKitRobot(Robot): """ def __init__(self): - super(CamJamKitRobot, self).__init__(left=(9, 10), right=(7, 8)) + super(CamJamKitRobot, self).__init__((9, 10), (7, 8)) class _EnergenieMaster(SharedMixin, CompositeOutputDevice): @@ -807,7 +768,8 @@ class _EnergenieMaster(SharedMixin, CompositeOutputDevice): self._lock = Lock() super(_EnergenieMaster, self).__init__( *(OutputDevice(pin) for pin in (17, 22, 23, 27)), - mode=OutputDevice(24), enable=OutputDevice(25)) + mode=OutputDevice(24), enable=OutputDevice(25), + _order=('mode', 'enable')) def close(self): if self._lock: @@ -823,7 +785,7 @@ class _EnergenieMaster(SharedMixin, CompositeOutputDevice): def transmit(self, socket, enable): with self._lock: try: - code = (8 * bool(enable)) + (7 - socket) + code = (8 * bool(enable)) + (8 - socket) for bit in self.all[:4]: bit.value = (code & 1) code >>= 1 @@ -844,12 +806,12 @@ class Energenie(SourceMixin, Device): from gpiozero import Energenie - lamp = Energenie(0) + lamp = Energenie(1) lamp.on() :param int socket: Which socket this instance should control. This is an integer number - between 0 and 3. + between 1 and 4. :param bool initial_value: The initial state of the socket. As Energenie sockets provide no @@ -863,8 +825,8 @@ class Energenie(SourceMixin, Device): def __init__(self, socket=None, initial_value=False): if socket is None: raise EnergenieSocketMissing('socket number must be provided') - if not (0 <= socket < 4): - raise EnergenieBadSocket('socket number must be between 0 and 3') + if not (1 <= socket <= 4): + raise EnergenieBadSocket('socket number must be between 1 and 4') super(Energenie, self).__init__() self._socket = socket self._master = _EnergenieMaster() diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index e5f806d..276e92e 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -41,6 +41,7 @@ class OutputDevice(SourceMixin, GPIODevice): """ def __init__(self, pin=None, active_high=True, initial_value=False): super(OutputDevice, self).__init__(pin) + self._lock = Lock() self.active_high = active_high if initial_value is None: self.pin.function = 'output' @@ -70,6 +71,17 @@ class OutputDevice(SourceMixin, GPIODevice): """ self._write(False) + def toggle(self): + """ + Reverse the state of the device. If it's on, turn it off; if it's off, + turn it on. + """ + with self._lock: + if self.is_active: + self.off() + else: + self.on() + @property def value(self): """ @@ -121,7 +133,6 @@ class DigitalOutputDevice(OutputDevice): def __init__(self, pin=None, active_high=True, initial_value=False): self._blink_thread = None super(DigitalOutputDevice, self).__init__(pin, active_high, initial_value) - self._lock = Lock() self._controller = None @property @@ -145,17 +156,6 @@ class DigitalOutputDevice(OutputDevice): self._stop_blink() self._write(False) - def toggle(self): - """ - Reverse the state of the device. If it's on, turn it off; if it's off, - turn it on. - """ - with self._lock: - if self.is_active: - self.off() - else: - self.on() - def blink(self, on_time=1, off_time=1, n=None, background=True): """ Make the device turn on and off repeatedly. @@ -757,7 +757,8 @@ class Motor(SourceMixin, CompositeDevice): ) super(Motor, self).__init__( forward_device=PWMOutputDevice(forward), - backward_device=PWMOutputDevice(backward)) + backward_device=PWMOutputDevice(backward), + _order=('forward_device', 'backward_device')) @property def value(self): diff --git a/tests/test_boards.py b/tests/test_boards.py index 6338f54..7576e31 100644 --- a/tests/test_boards.py +++ b/tests/test_boards.py @@ -7,14 +7,450 @@ from __future__ import ( str = type('') +import sys import pytest +from time import sleep -from gpiozero.pins.mock import MockPin +from gpiozero.pins.mock import MockPin, MockPWMPin from gpiozero import * +def setup_function(function): + import gpiozero.devices + # dirty, but it does the job + if function.__name__ in ('test_robot', 'test_ryanteck_robot', 'test_camjam_kit_robot'): + gpiozero.devices.DefaultPin = MockPWMPin + else: + gpiozero.devices.DefaultPin = MockPin + def teardown_function(function): MockPin.clear_pins() -# TODO boards tests! + +def test_composite_output_on_off(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + device = CompositeOutputDevice(OutputDevice(pin1), OutputDevice(pin2), foo=OutputDevice(pin3)) + device.on() + assert all((pin1.state, pin2.state, pin3.state)) + device.off() + assert not any((pin1.state, pin2.state, pin3.state)) + +def test_composite_output_toggle(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + device = CompositeOutputDevice(OutputDevice(pin1), OutputDevice(pin2), foo=OutputDevice(pin3)) + device.toggle() + assert all((pin1.state, pin2.state, pin3.state)) + device[0].off() + device.toggle() + assert pin1.state + assert not pin2.state + assert not pin3.state + +def test_composite_output_value(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + device = CompositeOutputDevice(OutputDevice(pin1), OutputDevice(pin2), foo=OutputDevice(pin3)) + assert device.value == (0, 0, 0) + device.toggle() + assert device.value == (1, 1, 1) + device.value = (1, 0, 1) + assert device[0].is_active + assert not device[1].is_active + assert device[2].is_active + +def test_led_board_on_off(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, pin2, foo=pin3) + assert isinstance(board[0], LED) + assert isinstance(board[1], LED) + assert isinstance(board[2], LED) + board.on() + assert all((pin1.state, pin2.state, pin3.state)) + board.off() + assert not any((pin1.state, pin2.state, pin3.state)) + board[0].on() + assert board.value == (1, 0, 0) + assert pin1.state + assert not pin2.state + assert not pin3.state + board.toggle() + assert board.value == (0, 1, 1) + assert not pin1.state + assert pin2.state + assert pin3.state + +def test_led_board_nested(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3)) + assert list(led.pin for led in board.leds) == [pin1, pin2, pin3] + assert board.value == (0, (0, 0)) + board.value = (1, (0, 1)) + assert pin1.state + assert not pin2.state + assert pin3.state + +def test_led_board_bad_blink(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3)) + with pytest.raises(ValueError): + board.blink(fade_in_time=1, fade_out_time=1) + with pytest.raises(ValueError): + board.blink(fade_out_time=1) + with pytest.raises(ValueError): + board.pulse() + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_led_board_blink_background(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3)) + board.blink(0.1, 0.1, n=2) + board._blink_thread.join() # naughty, but ensures no arbitrary waits in the test + test = [ + (0.0, False), + (0.0, True), + (0.1, False), + (0.1, True), + (0.1, False) + ] + pin1.assert_states_and_times(test) + pin2.assert_states_and_times(test) + pin3.assert_states_and_times(test) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_led_board_blink_foreground(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3)) + board.blink(0.1, 0.1, n=2, background=False) + test = [ + (0.0, False), + (0.0, True), + (0.1, False), + (0.1, True), + (0.1, False) + ] + pin1.assert_states_and_times(test) + pin2.assert_states_and_times(test) + pin3.assert_states_and_times(test) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_led_board_blink_control(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3)) + board.blink(0.1, 0.1, n=2) + # make sure the blink thread's started + while not board._blink_leds: + sleep(0.00001) + board[1][0].off() # immediately take over the second LED + board._blink_thread.join() # naughty, but ensures no arbitrary waits in the test + test = [ + (0.0, False), + (0.0, True), + (0.1, False), + (0.1, True), + (0.1, False) + ] + pin1.assert_states_and_times(test) + pin3.assert_states_and_times(test) + print(pin2.states) + pin2.assert_states_and_times([(0.0, False), (0.0, True), (0.0, False)]) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_led_board_blink_take_over(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3)) + board[1].blink(0.1, 0.1, n=2) + board.blink(0.1, 0.1, n=2) # immediately take over blinking + board[1]._blink_thread.join() + board._blink_thread.join() + test = [ + (0.0, False), + (0.0, True), + (0.1, False), + (0.1, True), + (0.1, False) + ] + pin1.assert_states_and_times(test) + pin2.assert_states_and_times(test) + pin3.assert_states_and_times(test) + +def test_led_board_blink_control_all(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3)) + board.blink(0.1, 0.1, n=2) + # make sure the blink thread's started + while not board._blink_leds: + sleep(0.00001) + board[0].off() # immediately take over all LEDs + board[1][0].off() + board[1][1].off() + board._blink_thread.join() # blink should terminate here anyway + test = [ + (0.0, False), + (0.0, True), + (0.0, False), + ] + pin1.assert_states_and_times(test) + pin2.assert_states_and_times(test) + pin3.assert_states_and_times(test) + +def test_led_board_blink_interrupt_on(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3)) + board.blink(1, 0.1) + sleep(0.2) + board.off() # should interrupt while on + pin1.assert_states([False, True, False]) + pin2.assert_states([False, True, False]) + pin3.assert_states([False, True, False]) + +def test_led_board_blink_interrupt_off(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3)) + board.blink(0.1, 1) + sleep(0.2) + board.off() # should interrupt while off + pin1.assert_states([False, True, False]) + pin2.assert_states([False, True, False]) + pin3.assert_states([False, True, False]) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_led_board_fade_background(): + pin1 = MockPWMPin(2) + pin2 = MockPWMPin(3) + pin3 = MockPWMPin(4) + board = LEDBoard(pin1, LEDBoard(pin2, pin3, pwm=True), pwm=True) + board.blink(0, 0, 0.1, 0.1, n=2) + board._blink_thread.join() + test = [ + (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), + ] + pin1.assert_states_and_times(test) + pin2.assert_states_and_times(test) + pin3.assert_states_and_times(test) + +def test_led_bar_graph_value(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + graph = LEDBarGraph(pin1, pin2, pin3) + graph.value = 0 + assert not any((pin1.state, pin2.state, pin3.state)) + graph.value = 1 + assert all((pin1.state, pin2.state, pin3.state)) + graph.value = 1/3 + assert pin1.state and not (pin2.state or pin3.state) + graph.value = -1/3 + assert pin3.state and not (pin1.state or pin2.state) + pin1.state = True + pin2.state = True + assert graph.value == 1 + pin3.state = False + assert graph.value == 2/3 + pin3.state = True + pin1.state = False + assert graph.value == -2/3 + +def test_led_bar_graph_pwm_value(): + pin1 = MockPWMPin(2) + pin2 = MockPWMPin(3) + pin3 = MockPWMPin(4) + graph = LEDBarGraph(pin1, pin2, pin3, pwm=True) + graph.value = 0 + assert not any((pin1.state, pin2.state, pin3.state)) + graph.value = 1 + assert all((pin1.state, pin2.state, pin3.state)) + graph.value = 1/3 + assert pin1.state and not (pin2.state or pin3.state) + graph.value = -1/3 + assert pin3.state and not (pin1.state or pin2.state) + graph.value = 1/2 + assert (pin1.state, pin2.state, pin3.state) == (1, 0.5, 0) + pin1.state = 0 + pin3.state = 1 + assert graph.value == -1/2 + +def test_led_bar_graph_bad_init(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + with pytest.raises(TypeError): + LEDBarGraph(pin1, pin2, foo=pin3) + +def test_pi_liter(): + pins = [MockPin(n) for n in (4, 17, 27, 18, 22, 23, 24, 25)] + board = PiLiter() + assert [device.pin for device in board] == pins + +def test_pi_liter_graph(): + pins = [MockPin(n) for n in (4, 17, 27, 18, 22, 23, 24, 25)] + board = PiLiterBarGraph() + board.value = 0.5 + assert [pin.state for pin in pins] == [1, 1, 1, 1, 0, 0, 0, 0] + pins[4].state = 1 + assert board.value == 5/8 + +def test_traffic_lights(): + red_pin = MockPin(2) + amber_pin = MockPin(3) + green_pin = MockPin(4) + board = TrafficLights(red_pin, amber_pin, green_pin) + board.red.on() + assert red_pin.state + assert not amber_pin.state + assert not green_pin.state + +def test_traffic_lights_bad_init(): + with pytest.raises(ValueError): + TrafficLights() + +def test_pi_traffic(): + pins = [MockPin(n) for n in (9, 10, 11)] + board = PiTraffic() + assert [device.pin for device in board] == pins + +def test_snow_pi(): + pins = [MockPin(n) for n in (23, 24, 25, 17, 18, 22, 7, 8, 9)] + board = SnowPi() + assert [device.pin for device in board.leds] == pins + +def test_traffic_lights_buzzer(): + red_pin = MockPin(2) + amber_pin = MockPin(3) + green_pin = MockPin(4) + buzzer_pin = MockPin(5) + button_pin = MockPin(6) + board = TrafficLightsBuzzer( + TrafficLights(red_pin, amber_pin, green_pin), + Buzzer(buzzer_pin), + Button(button_pin)) + board.lights.red.on() + board.buzzer.on() + assert red_pin.state + assert not amber_pin.state + assert not green_pin.state + assert buzzer_pin.state + button_pin.drive_low() + assert board.button.is_active + +def test_fish_dish(): + pins = [MockPin(n) for n in (9, 22, 4, 8, 7)] + board = FishDish() + assert [led.pin for led in board.lights] + [board.buzzer.pin, board.button.pin] == pins + +def test_traffic_hat(): + pins = [MockPin(n) for n in (24, 23, 22, 5, 25)] + board = TrafficHat() + assert [led.pin for led in board.lights] + [board.buzzer.pin, board.button.pin] == pins + +def test_robot(): + pins = [MockPWMPin(n) for n in (2, 3, 4, 5)] + robot = Robot((2, 3), (4, 5)) + assert ( + [device.pin for device in robot.left_motor] + + [device.pin for device in robot.right_motor]) == pins + robot.forward() + assert [pin.state for pin in pins] == [1, 0, 1, 0] + robot.backward() + assert [pin.state for pin in pins] == [0, 1, 0, 1] + robot.forward(0.5) + assert [pin.state for pin in pins] == [0.5, 0, 0.5, 0] + robot.left() + assert [pin.state for pin in pins] == [0, 1, 1, 0] + robot.right() + assert [pin.state for pin in pins] == [1, 0, 0, 1] + robot.reverse() + assert [pin.state for pin in pins] == [0, 1, 1, 0] + robot.stop() + assert [pin.state for pin in pins] == [0, 0, 0, 0] + +def test_ryanteck_robot(): + pins = [MockPWMPin(n) for n in (17, 18, 22, 23)] + board = RyanteckRobot() + assert [device.pin for motor in board for device in motor] == pins + +def test_camjam_kit_robot(): + pins = [MockPWMPin(n) for n in (9, 10, 7, 8)] + board = CamJamKitRobot() + assert [device.pin for motor in board for device in motor] == pins + +def test_energenie_bad_init(): + with pytest.raises(ValueError): + Energenie() + with pytest.raises(ValueError): + Energenie(0) + +def test_energenie(): + pins = [MockPin(n) for n in (17, 22, 23, 27, 24, 25)] + device1 = Energenie(1, initial_value=True) + device2 = Energenie(2, initial_value=False) + assert device1.value + assert not device2.value + [pin.clear_states() for pin in pins] + device1.on() + assert device1.value + pins[0].assert_states_and_times([(0.0, False), (0.0, True)]) + pins[1].assert_states_and_times([(0.0, True), (0.0, True)]) + pins[2].assert_states_and_times([(0.0, True), (0.0, True)]) + pins[3].assert_states_and_times([(0.0, False), (0.0, True)]) + pins[4].assert_states_and_times([(0.0, False)]) + pins[5].assert_states_and_times([(0.0, False), (0.1, True), (0.25, False)]) + [pin.clear_states() for pin in pins] + device2.on() + assert device2.value + pins[0].assert_states_and_times([(0.0, True), (0.0, False)]) + pins[1].assert_states_and_times([(0.0, True), (0.0, True)]) + pins[2].assert_states_and_times([(0.0, True), (0.0, True)]) + pins[3].assert_states_and_times([(0.0, True), (0.0, True)]) + pins[4].assert_states_and_times([(0.0, False)]) + pins[5].assert_states_and_times([(0.0, False), (0.1, True), (0.25, False)]) diff --git a/tests/test_real_pins.py b/tests/test_real_pins.py index e91065a..94d25ef 100644 --- a/tests/test_real_pins.py +++ b/tests/test_real_pins.py @@ -5,7 +5,10 @@ from __future__ import ( division, ) str = type('') - +try: + range = xrange +except NameError: + pass import io import subprocess @@ -13,6 +16,10 @@ import subprocess import pytest from gpiozero import PinFixedPull, PinInvalidPull, PinInvalidFunction +try: + from math import isclose +except ImportError: + from gpiozero.compat import isclose # This module assumes you've wired the following GPIO pins together @@ -56,7 +63,6 @@ try: except ImportError: NativePin = None - @pytest.fixture(scope='module', params=PIN_CLASSES) def pin_class(request): # pigpiod needs to be running for PiGPIOPin @@ -70,13 +76,13 @@ def pin_class(request): request.addfinalizer(kill_pigpiod) return request.param -@pytest.fixture +@pytest.fixture(scope='function') def pins(request, pin_class): # Why return both pins in a single fixture? If we defined one fixture for # each pin then pytest will (correctly) test RPiGPIOPin(22) against # NativePin(27) and so on. This isn't supported, so we don't test it - test_pin = pin_class(22) - input_pin = pin_class(27) + test_pin = pin_class(TEST_PIN) + input_pin = pin_class(INPUT_PIN) input_pin.function = 'input' input_pin.pull = 'down' def fin(): @@ -88,8 +94,8 @@ def pins(request, pin_class): def test_pin_numbers(pins): test_pin, input_pin = pins - assert test_pin.number == 22 - assert input_pin.number == 27 + assert test_pin.number == TEST_PIN + assert input_pin.number == INPUT_PIN def test_function_bad(pins): test_pin, input_pin = pins @@ -125,12 +131,18 @@ def test_pull_bad(pins): test_pin.function = 'input' with pytest.raises(PinInvalidPull): test_pin.pull = 'foo' + with pytest.raises(PinInvalidPull): + test_pin.input_with_pull('foo') def test_pull_down_warning(pin_class): + # XXX This assumes we're on a vaguely modern Pi and not a compute module + # Might want to refine this with the pi-info database pin = pin_class(2) try: with pytest.raises(PinFixedPull): pin.pull = 'down' + with pytest.raises(PinFixedPull): + pin.input_with_pull('down') finally: pin.close() @@ -141,3 +153,25 @@ def test_input_with_pull(pins): test_pin.input_with_pull('down') assert input_pin.state == 0 +@pytest.mark.skipif(True, reason='causes segfaults') +def test_bad_duty_cycle(pins): + test_pin, input_pin = pins + if test_pin.__class__.__name__ == 'NativePin': + pytest.skip("native pin doesn't support PWM") + test_pin.function = 'output' + test_pin.frequency = 100 + with pytest.raises(ValueError): + test_pin.state = 1.1 + +def test_duty_cycles(pins): + test_pin, input_pin = pins + if test_pin.__class__.__name__ == 'NativePin': + pytest.skip("native pin doesn't support PWM") + test_pin.function = 'output' + test_pin.frequency = 100 + for duty_cycle in (0.0, 0.1, 0.5, 1.0): + test_pin.state = duty_cycle + assert test_pin.state == duty_cycle + total = sum(input_pin.state for i in range(20000)) + assert isclose(total / 20000, duty_cycle, rel_tol=0.1, abs_tol=0.1) +