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/docs/examples/rgb_brightness.py b/docs/examples/rgb_brightness.py new file mode 100644 index 0000000..20eff8c --- /dev/null +++ b/docs/examples/rgb_brightness.py @@ -0,0 +1,15 @@ +from picozero import RGBLED +from time import sleep + +rgb = RGBLED(red=1, green=2, blue=3) + +# start with a mixed colour so brightness changes are obvious +rgb.color = (255, 128, 0) # orange + +while True: + rgb.brightness = 0.2 # dim + sleep(1) + rgb.brightness = 0.6 # medium + sleep(1) + rgb.brightness = 1.0 # full + sleep(1) diff --git a/docs/examples/rgb_led.py b/docs/examples/rgb_led.py index fd9b450..f677720 100644 --- a/docs/examples/rgb_led.py +++ b/docs/examples/rgb_led.py @@ -1,14 +1,14 @@ from picozero import RGBLED from time import sleep -rgb = RGBLED(red=2, green=1, blue=0) +rgb = RGBLED(red=1, green=2, blue=3) rgb.red = 255 # full red sleep(1) rgb.red = 128 # half red sleep(1) -rgb.on() # white +rgb.on() # white rgb.color = (0, 255, 0) # full green sleep(1) diff --git a/docs/recipes.rst b/docs/recipes.rst index 6288a24..2b99827 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -178,6 +178,13 @@ Use :meth:`~picozero.RGBLED.toggle` and :meth:`~picozero.RGBLED.invert`: .. literalinclude:: examples/rgb_toggle_invert.py +Brightness +~~~~~~~~~~ + +Adjust overall brightness while keeping the same colour: + +.. literalinclude:: examples/rgb_brightness.py + Blink ~~~~~ @@ -244,6 +251,13 @@ Play individual notes and control the timing or perform another action: .. literalinclude:: examples/speaker_notes.py +Play MIDI notes +~~~~~~~~~~~~~~~ + +Step through a few MIDI notes with custom durations: + +.. literalinclude:: examples/speaker_midi_notes.py + Servo ----- diff --git a/picozero/picozero.py b/picozero/picozero.py index 2e748bb..627623d 100644 --- a/picozero/picozero.py +++ b/picozero/picozero.py @@ -1094,6 +1094,9 @@ class RGBLED(OutputDevice, PinsMixin): If :data:`True` (the default), construct :class:`PWMLED` instances for each component of the RGBLED. If :data:`False`, construct :class:`DigitalLED` instances. + :param float brightness: + The overall brightness of the LED as a value between 0.0 and 1.0. + Defaults to 1.0 (full brightness). This scales all color values proportionally. """ @@ -1105,10 +1108,14 @@ def __init__( active_high=True, initial_value=(0, 0, 0), pwm=True, + brightness=1.0, ): self._pin_nums = (red, green, blue) self._leds = () self._last = initial_value + self._brightness = max( + 0.0, min(1.0, float(brightness)) + ) # clamp between 0 and 1 LEDClass = PWMLED if pwm else DigitalLED self._leds = tuple( LEDClass(pin, active_high=active_high) for pin in (red, green, blue) @@ -1118,7 +1125,9 @@ def __init__( def _write(self, value): if type(value) is not tuple: value = (value,) * 3 - for led, v in zip(self._leds, value): + # apply brightness scaling + scaled_value = tuple(v * self._brightness for v in value) + for led, v in zip(self._leds, scaled_value): led.value = v @property @@ -1207,6 +1216,28 @@ def blue(self, value): r, g, b = self.value self.value = r, g, self._from_255(value) + @property + def brightness(self): + """ + Represents the overall brightness of the LED as a value between 0 and 1. + Setting brightness scales all color components proportionally. + """ + return self._brightness + + @brightness.setter + def brightness(self, value): + # clamp value between 0 and 1 + value = max(0.0, min(1.0, float(value))) + # get current unscaled color values + if self._brightness > 0: + # recover original color by dividing by current brightness + current_color = tuple(v / self._brightness for v in self.value) + else: + current_color = self.value + self._brightness = value + # reapply the color which will use new brightness + self.value = current_color + def on(self): """ Turns the LED on. This is equivalent to setting the LED color to white, e.g. diff --git a/tests/test_picozero.py b/tests/test_picozero.py index 1441e3f..dfe816b 100644 --- a/tests/test_picozero.py +++ b/tests/test_picozero.py @@ -434,6 +434,119 @@ def test_rgb_led_alt_values(self): d.close() + def test_rgb_led_brightness_default(self): + d = RGBLED(1, 2, 3) + + # default brightness should be 1.0 + self.assertEqual(d.brightness, 1.0) + + # setting color should not be affected by default brightness + d.value = (0.5, 0.5, 0.5) + self.assertAlmostEqual(d.value[0], 0.5, places=2) + self.assertAlmostEqual(d.value[1], 0.5, places=2) + self.assertAlmostEqual(d.value[2], 0.5, places=2) + + d.close() + + def test_rgb_led_brightness_init(self): + # test brightness parameter in constructor + d = RGBLED(1, 2, 3, brightness=0.5) + + self.assertEqual(d.brightness, 0.5) + + # set color to full white + d.value = (1, 1, 1) + + # actual LED values should be scaled by brightness + self.assertAlmostEqual(d.value[0], 0.5, places=2) + self.assertAlmostEqual(d.value[1], 0.5, places=2) + self.assertAlmostEqual(d.value[2], 0.5, places=2) + + d.close() + + def test_rgb_led_brightness_scaling(self): + d = RGBLED(1, 2, 3, brightness=0.5) + + # set color to red + d.color = (255, 0, 0) + + # check that values are scaled by brightness + self.assertAlmostEqual(d.value[0], 0.5, places=2) + self.assertAlmostEqual(d.value[1], 0.0, places=2) + self.assertAlmostEqual(d.value[2], 0.0, places=2) + + # set partial color + d.color = (128, 64, 32) + + # values should be scaled proportionally + self.assertAlmostEqual(d.value[0], 0.25, places=2) # 128/255 * 0.5 + self.assertAlmostEqual(d.value[1], 0.125, places=2) # 64/255 * 0.5 + self.assertAlmostEqual(d.value[2], 0.0625, places=2) # 32/255 * 0.5 + + d.close() + + def test_rgb_led_brightness_change(self): + d = RGBLED(1, 2, 3) + + # set color first + d.value = (1, 0.5, 0.25) + + # change brightness + d.brightness = 0.5 + + # color ratios should be preserved + self.assertAlmostEqual(d.value[0], 0.5, places=2) + self.assertAlmostEqual(d.value[1], 0.25, places=2) + self.assertAlmostEqual(d.value[2], 0.125, places=2) + + # increase brightness + d.brightness = 0.8 + + self.assertAlmostEqual(d.value[0], 0.8, places=2) + self.assertAlmostEqual(d.value[1], 0.4, places=2) + self.assertAlmostEqual(d.value[2], 0.2, places=2) + + d.close() + + def test_rgb_led_brightness_clamping(self): + # test that brightness is clamped to 0-1 range + d = RGBLED(1, 2, 3, brightness=1.5) + self.assertEqual(d.brightness, 1.0) + d.close() + + d = RGBLED(1, 2, 3, brightness=-0.5) + self.assertEqual(d.brightness, 0.0) + + # test dynamic brightness clamping + d.brightness = 2.0 + self.assertEqual(d.brightness, 1.0) + + d.brightness = -1.0 + self.assertEqual(d.brightness, 0.0) + + d.close() + + def test_rgb_led_brightness_zero(self): + d = RGBLED(1, 2, 3, brightness=0) + + # set color + d.value = (1, 1, 1) + + # all values should be 0 due to brightness + self.assertEqual(d.value[0], 0) + self.assertEqual(d.value[1], 0) + self.assertEqual(d.value[2], 0) + + # changing brightness from zero should work + d.brightness = 0.5 + d.value = (1, 0.5, 0.25) + + self.assertAlmostEqual(d.value[0], 0.5, places=2) + self.assertAlmostEqual(d.value[1], 0.25, places=2) + self.assertAlmostEqual(d.value[2], 0.125, places=2) + + d.close() + def test_servo_default_value(self): d = Servo(1)