Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,6 @@ tags

# MKDocs build
site/

# Micropico
.micropico
80 changes: 65 additions & 15 deletions picozero/picozero.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()

Expand All @@ -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.

Expand All @@ -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,
Expand All @@ -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):
Expand Down
85 changes: 78 additions & 7 deletions tests/test_picozero.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down