A LED strip doesn’t only exist as a single strip, the same system is also used in a LED matrix. In this example, we will control such a 8*32 LED matrix.
Full LED strip code walkthrough, and additional info in this live session with Robert (aka Eitch) and Frank:
This example is based on the Pixelblaze Output Expander (PBOE) JBang example. Make sure to check out the PBOE example, so you fully understand how to set up and use JBang, and connect and control a LED strip via a PBOE.
This example uses the same helper.PixelBlazeOutputExpanderHelper
to send commands to the PBOE. An additional shared code-file helper.ImageHelper
is used to handle images.
As with each JBang example, we need to define the first script line and the dependencies, one in this case, and we need to include the helper-sources. This example also needs some more imports, and we do a static import of the ImageHelper methods, so we can easily use them in the code.
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS com.fazecast:jSerialComm:2.10.2
//SOURCES helper/ImageHelper.java
//SOURCES helper/PixelBlazeOutputExpanderHelper.java
import helper.PixelBlazeOutputExpanderHelper;
import java.io.IOException;
import java.util.Random;
import static helper.ImageHelper.getImageData;
import static helper.ImageHelper.imageToMatrix;
Within the JBang sample project you can find a data directory. In this directory, a number of test images is available, PNGs with a size of 8 pixels height by 32 pixels width, matching the size of the matrix. By defining them in the code as an enum, we can add extra info, like the duration we want to display them.
private enum TestImage {
LINE_1("image_8_32_line_1.png", 250),
LINE_2("image_8_32_line_2.png", 250),
LINE_3("image_8_32_line_3.png", 250),
LINE_4("image_8_32_line_4.png", 250),
LINE_5("image_8_32_line_5.png", 250),
LINE_6("image_8_32_line_6.png", 250),
LINE_7("image_8_32_line_7.png", 250),
LINE_8("image_8_32_line_8.png", 250),
RED("image_8_32_red.png", 500),
GREEN("image_8_32_green.png", 500),
BLUE("image_8_32_blue.png", 500),
STRIPES("image_8_32_stripes.png", 2000),
STRIPES_TEST("image_8_32_stripes_test.png", 2000),
DUKE("image_8_32_duke.png", 2000),
RPI("image_8_32_raspberrypi.png", 2000);
private final String fileName;
private final int duration;
TestImage(String fileName, int duration) {
this.fileName = fileName;
this.duration = duration;
}
public String getFileName() {
return fileName;
}
public int getDuration() {
return duration;
}
}
As we need to send a byte array with red, green, blue values to the LED strip, we need a method to load an image file and return each pixel as three values. There are several methods available in Java to achieve this. I used bufferedImage.getRGB(x, y)
to be able to validate the color returned from every pixel to make it easy to debug.
private static byte[] getImageData(String imagePath) throws IOException {
byte[] imageData = new byte[NUMBER_OF_LEDS * BYTES_PER_PIXEL];
// Open image
File imgPath = new File(imagePath);
BufferedImage bufferedImage = ImageIO.read(imgPath);
// Read color values for each pixel
int pixelCounter = 0;
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 32; x++) {
int color = bufferedImage.getRGB(x, y);
imageData[pixelCounter * BYTES_PER_PIXEL] = (byte) ((color & 0xff0000) >> 16); // Red
imageData[(pixelCounter * BYTES_PER_PIXEL) + 1] = (byte) ((color & 0xff00) >> 8); // Green
imageData[(pixelCounter * BYTES_PER_PIXEL) + 2] = (byte) (color & 0xff); // Blue
pixelCounter++;
}
}
return imageData;
}
While working on this example, I didn’t get the output I was expecting. My assumption was that the LEDs are wired in rows, which would be the easiest way to use them.
Column 1 2 3 4 ... 32
Row 1 IN > > OUT, connected to beginning of row 2
Row 2 IN > > OUT, connected to beginning of row 3
Row 3 IN > > OUT, connected to beginning of row 4
...
With the following code, RED is sent to each LED, one by one, from position 0 to the final LED.
// Check the position of the LEDs, to identify how the LED strip is wired
System.out.println("One by one RED");
for (i = 0; i < NUMBER_OF_LEDS; i++) {
byte[] pixelData = new byte[NUMBER_OF_LEDS * BYTES_PER_PIXEL];
pixelData[i * BYTES_PER_PIXEL] = (byte) 0xff; // red
helper.sendColors(CHANNEL, BYTES_PER_PIXEL, 1, 0, 2, 0, pixelData, false);
Thread.sleep(20);
}
With this code, I found out the wiring, on the matrix that I use, is actually totally different
2191
Column 1 2 3 4 ... 32
IN OUT IN OUT
TO 3 TO 5
⬇ ⬆ ⬇ ⬆ ...
Row 1
Row 2
Row 3
...
Row 8
⬇ ⬆ ⬇ ⬆ ...
OUT IN OUT IN
TO 2 TO 4
As it turns out the order of the LEDs doesn’t match the X/Y system of an image, an extra method is needed to “flip” the X/Y into the actual ordering of the LEDs. Although this method is easy, it took me some iterations to find a correct approach…
private static byte[] imageToMatrix(byte[] imageData) {
byte[] matrixData = new byte[imageData.length];
int indexInImage = 0;;
for (int row = 0; row < 8; row++) {
for (int column = 0; column < 32; column++) {
int indexInMatrix = (column * 8) + (column % 2 == 0 ? row : 7 - row);
//System.out.println("Row : " + row + " / column: " + column + " / index image : " + indexInImage + " / index matrix: " + indexInMatrix);
matrixData[indexInMatrix * BYTES_PER_PIXEL] = imageData[indexInImage * BYTES_PER_PIXEL];
matrixData[(indexInMatrix * BYTES_PER_PIXEL) + 1] = imageData[(indexInImage * BYTES_PER_PIXEL) + 1];
matrixData[(indexInMatrix * BYTES_PER_PIXEL) + 2] = imageData[(indexInImage * BYTES_PER_PIXEL) + 2];
indexInImage++;
}
}
return matrixData;
}
With the enum and additional methods, we can now very easy display all the images for the duration that is defined in each enum value.
// Output all defined images
for (TestImage testImage : TestImage.values()) {
System.out.println("Image: " + testImage);
// Get the bytes from the given image
byte[] pixelData = imageToMatrix(getImageData("data/" + testImage.getFileName()));
// Show the image on the LED matrix
helper.sendColors(CHANNEL, BYTES_PER_PIXEL, 1, 0, 2, 1, pixelData, false);
Thread.sleep(testImage.getDuration());
}
Some other effects are included in the example which are not using images.
// All white to test load on power supply
System.out.println("Full RGB");
byte[] allWhite = new byte[NUMBER_OF_LEDS * BYTES_PER_PIXEL];
for (i = 0; i < NUMBER_OF_LEDS * BYTES_PER_PIXEL; i++) {
allWhite[i] = (byte) 0xff;
}
helper.sendColors(CHANNEL, BYTES_PER_PIXEL, 1, 0, 2, 0, allWhite, false);
Thread.sleep(5000);
// Random colors
Random rd = new Random();
for (i = 0; i < 100; i++) {
byte[] random = new byte[8 * 32 * BYTES_PER_PIXEL];
rd.nextBytes(random);
helper.sendColors(CHANNEL, BYTES_PER_PIXEL, 1, 0, 2, 0, random, false);
Thread.sleep(50);
}
No sudo
is needed for serial communication with the jSerialComm
library, so the application can be started with:
$ jbang PixelblazeOutputExpanderImageMatrix8x32.java
[jbang] Building jar for PixelblazeOutputExpanderImageMatrix8x32.java...
Initializing serial
Opening /dev/ttyS0
All off on channel 2 with 256
One by one RED
Full RGB
Image data/image_8_32_line_1.png loaded with W 32 and H 8
Image data/image_8_32_line_2.png loaded with W 32 and H 8
Image data/image_8_32_line_3.png loaded with W 32 and H 8
Image data/image_8_32_line_4.png loaded with W 32 and H 8
Image data/image_8_32_line_5.png loaded with W 32 and H 8
Image data/image_8_32_line_6.png loaded with W 32 and H 8
Image data/image_8_32_line_7.png loaded with W 32 and H 8
Image data/image_8_32_line_8.png loaded with W 32 and H 8
Image data/image_8_32_red.png loaded with W 32 and H 8
Image data/image_8_32_green.png loaded with W 32 and H 8
Image data/image_8_32_blue.png loaded with W 32 and H 8
Image data/image_8_32_stripes.png loaded with W 32 and H 8
Image data/image_8_32_stripes_test.png loaded with W 32 and H 8
Image data/image_8_32_duke.png loaded with W 32 and H 8
Image data/image_8_32_raspberrypi.png loaded with W 32 and H 8
All off on channel 2 with 256
Closing /dev/ttyS0
Within the GitHub project, you can also find an example to send images to an 8x8 matrix.
By reusing the existing Pixelblaze Output Expander helper code, we are able to control a LED matrix and experiment with images.