diff --git a/docs/api_output.rst b/docs/api_output.rst index 845079c..1824f11 100644 --- a/docs/api_output.rst +++ b/docs/api_output.rst @@ -23,13 +23,13 @@ PWMLED ====== .. autoclass:: PWMLED(pin, active_high=True, initial_value=0, frequency=100) - :members: on, off, toggle, blink, pin, is_lit, value + :members: on, off, toggle, blink, pulse, pin, is_lit, value RGBLED ====== .. autoclass:: RGBLED(red, green, blue, active_high=True, initial_value=(0, 0, 0)) - :members: on, off, toggle, blink, red, green, blue, is_lit, color + :members: on, off, toggle, blink, pulse, red, green, blue, is_lit, color Buzzer ====== diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index 49a1b47..5c13956 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -435,12 +435,12 @@ class PWMOutputDevice(OutputDevice): Number of seconds to spend fading out. Defaults to 1. :param int n: - Number of times to blink; ``None`` (the default) means forever. + Number of times to pulse; ``None`` (the default) means forever. :param bool background: If ``True`` (the default), start a background thread to continue - blinking and return immediately. If ``False``, only return when the - blink is finished (warning: the default value of *n* will result in + pulsing and return immediately. If ``False``, only return when the + pulse is finished (warning: the default value of *n* will result in this method never returning). """ on_time = off_time = 0 @@ -670,13 +670,49 @@ class RGBLED(SourceMixin, Device): self._stop_blink() self._blink_thread = GPIOThread( target=self._blink_device, - args=(on_time, off_time, fade_in_time, fade_out_time, on_color, off_color, n) + args=( + on_time, off_time, fade_in_time, fade_out_time, + on_color, off_color, n + ) ) self._blink_thread.start() if not background: self._blink_thread.join() self._blink_thread = None + def pulse( + self, fade_in_time=1, fade_out_time=1, + on_color=(1, 1, 1), off_color=(0, 0, 0), n=None, background=True): + """ + Make the device fade in and out repeatedly. + + :param float fade_in_time: + Number of seconds to spend fading in. Defaults to 1. + + :param float fade_out_time: + Number of seconds to spend fading out. Defaults to 1. + + :param tuple on_color: + The color to use when the LED is "on". Defaults to white. + + :param tuple off_color: + The color to use when the LED is "off". Defaults to black. + + :param int n: + Number of times to pulse; ``None`` (the default) means forever. + + :param bool background: + If ``True`` (the default), start a background thread to continue + pulsing and return immediately. If ``False``, only return when the + pulse is finished (warning: the default value of *n* will result in + this method never returning). + """ + on_time = off_time = 0 + self.blink( + on_time, off_time, fade_in_time, fade_out_time, + on_color, off_color, n, background + ) + def _stop_blink(self, led=None): # If this is called with a single led, we stop all blinking anyway if self._blink_thread: diff --git a/tests/test_outputs.py b/tests/test_outputs.py index cd3abf2..435cf17 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -8,7 +8,7 @@ str = type('') import sys -from time import sleep +from time import sleep, time try: from math import isclose except ImportError: @@ -95,8 +95,11 @@ def test_output_digital_toggle(): def test_output_blink_background(): pin = MockPin(2) with DigitalOutputDevice(pin) as device: + start = time() device.blink(0.1, 0.1, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() # naughty, but ensures no arbitrary waits in the test + assert isclose(time() - start, 0.4, abs_tol=0.05) pin.assert_states_and_times([ (0.0, False), (0.0, True), @@ -110,7 +113,9 @@ def test_output_blink_background(): def test_output_blink_foreground(): pin = MockPin(2) with DigitalOutputDevice(pin) as device: + start = time() device.blink(0.1, 0.1, n=2, background=False) + assert isclose(time() - start, 0.4, abs_tol=0.05) pin.assert_states_and_times([ (0.0, False), (0.0, True), @@ -212,8 +217,11 @@ def test_output_pwm_write_silly(): def test_output_pwm_blink_background(): pin = MockPWMPin(2) with PWMOutputDevice(pin) as device: + start = time() device.blink(0.1, 0.1, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() + assert isclose(time() - start, 0.4, abs_tol=0.05) pin.assert_states_and_times([ (0.0, 0), (0.0, 1), @@ -227,7 +235,9 @@ def test_output_pwm_blink_background(): def test_output_pwm_blink_foreground(): pin = MockPWMPin(2) with PWMOutputDevice(pin) as device: + start = time() device.blink(0.1, 0.1, n=2, background=False) + assert isclose(time() - start, 0.4, abs_tol=0.05) pin.assert_states_and_times([ (0.0, 0), (0.0, 1), @@ -241,8 +251,11 @@ def test_output_pwm_blink_foreground(): def test_output_pwm_fade_background(): pin = MockPWMPin(2) with PWMOutputDevice(pin) as device: + start = time() device.blink(0, 0, 0.2, 0.2, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() + assert isclose(time() - start, 0.8, abs_tol=0.05) pin.assert_states_and_times([ (0.0, 0), (0.04, 0.2), @@ -272,7 +285,75 @@ def test_output_pwm_fade_background(): def test_output_pwm_fade_foreground(): pin = MockPWMPin(2) with PWMOutputDevice(pin) as device: + start = time() device.blink(0, 0, 0.2, 0.2, n=2, background=False) + assert isclose(time() - start, 0.8, abs_tol=0.05) + pin.assert_states_and_times([ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ]) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_output_pwm_pulse_background(): + pin = MockPWMPin(2) + with PWMOutputDevice(pin) as device: + start = time() + device.pulse(0.2, 0.2, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) + device._blink_thread.join() + assert isclose(time() - start, 0.8, abs_tol=0.05) + pin.assert_states_and_times([ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ]) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_output_pwm_pulse_foreground(): + pin = MockPWMPin(2) + with PWMOutputDevice(pin) as device: + start = time() + device.pulse(0.2, 0.2, n=2, background=False) + assert isclose(time() - start, 0.8, abs_tol=0.05) pin.assert_states_and_times([ (0.0, 0), (0.04, 0.2), @@ -409,8 +490,11 @@ def test_rgbled_toggle(): def test_rgbled_blink_background(): r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: + start = time() device.blink(0.1, 0.1, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() + assert isclose(time() - start, 0.4, abs_tol=0.05) expected = [ (0.0, 0), (0.0, 1), @@ -427,7 +511,9 @@ def test_rgbled_blink_background(): def test_rgbled_blink_foreground(): r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: + start = time() device.blink(0.1, 0.1, n=2, background=False) + assert isclose(time() - start, 0.4, abs_tol=0.05) expected = [ (0.0, 0), (0.0, 1), @@ -444,8 +530,118 @@ def test_rgbled_blink_foreground(): def test_rgbled_fade_background(): r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: + start = time() device.blink(0, 0, 0.2, 0.2, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() + assert isclose(time() - start, 0.8, abs_tol=0.05) + expected = [ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_rgbled_fade_foreground(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b) as device: + start = time() + device.blink(0, 0, 0.2, 0.2, n=2, background=False) + assert isclose(time() - start, 0.8, abs_tol=0.05) + expected = [ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_rgbled_pulse_background(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b) as device: + start = time() + device.pulse(0.2, 0.2, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) + device._blink_thread.join() + assert isclose(time() - start, 0.8, abs_tol=0.05) + expected = [ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_rgbled_pulse_foreground(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b) as device: + start = time() + device.pulse(0.2, 0.2, n=2, background=False) + assert isclose(time() - start, 0.8, abs_tol=0.05) expected = [ (0.0, 0), (0.04, 0.2),