diff --git a/.gitignore b/.gitignore index 5568a6e..697cdd6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ library/debian/ dist/ __pycache__ .DS_Store +.coverage +coverage.xml diff --git a/library/pytest.ini b/library/pytest.ini new file mode 100644 index 0000000..4af6669 --- /dev/null +++ b/library/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --cov=rpi_ws281x --cov-report xml diff --git a/library/rpi_ws281x/__init__.py b/library/rpi_ws281x/__init__.py index 4eafcbe..f48f469 100644 --- a/library/rpi_ws281x/__init__.py +++ b/library/rpi_ws281x/__init__.py @@ -1,5 +1,5 @@ # New canonical package, to support `import rpi_ws281x` -from .rpi_ws281x import PixelStrip, Adafruit_NeoPixel, Color, RGBW, ws +from .rpi_ws281x import PixelStrip, InvalidStrip, Adafruit_NeoPixel, Color, RGBW, ws from _rpi_ws281x import * __version__ = '5.0.0' diff --git a/library/rpi_ws281x/rpi_ws281x.py b/library/rpi_ws281x/rpi_ws281x.py index f9d6a8a..b0d165e 100644 --- a/library/rpi_ws281x/rpi_ws281x.py +++ b/library/rpi_ws281x/rpi_ws281x.py @@ -11,6 +11,10 @@ def __new__(self, r, g=None, b=None, w=None): else: if w is None: w = 0 + if g is None: + g = 0 + if b is None: + b = 0 return int.__new__(self, (w << 24) | (r << 16) | (g << 8) | b) @property @@ -48,6 +52,9 @@ def __init__(self, num, pin, freq_hz=800000, dma=10, invert=False, 800khz), dma, the DMA channel to use (default 10), invert, a boolean specifying if the signal line should be inverted (default False), and channel, the PWM channel to use (defaults to 0). + + All the methods of a PixelSubStrip are available on PixelStrip + objects. """ if gamma is None: @@ -89,6 +96,18 @@ def __init__(self, num, pin, freq_hz=800000, dma=10, invert=False, self.size = num + # Create a PixelSubStrip and delegate these methods to it + self.main_strip = self.PixelSubStrip(self, 0, num=num) + self.setPixelColor = self.main_strip.setPixelColor + self.setPixelColorRGB = self.main_strip.setPixelColorRGB + self.setBrightness = self.main_strip.setBrightness + self.getBrightness = self.main_strip.getBrightness + self.getPixels = self.main_strip.getPixels + self.getPixelColor = self.main_strip.getPixelColor + self.getPixelColorRGB = self.main_strip.getPixelColorRGB + self.getPixelColorRGBW = self.main_strip.getPixelColorRGBW + self.off = self.main_strip.off + # Substitute for __del__, traps an exit condition and cleans up properly atexit.register(self._cleanup) @@ -106,20 +125,30 @@ def __getitem__(self, pos): def __setitem__(self, pos, value): """Set the 24-bit RGB color value at the provided position or slice of - positions. + positions. If value is a slice it is zip()'ed with pos to set as many + leds as there are values. """ # Handle if a slice of positions are passed in by setting the appropriate # LED data values to the provided value. + # Cast to int() as value may be a numpy non-int value. if isinstance(pos, slice): - for n in range(*pos.indices(self.size)): - ws.ws2811_led_set(self._channel, n, value) + try: + for n, c in zip(range(*pos.indices(self.size)), value): + ws.ws2811_led_set(self._channel, n, int(c)) + except TypeError: + for n in range(*pos.indices(self.size)): + ws.ws2811_led_set(self._channel, n, int(value)) # Else assume the passed in value is a number to the position. else: - return ws.ws2811_led_set(self._channel, pos, value) + return ws.ws2811_led_set(self._channel, pos, int(value)) def __len__(self): return ws.ws2811_channel_t_count_get(self._channel) + def numPixels(self): + """Return the number of pixels in the display.""" + return len(self) + def _cleanup(self): # Clean up memory used by the library when not needed anymore. if self._leds is not None: @@ -149,46 +178,148 @@ def show(self): str_resp = ws.ws2811_get_return_t_str(resp) raise RuntimeError('ws2811_render failed with code {0} ({1})'.format(resp, str_resp)) - def setPixelColor(self, n, color): - """Set LED at position n to the provided 24-bit color value (in RGB order). - """ - self[n] = color - - def setPixelColorRGB(self, n, red, green, blue, white=0): - """Set LED at position n to the provided red, green, and blue color. - Each color component should be a value from 0 to 255 (where 0 is the - lowest intensity and 255 is the highest intensity). - """ - self.setPixelColor(n, Color(red, green, blue, white)) + def createPixelSubStrip(self, first, last=None, num=None): + """Create a PixelSubStrip starting with pixel `first` + Either specify the `num` of pixels or the `last` pixel. - def getBrightness(self): - return ws.ws2811_channel_t_brightness_get(self._channel) + All the methods of a PixelSubStrip are available on PixelStrip + objects. - def setBrightness(self, brightness): - """Scale each LED in the buffer by the provided brightness. A brightness - of 0 is the darkest and 255 is the brightest. + Note: PixelSubStrips are not prevented from overlappping """ - ws.ws2811_channel_t_brightness_set(self._channel, brightness) + return self.PixelSubStrip(self, first, last=last, num=num) - def getPixels(self): - """Return an object which allows access to the LED display data as if - it were a sequence of 24-bit RGB values. - """ - return self[:] - def numPixels(self): - """Return the number of pixels in the display.""" - return len(self) + class PixelSubStrip: + """A PixelSubStrip handles a subset of the pixels in a PixelStrip - def getPixelColor(self, n): - """Get the 24-bit RGB color value for the LED at position n.""" - return self[n] + strip = PixelStrip(...) + strip1 = strip.createPixelSubStrip(0, num=10) # controls first 10 pixels + strip2 = strip.createPixelSubStrip(10, num=10) # controls next 10 pixels - def getPixelColorRGB(self, n): - return RGBW(self[n]) + strip2[5] will access the 15th pixel + """ - def getPixelColorRGBW(self, n): - return RGBW(self[n]) + def __init__(self, strip, first, last=None, num=None): + self.strip = strip + if first < 0: + raise InvalidStrip(f"First pixel is negative ({first}).") + if first > len(strip): + raise InvalidStrip(f"First pixel is too big ({first})." + f"Strip only has {len(strip)}.") + self.first = first + if last: + if last < 0: + raise InvalidStrip(f"Last pixel is negative ({last}).") + if last > len(strip): + raise InvalidStrip(f"Too many pixels ({last})." + f"Strip only has {len(strip)}.") + self.last = last + self.num = last - first + elif num: + if num < 0: + raise InvalidStrip(f"number of pixels is negative ({num}).") + if first + num > len(strip): + raise InvalidStrip(f"Too many pixels (last would be {first + num})." + f"Strip only has {len(strip)}.") + self.last = first + num + self.num = num + else: + raise InvalidStrip("Must specify number or last pixel to " + "create a PixelSubStrip") + + def __len__(self): + return self.num + + def _adjust_pos(self, pos): + # create an adjusted pos, either a slice or index + if isinstance(pos, slice): + if pos.start and pos.start < 0: + apos_start = self.first + self.num + pos.start + else: + apos_start = (0 if pos.start is None else pos.start) + self.first + + if pos.stop and pos.stop < 0: + apos_stop = pos.stop + self.last + else: + apos_stop = (self.num if pos.stop is None else pos.stop) + self.first + apos = slice(apos_start, + apos_stop, + pos.step) + return apos + if pos < 0: + return self.num + pos + self.first + return pos + self.first + + + def __getitem__(self, pos): + """Return the 24-bit RGB color value at the provided position or slice + of positions. + """ + return self.strip[self._adjust_pos(pos)] + + def __setitem__(self, pos, value): + """Set the 24-bit RGB color value at the provided position or slice of + positions. If value is a slice it is zip()'ed with pos to set as many + leds as there are values. + """ + self.strip[self._adjust_pos(pos)] = value + + def setPixelColor(self, n, color): + """Set LED at position n to the provided 24-bit color value (in RGB order). + If n is a slice then color can be a value which is repeated for all leds + or a slice of values which are applied to the leds. + """ + self[n] = color + + def setPixelColorRGB(self, n, red, green, blue, white=0): + """Set LED at position n to the provided red, green, and blue color. + Each color component should be a value from 0 to 255 (where 0 is the + lowest intensity and 255 is the highest intensity). + """ + # Translation to n done in setPixelColor + self[n] = Color(red, green, blue, white) + + def getBrightness(self): + return ws.ws2811_channel_t_brightness_get(self.strip._channel) + + def setBrightness(self, brightness): + """Scale each LED in the buffer by the provided brightness. A brightness + of 0 is the darkest and 255 is the brightest. + + This method affects all pixels in all PixelSubStrips. + """ + ws.ws2811_channel_t_brightness_set(self.strip._channel, brightness) + + def getPixels(self): + """Return an object which allows access to the LED display data as if + it were a sequence of 24-bit RGB values. + """ + return self[:] + + def numPixels(self): + """Return the number of pixels in the strip.""" + return self.num + + def getPixelColor(self, n): + """Get the 24-bit RGB color value for the LED at position n.""" + return self[n] + + def getPixelColorRGB(self, n): + return RGBW(self[n]) + + def getPixelColorRGBW(self, n): + return RGBW(self[n]) + + def show(self): + self.strip.show() + + def off(self): + self[:] = 0 + self.strip.show() + +class InvalidStrip(Exception): + pass # Shim for back-compatibility class Adafruit_NeoPixel(PixelStrip): diff --git a/library/tests/conftest.py b/library/tests/conftest.py index a017cd0..d625127 100644 --- a/library/tests/conftest.py +++ b/library/tests/conftest.py @@ -39,6 +39,7 @@ def ws2811_led_get(ch, n): @pytest.fixture(scope='function', autouse=False) def _rpi_ws281x(): _mock_rpi_ws281x.ws2811_init.return_value = 0 + _mock_rpi_ws281x.ws2811_render.return_value = 0 sys.modules['_rpi_ws281x'] = _mock_rpi_ws281x yield _mock_rpi_ws281x diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 0d8bc11..3861215 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -1,11 +1,25 @@ import pytest +@pytest.fixture() +def strip(_rpi_ws281x): + from rpi_ws281x import PixelStrip + strip = PixelStrip(10, 20) + strip.begin() + strip.off() + yield strip def test_setup(_rpi_ws281x): from rpi_ws281x import PixelStrip strip = PixelStrip(10, 20) strip.begin() +def test_setup_compat(_rpi_ws281x): + from rpi_ws281x import PixelStrip + strip = PixelStrip(10, 20, strip_type=[1.0]*256) + strip.begin() + _rpi_ws281x.ws2811_channel_t_strip_type_set.assert_called_with(strip._channel, + _rpi_ws281x.WS2811_STRIP_GRB) + _rpi_ws281x.ws2811_channel_t_gamma_set.assert_called_with(strip._channel,[1.0]*256) def test_setup_init_fail(_rpi_ws281x): from rpi_ws281x import PixelStrip @@ -14,20 +28,52 @@ def test_setup_init_fail(_rpi_ws281x): with pytest.raises(RuntimeError): strip.begin() +def test_cleanup(_rpi_ws281x, strip): + assert _rpi_ws281x.ws2811_fini.call_count == 0 + assert _rpi_ws281x.delete_ws2811_t.call_count == 0 + strip._cleanup() + assert _rpi_ws281x.ws2811_fini.call_count == 1 + assert _rpi_ws281x.delete_ws2811_t.call_count == 1 + assert strip._leds is None + assert strip._channel is None -def test_num_pixels(_rpi_ws281x): - from rpi_ws281x import PixelStrip - strip = PixelStrip(10, 20) - strip.begin() +def test_show(_rpi_ws281x, strip): + assert _rpi_ws281x.ws2811_render.call_count == 1 + strip.show() + assert _rpi_ws281x.ws2811_render.call_count == 2 + sub = strip.createPixelSubStrip(5, last=10) + sub.show() + assert _rpi_ws281x.ws2811_render.call_count == 3 + + _rpi_ws281x.ws2811_render.return_value = 1 + with pytest.raises(RuntimeError): + sub.show() + +def test_brightness(_rpi_ws281x, strip): + assert _rpi_ws281x.ws2811_channel_t_brightness_get.call_count == 0 + strip.getBrightness() + assert _rpi_ws281x.ws2811_channel_t_brightness_get.call_count == 1 + calls = _rpi_ws281x.ws2811_channel_t_brightness_set.call_count + strip.setBrightness(20) + assert _rpi_ws281x.ws2811_channel_t_brightness_set.call_count == calls + 1 + +def test_gamma(_rpi_ws281x, strip): + calls = _rpi_ws281x.ws2811_channel_t_gamma_set.call_count + strip.setGamma(1.0) + assert _rpi_ws281x.ws2811_channel_t_gamma_set.call_count == calls + strip.setGamma([1.0, 1.0]) + assert _rpi_ws281x.ws2811_channel_t_gamma_set.call_count == calls + strip.setGamma([1.0] * 256) + assert _rpi_ws281x.ws2811_channel_t_gamma_set.call_count == calls + 1 + + +def test_num_pixels(strip): assert len(strip[:]) == 10 assert len(strip) == 10 assert strip.numPixels() == 10 - -def test_set_pixel(_rpi_ws281x): - from rpi_ws281x import PixelStrip, RGBW - strip = PixelStrip(10, 20) - strip.begin() +def test_set_pixel(strip): + from rpi_ws281x import RGBW strip[0] = RGBW(255, 0, 0) assert strip[0] == strip.getPixelColor(0) assert strip[0] == RGBW(255, 0, 0) @@ -35,18 +81,132 @@ def test_set_pixel(_rpi_ws281x): assert strip.getPixelColorRGBW(0) == RGBW(255, 0, 0) assert strip.getPixelColorRGBW(0).r == 255 - -def test_set_multiple(_rpi_ws281x): - from rpi_ws281x import PixelStrip, RGBW - strip = PixelStrip(10, 20) - strip.begin() +def test_set_multiple(strip): + from rpi_ws281x import RGBW strip[:] = RGBW(255, 0, 0) assert strip[:] == [RGBW(255, 0, 0)] * 10 +def test_set_pixel_slice(strip): + from rpi_ws281x import RGBW + colours = [RGBW(i*10, 0, 0) for i in range(0, 10)] + strip[0:10] = colours + for i in range(0, 10): + assert strip[i] == RGBW(i*10, 0, 0) -def test_set_odd(_rpi_ws281x): - from rpi_ws281x import PixelStrip, RGBW - strip = PixelStrip(10, 20) - strip.begin() +def test_set_odd(strip): + from rpi_ws281x import RGBW strip[::2] = RGBW(255, 0, 0) assert strip[:] == [RGBW(255, 0, 0), RGBW(0, 0, 0)] * 5 + + +def test_create_substrip(strip): + from rpi_ws281x import InvalidStrip + with pytest.raises(InvalidStrip): + sub = strip.createPixelSubStrip(100) + with pytest.raises(InvalidStrip): + sub = strip.createPixelSubStrip(5) + with pytest.raises(InvalidStrip): + sub = strip.createPixelSubStrip(5, 11) + with pytest.raises(InvalidStrip): + sub = strip.createPixelSubStrip(5, -2) + with pytest.raises(InvalidStrip): + sub = strip.createPixelSubStrip(5, num=6) + with pytest.raises(InvalidStrip): + sub = strip.createPixelSubStrip(-1, num=5) + with pytest.raises(InvalidStrip): + sub = strip.createPixelSubStrip(5, num=-2) + sub = strip.createPixelSubStrip(5, last=10) + assert len(sub[:]) == 5 + assert len(sub) == 5 + assert sub.numPixels() == 5 + sub = strip.createPixelSubStrip(5, num=5) + assert len(sub[:]) == 5 + assert len(sub) == 5 + assert sub.numPixels() == 5 + +def test_adjust_pos(strip): + sub = strip.createPixelSubStrip(5, 10) + assert sub._adjust_pos(3) == 8 + assert sub._adjust_pos(-2) == 8 + assert sub._adjust_pos(slice(None, None)) == slice(5, 10) # [:] + assert sub._adjust_pos(slice(0, None)) == slice(5, 10) # [0:] + assert sub._adjust_pos(slice(None, 5)) == slice(5, 10) # [:5] + + assert sub._adjust_pos(slice(-1, None)) == slice(9, 10) # [-1:] (start, step default to None) + assert sub._adjust_pos(slice(-2, None)) == slice(8, 10) # [-2:] + assert sub._adjust_pos(slice(None, -1)) == slice(5, 9) # [:-1] + assert sub._adjust_pos(slice(None, -2)) == slice(5, 8) # [:-2] + assert sub._adjust_pos(slice(-1, 0)) == slice(9, 5) # [-1:0] + + assert sub._adjust_pos(slice(2, 4)) == slice(7, 9) # [2:4] + assert sub._adjust_pos(slice(None, None, -2)) == slice(5, 10, -2) # [::-2] + +def test_substrip_set(strip): + from rpi_ws281x import RGBW + sub = strip.createPixelSubStrip(5, 10) + + sub[0] = RGBW(1, 1, 1) + assert strip[5] == RGBW(1, 1, 1) + + sub.setPixelColor(2, RGBW(2, 2, 2)) + assert strip[7] == RGBW(2, 2, 2) + + sub.setPixelColorRGB(3, 3, 4, 5) + assert strip[8] == RGBW(3, 4, 5) + + strip.off() + sub[1:4] = RGBW(1, 2, 3) + assert strip[6:9] == sub[1:4] + assert strip[6:9] == [RGBW(1, 2, 3)] * 3 + assert strip.getPixels() == [RGBW(0, 0, 0)] * 6 + [RGBW(1, 2, 3)] * 3 + [RGBW(0, 0, 0)] + + strip.off() + colours = [RGBW(i, i, i) for i in range(1, 4)] + sub[1:4] = colours + print(f"strip is {strip[:]}") + + assert strip[6:9] == sub[1:4] + assert strip[6:9] == colours + + strip.off() + sub = strip.createPixelSubStrip(2, 10) + + sub[-1:0:-2] = [RGBW(i, i, i) for i in range(1, 9)] + assert strip[:] == ([RGBW(0, 0, 0)] + + [RGBW(0, 0, 0)] + + [RGBW(0, 0, 0)] + + [RGBW(4, 4, 4)] + + [RGBW(0, 0, 0)] + + [RGBW(3, 3, 3)] + + [RGBW(0, 0, 0)] + + [RGBW(2, 2, 2)] + + [RGBW(0, 0, 0)] + + [RGBW(1, 1, 1)]) + + strip.off() + sub[-1:2:-2] = [RGBW(i, i, i) for i in range(1, 9)] + assert strip[:] == ([RGBW(0, 0, 0)] + + [RGBW(0, 0, 0)] + + [RGBW(0, 0, 0)] + + [RGBW(0, 0, 0)] + + [RGBW(0, 0, 0)] + + [RGBW(3, 3, 3)] + + [RGBW(0, 0, 0)] + + [RGBW(2, 2, 2)] + + [RGBW(0, 0, 0)] + + [RGBW(1, 1, 1)]) + +def test_RGBW(): + from rpi_ws281x import RGBW + c = RGBW(0x50) + assert c == 0x50 + assert c.r == 0x00 and c.g == 0x00 and c.b == 0x50 and c.w == 0x0 + c = RGBW(0x50, g=0x60) + assert c.r == 0x50 and c.g == 0x60 and c.b == 0x00 and c.w == 0x00 + c = RGBW(0x50, b=0x60) + assert c.r == 0x50 and c.g == 0x00 and c.b == 0x60 and c.w == 0x00 + c = RGBW(0x50, 0x60, 0x70) + assert c.r == 0x50 and c.g == 0x60 and c.b == 0x70 and c.w == 0x00 + c = RGBW(0x50, 0x60, 0x70, 0x80) + assert c.r == 0x50 and c.g == 0x60 and c.b == 0x70 and c.w == 0x80 + assert c == (0x80 << 24) + (0x50 << 16) + (0x60 << 8) + 0x70