We had a busy time last weekend at Maker Festival, and we'd really like to thank everyone who stopped by to buy something, chat, or just marvel at our blinking lights.
One of the big hits of our stand was the Flying Toaster panel. If you're old enough to remember when people would actually buy screensavers for their computers, you'll recognise this iconic bundle of pixels from Berkeley Systems' After Dark screensaver.
If you wish to keep the toaster flying in your home, here's what I used to build the display:
You could also use an Arduino Uno to drive the the panel, but the backpack has the advantage of power and signal connectors that plug directly into the LED matrix. After you've built the backpack (following Nootropic Design's excellent build instructions), here's the code you'll need to get the animation running:
/* Toaster32x32.ino - Flying Toasters in 32x32 RGB LED matrix scruss - 2017-07 - for elmwood.to freely modified from - Phillip Burgess's Animated Flying Toasters for Adafruit https://learn.adafruit.com/animated-flying-toaster-oled-jewelry - Adafruit RGB matrix examples by Limor Fried and Phillip Burgess https://learn.adafruit.com/32x16-32x32-rgb-led-matrix The toaster bitmaps are most likely lifted from Berkeley Systems' "After Dark" screen saver for the Apple Macintosh. */
#include <Adafruit_GFX.h> // Core graphics library #include <RGBmatrixPanel.h> // Hardware-specific library #include "bitmaps.h" // animation frame definitions // Colour definitions - format is RGB-565 16-bit #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define WHITE 0xFFFF // array of visible colours, stepped through in loop uint16_t colarray[] = { BLUE, RED, GREEN, CYAN, MAGENTA, YELLOW, WHITE }; #define COLMOD 7 // array modulus for looping int colsub = 0; // current colour array position // configuration for // 32x32 matrix with SINGLE HEADER input pinout // talking to Nootropic Matrix Backpack v2 #define CLK 8 #define OE 9 #define LAT 10 #define A A0 #define B A1 #define C A2 #define D A3 // If your matrix has the DOUBLE HEADER input, // please use values from RGB matrix panel code examples RGBmatrixPanel matrix(A, B, C, D, CLK, LAT, OE, false); int i = 0; // animation frame counter void setup() { matrix.begin(); // initialise LED matrix matrix.fillScreen(BLACK); // clear the screen } void loop() { // first draw mask in black to clean up stray pixels // mask[i] frame is simply inverse of img[i] frame matrix.drawBitmap(0, 0, (const uint8_t *)pgm_read_word(&mask[i]), 32, 32, BLACK); // next draw bitmap in the colour we want matrix.drawBitmap(0, 0, (const uint8_t *)pgm_read_word(&img[i]), 32, 32, colarray[colsub]); i++; if (i == 4) { // reset frame count as there are only 4 frames i = 0; // change colour too at end of each animation colsub++; colsub %= COLMOD; } delay(75); // leisurely flying toasters ensue }
You'll need to add a file tab to the sketch called bitmaps.h for the toaster animation frames. You can do this with the little down-arrow (⏷) tab at the end of the Arduino IDE menu bar, then select “New Tab”. Paste this code in the new tab:
/* bitmaps.h - Toaster32x32 bitmap definitions scruss 2017-07 - for elmwood.to generated by image2cpp https://javl.github.io/image2cpp/ from images provided in Phillip Burgess's Animated Flying Toasters for Adafruit https://learn.adafruit.com/animated-flying-toaster-oled-jewelry */ // AVR-specific code for storing data in program flash memory #include <avr/pgmspace.h> static const uint8_t PROGMEM toastermask0[] = { // toaster0 mask - inverse of toaster0 image 0xff, 0xcf, 0xff, 0xff, 0xff, 0x17, 0xff, 0xff, 0xfe, 0x27, 0xff, 0xff, 0xfc, 0x8b, 0x0f, 0xff, 0xf8, 0x10, 0x01, 0xff, 0xf8, 0x43, 0xf8, 0x7f, 0xf0, 0x1f, 0x80, 0x1f, 0xf0, 0x7c, 0x3f, 0x8f, 0xe1, 0xf1, 0xf8, 0xf7, 0xe7, 0xc7, 0xc7, 0x00, 0xcf, 0x1f, 0x08, 0x2b, 0x9e, 0x7c, 0x11, 0x00, 0x84, 0xf0, 0x20, 0x57, 0x61, 0xe4, 0x22, 0x01, 0x78, 0x48, 0x40, 0xaf, 0x4e, 0x10, 0x44, 0x07, 0x53, 0x90, 0x81, 0xff, 0x5c, 0xa0, 0x90, 0x5f, 0x7c, 0xa1, 0x07, 0x97, 0x74, 0xa1, 0x21, 0x3f, 0x7f, 0xb5, 0x0e, 0x37, 0x77, 0xb0, 0x90, 0x7f, 0x7f, 0xb4, 0x60, 0xf7, 0x77, 0xbb, 0x03, 0xef, 0x7f, 0xbc, 0x0f, 0xdf, 0x77, 0xbf, 0xff, 0x3f, 0x7f, 0xbf, 0xfc, 0xff, 0xbf, 0xbf, 0xf3, 0xff, 0xdf, 0xbf, 0xcf, 0xff, 0xe7, 0xbe, 0x3f, 0xff, 0xf9, 0xb1, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xff }, toastermask1[] = { // toaster1 mask - inverse of toaster1 image 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0x0f, 0xff, 0xff, 0x08, 0x01, 0xff, 0xfe, 0x43, 0xf8, 0x7f, 0xfc, 0x1f, 0x80, 0x1f, 0xf8, 0x7c, 0x3f, 0x8f, 0xf1, 0xf1, 0xf8, 0x07, 0xe7, 0xc7, 0xc1, 0xe7, 0xcf, 0x1f, 0x06, 0x17, 0x9e, 0x7c, 0x00, 0x07, 0x84, 0xf0, 0x07, 0xf7, 0x61, 0xe4, 0x18, 0x0f, 0x78, 0x48, 0x20, 0x80, 0x4e, 0x10, 0x40, 0x2b, 0x53, 0x90, 0x81, 0x00, 0x5c, 0xa0, 0x80, 0x59, 0x7c, 0xa1, 0x02, 0x07, 0x74, 0xa1, 0x28, 0xbf, 0x7f, 0xb5, 0x0f, 0xf7, 0x77, 0xb0, 0x90, 0x7f, 0x7f, 0xb4, 0x60, 0xf7, 0x77, 0xbb, 0x03, 0xef, 0x7f, 0xbc, 0x0f, 0xdf, 0x77, 0xbf, 0xff, 0x3f, 0x7f, 0xbf, 0xfc, 0xff, 0xbf, 0xbf, 0xf3, 0xff, 0xdf, 0xbf, 0xcf, 0xff, 0xe7, 0xbe, 0x3f, 0xff, 0xf9, 0xb1, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xff }, toastermask2[] = { // toaster2 mask - inverse of toaster2 image 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xc3, 0xf8, 0x7f, 0xff, 0x1f, 0x80, 0x1f, 0xfc, 0x7c, 0x3f, 0x8f, 0xf1, 0xf1, 0xf8, 0x07, 0xe7, 0xc7, 0xc1, 0xe7, 0xcf, 0x1f, 0x06, 0x17, 0x9e, 0x7c, 0x00, 0x07, 0x84, 0xf0, 0x00, 0x07, 0x61, 0xe4, 0x00, 0x0f, 0x78, 0x48, 0x00, 0x17, 0x4e, 0x10, 0x00, 0x1f, 0x53, 0x90, 0x60, 0x77, 0x5c, 0xa0, 0x9f, 0xc9, 0x7c, 0xa1, 0x04, 0x92, 0x74, 0xa1, 0x09, 0x24, 0x7f, 0xb5, 0x02, 0x49, 0x77, 0xb0, 0x80, 0x11, 0x7f, 0xb4, 0x41, 0x03, 0x77, 0xbb, 0x20, 0x43, 0x7f, 0xbc, 0x10, 0x07, 0x77, 0xbf, 0xf8, 0x0f, 0x7f, 0xbf, 0xfc, 0x1f, 0xbf, 0xbf, 0xf3, 0x3f, 0xdf, 0xbf, 0xcf, 0xff, 0xe7, 0xbe, 0x3f, 0xff, 0xf9, 0xb1, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xff }, toaster0[] = { 0x00, 0x30, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x01, 0xD8, 0x00, 0x00, 0x03, 0x74, 0xF0, 0x00, 0x07, 0xEF, 0xFE, 0x00, 0x07, 0xBC, 0x07, 0x80, 0x0F, 0xE0, 0x7F, 0xE0, 0x0F, 0x83, 0xC0, 0x70, 0x1E, 0x0E, 0x07, 0x08, 0x18, 0x38, 0x38, 0xFF, 0x30, 0xE0, 0xF7, 0xD4, 0x61, 0x83, 0xEE, 0xFF, 0x7B, 0x0F, 0xDF, 0xA8, 0x9E, 0x1B, 0xDD, 0xFE, 0x87, 0xB7, 0xBF, 0x50, 0xB1, 0xEF, 0xBB, 0xF8, 0xAC, 0x6F, 0x7E, 0x00, 0xA3, 0x5F, 0x6F, 0xA0, 0x83, 0x5E, 0xF8, 0x68, 0x8B, 0x5E, 0xDE, 0xC0, 0x80, 0x4A, 0xF1, 0xC8, 0x88, 0x4F, 0x6F, 0x80, 0x80, 0x4B, 0x9F, 0x08, 0x88, 0x44, 0xFC, 0x10, 0x80, 0x43, 0xF0, 0x20, 0x88, 0x40, 0x00, 0xC0, 0x80, 0x40, 0x03, 0x00, 0x40, 0x40, 0x0C, 0x00, 0x20, 0x40, 0x30, 0x00, 0x18, 0x41, 0xC0, 0x00, 0x06, 0x4E, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00 }, toaster1[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xF0, 0x00, 0x00, 0xF7, 0xFE, 0x00, 0x01, 0xBC, 0x07, 0x80, 0x03, 0xE0, 0x7F, 0xE0, 0x07, 0x83, 0xC0, 0x70, 0x0E, 0x0E, 0x07, 0xF8, 0x18, 0x38, 0x3E, 0x18, 0x30, 0xE0, 0xF9, 0xE8, 0x61, 0x83, 0xFF, 0xF8, 0x7B, 0x0F, 0xF8, 0x08, 0x9E, 0x1B, 0xE7, 0xF0, 0x87, 0xB7, 0xDF, 0x7F, 0xB1, 0xEF, 0xBF, 0xD4, 0xAC, 0x6F, 0x7E, 0xFF, 0xA3, 0x5F, 0x7F, 0xA6, 0x83, 0x5E, 0xFD, 0xF8, 0x8B, 0x5E, 0xD7, 0x40, 0x80, 0x4A, 0xF0, 0x08, 0x88, 0x4F, 0x6F, 0x80, 0x80, 0x4B, 0x9F, 0x08, 0x88, 0x44, 0xFC, 0x10, 0x80, 0x43, 0xF0, 0x20, 0x88, 0x40, 0x00, 0xC0, 0x80, 0x40, 0x03, 0x00, 0x40, 0x40, 0x0C, 0x00, 0x20, 0x40, 0x30, 0x00, 0x18, 0x41, 0xC0, 0x00, 0x06, 0x4E, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00 }, toaster2[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x07, 0xFE, 0x00, 0x00, 0x3C, 0x07, 0x80, 0x00, 0xE0, 0x7F, 0xE0, 0x03, 0x83, 0xC0, 0x70, 0x0E, 0x0E, 0x07, 0xF8, 0x18, 0x38, 0x3E, 0x18, 0x30, 0xE0, 0xF9, 0xE8, 0x61, 0x83, 0xFF, 0xF8, 0x7B, 0x0F, 0xFF, 0xF8, 0x9E, 0x1B, 0xFF, 0xF0, 0x87, 0xB7, 0xFF, 0xE8, 0xB1, 0xEF, 0xFF, 0xE0, 0xAC, 0x6F, 0x9F, 0x88, 0xA3, 0x5F, 0x60, 0x36, 0x83, 0x5E, 0xFB, 0x6D, 0x8B, 0x5E, 0xF6, 0xDB, 0x80, 0x4A, 0xFD, 0xB6, 0x88, 0x4F, 0x7F, 0xEE, 0x80, 0x4B, 0xBE, 0xFC, 0x88, 0x44, 0xDF, 0xBC, 0x80, 0x43, 0xEF, 0xF8, 0x88, 0x40, 0x07, 0xF0, 0x80, 0x40, 0x03, 0xE0, 0x40, 0x40, 0x0C, 0xC0, 0x20, 0x40, 0x30, 0x00, 0x18, 0x41, 0xC0, 0x00, 0x06, 0x4E, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00 }; // mask frame array for animation // note that here frames 2 & 4 are the same const uint8_t* const mask[] PROGMEM = { toastermask0, toastermask1, toastermask2, toastermask1 }; // image frame array for animation // note that here frames 2 & 4 are the same const uint8_t* const img[] PROGMEM = { toaster0, toaster1, toaster2, toaster1 };
Defining animation frames of your own is relatively simple. The Flying Toasters are just four bitmap frames, each 32×32 pixels:
Image: | ||||
Frame: | 1 | 2 | 3 | 4 (same as 2) |
In the old days, we'd have to work out bitmaps on squared paper. It was slow and annoying, but now entirely superseded by the image2cpp tool. You can drag and drop images on it, and it will create the correct data for copying into the Arduino IDE.
Unlike making an animated GIF, however, LED animations need to explicitly blank out the previous frames, or you'll quickly end up with a display that looks like this, with all the frames overlapping:
So each frame has to be written in two stages:
Since the animation frames are written to the Arduino's flash memory and can't be modified by the sketch as it runs, we have to define the masks as well as the image in the code. This is really easily done through image2cpp by selecting “Invert image colors” for each frame.
Image: | ||||
Mask: | ||||
Frame: | 1 | 2 | 3 | 4 (same as 2) |
The masks look a little confusing as the white pixels above are actually drawn in black on the RGB matrix to erase what was there before.