Skip to content

Commit 781ef0a

Browse files
committed
Refactor RGBLEDBoard tests and update docstring
1 parent 11aa011 commit 781ef0a

File tree

2 files changed

+267
-16
lines changed

2 files changed

+267
-16
lines changed

gpiozero/boards.py

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,11 +1057,11 @@ def __init__(self, pwm=False, initial_value=False, pin_factory=None):
10571057

10581058
class RGBLEDBoard(CompositeOutputDevice):
10591059
"""
1060-
Extends :class:`CompositeOutputDevice`. and represents a generic RGBLED
1061-
board or collection of LEDs.
1060+
Extends :class:`CompositeOutputDevice`. and represents a generic RGB LED
1061+
board or collection of RGB LEDs.
10621062
1063-
The following example turns on all the LEDs on a board containing 5 LEDs
1064-
attached to GPIO pins 2 through 6::
1063+
The following example creates a device containing three RGB LEDs and turns
1064+
them on red, green and blue respectfully:
10651065
10661066
from gpiozero import RGBLEDBoard
10671067
@@ -1080,7 +1080,7 @@ def __init__(self, *args, **kwargs):
10801080
order = kwargs.pop('_order', None)
10811081

10821082
if len(args) == 0 and len(kwargs) == 0:
1083-
raise ValueError(
1083+
raise GPIOPinMissing(
10841084
'at least 1 sequence of RGB pins must be provided')
10851085
else:
10861086
try:
@@ -1089,7 +1089,7 @@ def __init__(self, *args, **kwargs):
10891089
for arg in kwargs.values():
10901090
r, g, b = arg
10911091
except:
1092-
raise ValueError(
1092+
raise GPIOPinMissing(
10931093
'pins must be provided as a tuple of 3-tuples '
10941094
'e.g. RGBLEDBoard((2, 3, 4), (5, 6, 7)')
10951095

@@ -1152,6 +1152,60 @@ def toggle(self, *args):
11521152
else:
11531153
super(RGBLEDBoard, self).toggle()
11541154

1155+
def blink(self, on_time=1, off_time=1, fade_in_time=0, fade_out_time=0,
1156+
on_color=(1, 1, 1), off_color=(0, 0, 0), n=None, background=True):
1157+
"""
1158+
Make all the LEDs in the device turn on and off repeatedly.
1159+
1160+
:param float on_time:
1161+
Number of seconds on. Defaults to 1 second.
1162+
1163+
:param float off_time:
1164+
Number of seconds off. Defaults to 1 second.
1165+
1166+
:param float fade_in_time:
1167+
Number of seconds to spend fading in. Defaults to 0. Must be 0 if
1168+
``pwm`` was ``False`` when the class was constructed
1169+
(:exc:`ValueError` will be raised if not).
1170+
1171+
:param float fade_out_time:
1172+
Number of seconds to spend fading out. Defaults to 0. Must be 0 if
1173+
``pwm`` was ``False`` when the class was constructed
1174+
(:exc:`ValueError` will be raised if not).
1175+
1176+
:param tuple on_color:
1177+
The color to use when the LED is "on". Defaults to white.
1178+
1179+
:param tuple off_color:
1180+
The color to use when the LED is "off". Defaults to black.
1181+
1182+
:param int n:
1183+
Number of times to blink; ``None`` (the default) means forever.
1184+
1185+
:param bool background:
1186+
If ``True`` (the default), start a background thread to continue
1187+
blinking and return immediately. If ``False``, only return when the
1188+
blink is finished (warning: the default value of *n* will result in
1189+
this method never returning).
1190+
"""
1191+
if isinstance(self._leds[0]._leds[0], LED):
1192+
if fade_in_time:
1193+
raise ValueError('fade_in_time must be 0 with non-PWM RGBLEDs')
1194+
if fade_out_time:
1195+
raise ValueError('fade_out_time must be 0 with non-PWM RGBLEDs')
1196+
self._stop_blink()
1197+
self._blink_thread = GPIOThread(
1198+
target=self._blink_device,
1199+
args=(
1200+
on_time, off_time, fade_in_time, fade_out_time,
1201+
on_color, off_color, n
1202+
)
1203+
)
1204+
self._blink_thread.start()
1205+
if not background:
1206+
self._blink_thread.join()
1207+
self._blink_thread = None
1208+
11551209
def _stop_blink(self, led=None):
11561210
if led is None:
11571211
if self._blink_thread:
@@ -1161,6 +1215,40 @@ def _stop_blink(self, led=None):
11611215
with self._blink_lock:
11621216
self._blink_leds.remove(led)
11631217

1218+
def _blink_device(self, on_time, off_time, fade_in_time, fade_out_time, n,
1219+
on_color, off_color, nfps=25):
1220+
sequence = []
1221+
if fade_in_time > 0:
1222+
sequence += [
1223+
(i * (1 / fps) / fade_in_time, 1 / fps)
1224+
for i in range(int(fps * fade_in_time))
1225+
]
1226+
sequence.append((1, on_time))
1227+
if fade_out_time > 0:
1228+
sequence += [
1229+
(1 - (i * (1 / fps) / fade_out_time), 1 / fps)
1230+
for i in range(int(fps * fade_out_time))
1231+
]
1232+
sequence.append((0, off_time))
1233+
sequence = (
1234+
cycle(sequence) if n is None else
1235+
chain.from_iterable(repeat(sequence, n))
1236+
)
1237+
with self._blink_lock:
1238+
self._blink_leds = list(self.leds)
1239+
for led in self._blink_leds:
1240+
if led._controller not in (None, self):
1241+
led._controller._stop_blink(led)
1242+
led._controller = self
1243+
for value, delay in sequence:
1244+
with self._blink_lock:
1245+
if not self._blink_leds:
1246+
break
1247+
for led in self._blink_leds:
1248+
led._write(value)
1249+
if self._blink_thread.stopping.wait(delay):
1250+
break
1251+
11641252

11651253
class TrafficLightsBuzzer(CompositeOutputDevice):
11661254
"""

tests/test_boards.py

Lines changed: 173 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,6 @@ def test_led_board_nested():
261261
assert pin3.state
262262

263263
def test_led_board_bad_blink():
264-
pin1 = Device.pin_factory.pin(2)
265-
pin2 = Device.pin_factory.pin(3)
266-
pin3 = Device.pin_factory.pin(4)
267264
with LEDBoard(2, LEDBoard(3, 4)) as board:
268265
with pytest.raises(ValueError):
269266
board.blink(fade_in_time=1, fade_out_time=1)
@@ -846,13 +843,179 @@ def test_rgbledboard_initial_value_no_pwm():
846843
board.value = ((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
847844
assert board.value == ((0, 0, 0), (0, 0, 0))
848845

849-
def test_rgbledboard_blink():
850-
with RGBLEDBoard((2, 3, 4), (5, 6, 7)) as board:
851-
board.blink()
852-
assert board.value == ((1, 1, 1), (1, 1, 1))
853-
board.off()
854-
board[0].blink()
855-
assert board.value == ((1, 1, 1), (0, 0, 0))
846+
# start
847+
def test_rgbledboard_bad_blink():
848+
with RGBLEDBoard((2, 3, 4), (5, 6, 7), pwm=False) as board:
849+
with pytest.raises(ValueError):
850+
board.blink(fade_in_time=1, fade_out_time=1)
851+
with pytest.raises(ValueError):
852+
board.blink(fade_out_time=1)
853+
with pytest.raises(ValueError):
854+
board.pulse()
855+
856+
@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'),
857+
reason='timing is too random on pypy')
858+
def test_rgbledboard_blink_background():
859+
pins = [Device.pin_factory.pin(n) for n in (2, 3, 4, 5, 6, 7)]
860+
with RGBLEDBoard((2, 3, 4), (5, 6, 7), pwm=False) as board:
861+
# Instantiation takes a long enough time that it throws off our timing
862+
# here!
863+
for pin in pins:
864+
pin.clear_states()
865+
board.blink(0.1, 0.1, n=2)
866+
board._blink_thread.join() # naughty, but ensures no arbitrary waits in the test
867+
test = [
868+
(0.0, False),
869+
(0.0, True),
870+
(0.1, False),
871+
(0.1, True),
872+
(0.1, False)
873+
]
874+
for pin in pins:
875+
pin.assert_states_and_times(test)
876+
877+
@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'),
878+
reason='timing is too random on pypy')
879+
def test_rgbledboard_blink_foreground():
880+
pins = [Device.pin_factory.pin(n) for n in (2, 3, 4, 5, 6, 7)]
881+
with RGBLEDBoard((2, 3, 4), (5, 6, 7), pwm=False) as board:
882+
for pin in pins:
883+
pin.clear_states()
884+
board.blink(0.1, 0.1, n=2, background=False)
885+
test = [
886+
(0.0, False),
887+
(0.0, True),
888+
(0.1, False),
889+
(0.1, True),
890+
(0.1, False)
891+
]
892+
for pin in pins:
893+
pin.assert_states_and_times(test)
894+
895+
@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'),
896+
reason='timing is too random on pypy')
897+
def test_rgbledboard_blink_control():
898+
pins = [Device.pin_factory.pin(n) for n in (2, 3, 4, 5, 6, 7)]
899+
with RGBLEDBoard((2, 3, 4), (5, 6, 7), pwm=False) as board:
900+
for pin in pins:
901+
pin.clear_states()
902+
board.blink(0.1, 0.1, n=2)
903+
# make sure the blink thread's started
904+
while not board._blink_leds:
905+
sleep(0.00001) # pragma: no cover
906+
board[1][0].off() # immediately take over the second LED
907+
board._blink_thread.join() # naughty, but ensures no arbitrary waits in the test
908+
test = [
909+
(0.0, False),
910+
(0.0, True),
911+
(0.1, False),
912+
(0.1, True),
913+
(0.1, False)
914+
]
915+
test2 = [(0.0, False), (0.0, True), (0.0, False)]
916+
for pin in pins[:3]:
917+
pin.assert_states_and_times(test)
918+
for pin in pins[3:]:
919+
pin.assert_states_and_times(test2)
920+
921+
@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'),
922+
reason='timing is too random on pypy')
923+
def test_rgbledboard_blink_take_over():
924+
pins = [Device.pin_factory.pin(n) for n in (2, 3, 4, 5, 6, 7)]
925+
with RGBLEDBoard((2, 3, 4), (5, 6, 7), pwm=False) as board:
926+
for pin in pins:
927+
pin.clear_states()
928+
board[1].blink(0.1, 0.1, n=2)
929+
board.blink(0.1, 0.1, n=2) # immediately take over blinking
930+
board[1]._blink_thread.join()
931+
board._blink_thread.join()
932+
test = [
933+
(0.0, False),
934+
(0.0, True),
935+
(0.1, False),
936+
(0.1, True),
937+
(0.1, False)
938+
]
939+
for pin in pins:
940+
pin.assert_states_and_times(test)
941+
942+
@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'),
943+
reason='timing is too random on pypy')
944+
def test_rgbledboard_blink_control_all():
945+
pins = [Device.pin_factory.pin(n) for n in (2, 3, 4, 5, 6, 7)]
946+
with RGBLEDBoard((2, 3, 4), (5, 6, 7), pwm=False) as board:
947+
for pin in pins:
948+
pin.clear_states()
949+
board.blink(0.1, 0.1, n=2)
950+
# make sure the blink thread's started
951+
while not board._blink_leds:
952+
sleep(0.00001) # pragma: no cover
953+
board[0].off() # immediately take over all LEDs
954+
board[1].off()
955+
board._blink_thread.join() # blink should terminate here anyway
956+
test = [
957+
(0.0, False),
958+
(0.0, True),
959+
(0.0, False),
960+
]
961+
for pin in pins:
962+
pin.assert_states_and_times(test)
963+
964+
def test_rgbledboard_blink_interrupt_on():
965+
pins = [Device.pin_factory.pin(n) for n in (2, 3, 4, 5, 6, 7)]
966+
with RGBLEDBoard((2, 3, 4), (5, 6, 7), pwm=False) as board:
967+
board.blink(1, 0.1)
968+
sleep(0.2)
969+
board.off() # should interrupt while on
970+
for pin in pins:
971+
pin.assert_states([False, True, False])
972+
973+
def test_rgbledboard_blink_interrupt_off():
974+
pins = [Device.pin_factory.pin(n) for n in (2, 3, 4, 5, 6, 7)]
975+
with RGBLEDBoard((2, 3, 4), (5, 6, 7), pwm=False) as board:
976+
for pin in pins:
977+
pin.clear_states()
978+
board.blink(0.1, 1)
979+
sleep(0.2)
980+
board.off() # should interrupt while off
981+
for pin in pins:
982+
pin.assert_states([False, True, False])
983+
984+
@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'),
985+
reason='timing is too random on pypy')
986+
def test_rgbledboard_fade_background():
987+
pins = [Device.pin_factory.pin(n) for n in (2, 3, 4, 5, 6, 7)]
988+
with RGBLEDBoard((2, 3, 4), (5, 6, 7), pwm=False) as board:
989+
for pin in pins:
990+
pin.clear_states()
991+
board.blink(0, 0, 0.2, 0.2, n=2)
992+
board._blink_thread.join()
993+
test = [
994+
(0.0, 0),
995+
(0.04, 0.2),
996+
(0.04, 0.4),
997+
(0.04, 0.6),
998+
(0.04, 0.8),
999+
(0.04, 1),
1000+
(0.04, 0.8),
1001+
(0.04, 0.6),
1002+
(0.04, 0.4),
1003+
(0.04, 0.2),
1004+
(0.04, 0),
1005+
(0.04, 0.2),
1006+
(0.04, 0.4),
1007+
(0.04, 0.6),
1008+
(0.04, 0.8),
1009+
(0.04, 1),
1010+
(0.04, 0.8),
1011+
(0.04, 0.6),
1012+
(0.04, 0.4),
1013+
(0.04, 0.2),
1014+
(0.04, 0),
1015+
]
1016+
for pin in pins:
1017+
pin.assert_states_and_times(test)
1018+
# end
8561019

8571020
def test_traffic_lights_buzzer():
8581021
red_pin = Device.pin_factory.pin(2)

0 commit comments

Comments
 (0)