diff --git a/.gitignore b/.gitignore index 9fefe27..70c96cc 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,6 @@ tags # MKDocs build site/ + +# Micropico +.micropico \ No newline at end of file diff --git a/picozero/picozero.py b/picozero/picozero.py index 2e748bb..79d0da8 100644 --- a/picozero/picozero.py +++ b/picozero/picozero.py @@ -154,8 +154,13 @@ def _set_value(self, timer_obj=None): ) if next_seq is None: - # the sequence has finished, turn the device off - self._output_device.off() + # the sequence has finished + # If using custom min_value, set to that instead of calling off() + if hasattr(self._output_device, '_blink_min_value'): + self._output_device.value = self._output_device._blink_min_value + del self._output_device._blink_min_value + else: + self._output_device.off() self._running = False def _get_value(self): @@ -532,6 +537,8 @@ def blink( fade_in_time=0, fade_out_time=None, fps=25, + min_value=0, + max_value=1, ): """ Makes the device turn on and off repeatedly. @@ -562,6 +569,14 @@ def blink( :param int fps: The frames per second that will be used to calculate the number of steps between off/on states when fading. Defaults to 25. + + :param float min_value: + The minimum value for the device when off or at the start of a fade. + Must be between 0 and 1. Defaults to 0. + + :param float max_value: + The maximum value for the device when on or at the peak of a fade. + Must be between 0 and 1. Defaults to 1. """ self.off() @@ -570,30 +585,55 @@ def blink( def blink_generator(): if fade_in_time > 0: - for s in [ - (i * (1 / fps) / fade_in_time, 1 / fps) - for i in range(int(fps * fade_in_time)) - ]: - yield s + # Calculate number of steps: use fps directly for custom min/max, + # otherwise use fps * fade_time for backward compatibility + fade_in_steps = ( + fps + if (min_value != 0 or max_value != 1) + else int(fps * fade_in_time) + ) + + for i in range(fade_in_steps + 1): + value = min_value + (i / fade_in_steps) * (max_value - min_value) + sleep_time = fade_in_time / fade_in_steps + yield (value, sleep_time) if on_time > 0: - yield (1, on_time) + yield (max_value, on_time) if fade_out_time > 0: - for s in [ - (1 - (i * (1 / fps) / fade_out_time), 1 / fps) - for i in range(int(fps * fade_out_time)) - ]: - yield s + # Calculate number of steps: use fps directly for custom min/max, + # otherwise use fps * fade_time for backward compatibility + fade_out_steps = ( + fps + if (min_value != 0 or max_value != 1) + else int(fps * fade_out_time) + ) + + for i in range(1, fade_out_steps + 1): + value = max_value - (i / fade_out_steps) * (max_value - min_value) + sleep_time = fade_out_time / fade_out_steps + yield (value, sleep_time) if off_time > 0: - yield (0, off_time) + yield (min_value, off_time) # is there anything to change? if on_time > 0 or off_time > 0 or fade_in_time > 0 or fade_out_time > 0: + # Store min_value so ValueChange knows the final state + self._blink_min_value = min_value self._start_change(blink_generator, n, wait) - def pulse(self, fade_in_time=1, fade_out_time=None, n=None, wait=False, fps=25): + def pulse( + self, + fade_in_time=1, + fade_out_time=None, + n=None, + wait=False, + fps=25, + min_value=0, + max_value=1, + ): """ Makes the device pulse on and off repeatedly. @@ -617,6 +657,14 @@ def pulse(self, fade_in_time=1, fade_out_time=None, n=None, wait=False, fps=25): If True, the method will block until the LED stops pulsing. If False, the method will return and the LED will pulse in the background. Defaults to False. + + :param float min_value: + The minimum value for the device when at the lowest point of the pulse. + Must be between 0 and 1. Defaults to 0. + + :param float max_value: + The maximum value for the device when at the peak of the pulse. + Must be between 0 and 1. Defaults to 1. """ self.blink( on_time=0, @@ -626,6 +674,8 @@ def pulse(self, fade_in_time=1, fade_out_time=None, n=None, wait=False, fps=25): n=n, wait=wait, fps=fps, + min_value=min_value, + max_value=max_value, ) def close(self): diff --git a/tests/test_picozero.py b/tests/test_picozero.py index 1441e3f..aa16ffe 100644 --- a/tests/test_picozero.py +++ b/tests/test_picozero.py @@ -297,6 +297,77 @@ def test_pwm_output_device_pulse(self): d.close() + def test_pwm_output_device_pulse_with_min_max_value(self): + d = PWMOutputDevice(7) + + # Test pulse with max_value=0.2 (sleep mode use case) + d.pulse( + fade_in_time=0.5, fade_out_time=0.5, n=1, fps=4, min_value=0, max_value=0.2 + ) + values = log_device_values(d, 1.1) + + expected = [0.0, 0.05, 0.1, 0.15, 0.2, 0.15, 0.1, 0.05, 0.0] + + if len(values) == len(expected): + for i in range(len(values)): + self.assertAlmostEqual(values[i], expected[i], places=2) + else: + self.fail( + f"{len(values)} values were generated, {len(expected)} were expected." + ) + + # Test pulse with min_value=0.2 and max_value=0.8 + d.pulse( + fade_in_time=0.5, + fade_out_time=0.5, + n=1, + fps=4, + min_value=0.2, + max_value=0.8, + ) + values = log_device_values(d, 1.1) + + expected = [0.2, 0.35, 0.5, 0.65, 0.8, 0.65, 0.5, 0.35, 0.2] + + if len(values) == len(expected): + for i in range(len(values)): + self.assertAlmostEqual(values[i], expected[i], places=2) + else: + self.fail( + f"{len(values)} values were generated, {len(expected)} were expected." + ) + + d.close() + + def test_pwm_output_device_blink_with_min_max_value(self): + d = PWMOutputDevice(7) + + # Test blink with fade and custom min/max values + d.blink( + on_time=0.2, + off_time=0.2, + fade_in_time=0.2, + fade_out_time=0.2, + n=1, + fps=4, + min_value=0.1, + max_value=0.5, + ) + values = log_device_values(d, 1.0) + + # Fade in from 0.1 to 0.5, stay at 0.5, fade out to 0.1, stay at 0.1 + expected = [0.1, 0.2, 0.3, 0.4, 0.5, 0.4, 0.3, 0.2, 0.1] + + if len(values) == len(expected): + for i in range(len(values)): + self.assertAlmostEqual(values[i], expected[i], places=2) + else: + self.fail( + f"{len(values)} values were generated, {len(expected)} were expected." + ) + + d.close() + def test_motor_default_values(self): d = Motor(8, 9) @@ -651,24 +722,24 @@ def test_digital_input_device_activated_deactivated(self): def test_digital_input_device_bounce_time(self): # Test that bounce_time is properly stored and device works with it d = DigitalInputDevice(1, bounce_time=0.01) - + self.assertEqual(d._bounce_time, 0.01) - + pin = MockPin(irq_handler=d._pin_change) d._pin = pin - + event_activated = MockEvent() d.when_activated = event_activated.set - + # Trigger should still work with bounce_time set self.assertFalse(event_activated.is_set()) self.assertFalse(d.is_active) - + pin.write(1) # With the timestamp-based approach, first event always fires callback self.assertTrue(event_activated.is_set()) self.assertTrue(d.is_active) - + d.close() def test_adc_input_device_default_values(self): @@ -1011,7 +1082,7 @@ def test_stepper_angle_wrapping(self): self.assertAlmostEqual(stepper.angle, 270.0, places=1) # -90 normalised to 270 stepper.close() - + def test_distance_sensor_basic(self): # Create a mock distance sensor d = DistanceSensor(echo=22, trigger=23)