I presented my game show LED scoreboard on Show & Tell, and if anyone wants to make one like it, here's what I did.
What I used:
- Pro Trinket 5V (or Adafruit Metro)
- TSOP38238 IR (Infrared) Receiver
- NeoMatrix 32x8
- 40% transmissive white plastic sheet (about 4mm thick)
- RC66RX universal infrared remote control, also called MG32993 (thrift stores or online)
- Fresh AA batteries
- USB charger 2000mA (you'll need 'em)
- USB power breakout cable (I made mine from a broken cord)
- a small solderless breadboard or Perma-Proto board, half size
- 4700uF 10V capacitor
- 470ohm resistor
- Clear parcel tape
- IRlib2
- Each audience member held a small flag which was used to signal a color choice, yellow or blue.
- The game had four rounds. Each round would start with a small church skit that would pause at a certain point.
- The audience was challenged to decide "What would Jesus do?" and was given two possible answers tagged with the two colors.
- Everyone would indicate their choices with the flags, and numbers would be tallied for each color answer, and posted on the scoreboard.
- The skit would conclude and the color of the correct answer would get its tally added to the total score.
- After four rounds, if the total score was greater than the number of audience members playing, victory was declared and prizes awarded (plus a gospel message and prayer). No audience lost the game that day, as intended.
Code: Select all
/* FamilyFunScoreboard_ShowTell_006.ino - Detect IR remote codes from DTV clicker using IRLib2.
* Display a gameshow-type score with color codes on a NeoMatrix 32x8.
* Version 1 - Convert detected remote codes to debounced input.
* Version 2 - Replace char array with string object, more functional.
* Version 3 - Replace test text with actual tally displays.
* Version 4 - Add a display flip function.
* Version 5 - Increase 2 color vote tallies to 4 by swapping displayed color pairs.
* Version 6 - Add a 0-90 minute countdown timer
*/
#include <Adafruit_GFX.h>
#include <Adafruit_NeoMatrix.h>
#include <Adafruit_NeoPixel.h>
// Parameter 1 = number of pixels in strip
// Parameter 2 = pin number (most are valid)
// Parameter 3 = pixel type flags, add together as needed:
// NEO_KHZ800 800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
// NEO_KHZ400 400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
// NEO_GRB Pixels are wired for GRB bitstream (most NeoPixel products)
// NEO_RGB Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
#define NEOPIN 6
Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(32, 8, NEOPIN, NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_COLUMNS + NEO_MATRIX_ZIGZAG, NEO_GRB + NEO_KHZ800);
// These are the protocols from IRlib2 needed to decipher signals
// from NEC standard remote controllers like the RC66RX.
// Different remotes may require different components.
// Visit http://tech.cyborg5.com/irlib/ to learn more.
#include <IRLibDecodeBase.h>
#include <IRLib_P01_NEC.h>
#include <IRLib_P10_DirecTV.h>
#include <IRLibCombo.h> // After all protocols, include this
// All of the above automatically creates a universal decoder
// class called "IRdecode" containing only the protocols you want.
// Now declare an instance of that decoder.
IRdecode myDecoder;
// Include a receiver either this or IRLibRecvPCI or IRLibRecvLoop
#include <IRLibRecv.h>
IRrecv myReceiver(2); //pin number for the receiver
// These are some of the hex values sent from a RC66RX remote with the satellite TV control switch set.
// Other remotes can be substituted. Their hex values should replace the values here.
#define RESTART 0xC364
#define JUMP_END 0xC375
#define RIGHT_ARROW 0xC24D
#define LEFT_ARROW 0xC23D
#define SELECT_BUTTON 0xC25E
#define UP_ARROW 0xC21B
#define DOWN_ARROW 0xC22C
#define BUTTON_0 0xC116
#define BUTTON_1 0xC011
#define BUTTON_2 0xC022
#define BUTTON_3 0xC033
#define BUTTON_4 0xC043
#define BUTTON_5 0xC054
#define BUTTON_6 0xC065
#define BUTTON_7 0xC076
#define BUTTON_8 0xC086
#define BUTTON_9 0xC097
#define BUTN_PLY 0xC30F
#define BUTN_REC 0xC353
#define CHANLUP 0xC0DA
#define CHANLDN 0xC0EB
#define BUTTONRD 0xC418
#define BUTTONGR 0xC43A
#define BUTTONYL 0xC429
#define BUTTONBL 0xC44A
#define BUTNDASH 0xC127
#define BTNENTER 0xC138
#define BUTNPREV 0xC0FC
#define BUTNBACK 0xC270
#define BUTNMENU 0xC20A
#define BUTNINFO 0xC2E5
#define BOUNCE_GAP 100
#define FX_DELAY 60
#define ONE_SEC 1000
#define LED_PIN 13
// A string to display as a title to the game, up to 5 chars
#define TITLE_STR "WWJD"
const uint16_t colorTable[4] = {0x7800, 0x03E0, 0x79E0, 0x000F}; // Table of 4 possible color values, 16-bit red green yellow blue
int colorScores[4] = {0, 0, 0, 0}; // Vote tally numbers for each color
String dispString = TITLE_STR;
void setup() {
pinMode(LED_PIN, OUTPUT);
matrix.begin(); // Initialize all Neopixels to 'off'
matrix.setBrightness(20); // Start with colors at the dimmest
Serial.begin(38400);
delay(2000); while (!Serial); //delay for Leonardo
myReceiver.enableIRIn(); // Start the receiver
}
void loop() {
char typeLetter;
static uint32_t deBounce = 0, newCode, lastCode = 0, effectWait = 0, timeHack = 0, countDown = 0, lastClock = 0;
static int totalScore = 0, keyedVal = 0, keyedSign = 1, activeColor = 0, dispBright = 20, effectStep = 0, effectAdvance = 0, imgFlip = 0;
static boolean keyingInProg = false, animationOn = false, refreshDisplay = true, multiColors = false, swapYB = true, showTimer = false;
// Continue looping until you receive a complete IR signal
if(myReceiver.getResults()) {
if(myDecoder.decode()) {
newCode = myDecoder.value; // A remote control click is detected!
// Debounce and de-repeat RC66RX remote buttons
if((newCode == lastCode) && (millis() < deBounce)) { // The same code in a very short time?
newCode = 0; // It's a bounce/repeat. Clear it.
deBounce = millis() + BOUNCE_GAP; // Restart the time limit.
} else {
lastCode = newCode;
deBounce = millis() + BOUNCE_GAP; // Restart the time limit.
digitalWrite(LED_PIN, HIGH); // LED on while we process a button-press
}
switch(newCode) { // A remote button has been pushed. Which action will happen?
case RESTART: refreshDisplay = true; matrix.setRotation(imgFlip ^= 2); break; // Flip the display 180
case JUMP_END: refreshDisplay = true; showTellBanner(3); break; // "As seen on Show & Tell"
case LEFT_ARROW: refreshDisplay = multiColors = true; animationOn = false;
swapYB = !swapYB; colorRectPush(swapYB); break; // Animate a swap between showing red/green vs. yellow/blue
case RIGHT_ARROW: refreshDisplay = multiColors = true; animationOn = false;
swapYB = !swapYB; colorRectPush(swapYB); break; // Swap again
case UP_ARROW: refreshDisplay = true; matrix.setBrightness(dispBright = dispBright > 60 ? 70 : dispBright + 10); break; // Go brighter
case DOWN_ARROW: refreshDisplay = true; matrix.setBrightness(dispBright = dispBright < 30 ? 20 : dispBright - 10); break; // Go dimmer
// These are for keying in a numeric value
case BUTTON_0: typeLetter = '0'; break;
case BUTTON_1: typeLetter = '1'; break;
case BUTTON_2: typeLetter = '2'; break;
case BUTTON_3: typeLetter = '3'; break;
case BUTTON_4: typeLetter = '4'; break;
case BUTTON_5: typeLetter = '5'; break;
case BUTTON_6: typeLetter = '6'; break;
case BUTTON_7: typeLetter = '7'; break;
case BUTTON_8: typeLetter = '8'; break;
case BUTTON_9: typeLetter = '9'; break;
case BUTNPREV: typeLetter = 'P'; break;
case BUTNDASH: typeLetter = 'D'; break;
case SELECT_BUTTON: typeLetter = 'S'; showTimer = refreshDisplay = true; break; // Finish keyed input for the countdown timer
case BTNENTER: typeLetter = 'E'; break; // Finish keyed input for a color vote tally
case BUTN_PLY: animationOn = !animationOn; refreshDisplay = true; multiColors = false; break; // Animated highlight of current display
case BUTN_REC: colorScores[0] = colorScores[1] = colorScores[2] = colorScores[3] = 0;
animationOn = false; multiColors = refreshDisplay = true; break; // Reset & display color tallies
case CHANLUP: dispString = String(totalScore += colorScores[activeColor]); multiColors = false; refreshDisplay = true; break; // Add active color's tally to total score
case CHANLDN: dispString = String(totalScore -= colorScores[activeColor]); multiColors = false; refreshDisplay = true; break; // Deduct active color's tally
case BUTTONRD: activeColor = 0; colorWipeOut(colorTable[activeColor]);
animationOn = swapYB = false; multiColors = refreshDisplay = true; break; // Change active color to red
case BUTTONGR: activeColor = 1; colorWipeOut(colorTable[activeColor]);
animationOn = swapYB = false; multiColors = refreshDisplay = true; break; // Green
case BUTTONYL: activeColor = 2; colorWipeOut(colorTable[activeColor]);
animationOn = false; swapYB = multiColors = refreshDisplay = true; break; // Yellow
case BUTTONBL: activeColor = 3; colorWipeOut(colorTable[activeColor]);
animationOn = false; swapYB = multiColors = refreshDisplay = true; break; // Blue
case BUTNBACK: animationOn = false; multiColors = refreshDisplay = true; break; // Display color tallies
case BUTNMENU: dispString = String(TITLE_STR); multiColors = showTimer = false; refreshDisplay = true; break; // Display title text
case BUTNINFO: dispString = String(totalScore); multiColors = showTimer = false; refreshDisplay = true; break; // Display total score
default: typeLetter = 0; // Unrecognized input, ignore
}
// Manage numeric input
if((typeLetter >= '0') && (typeLetter <= '9')) { // Digit pressed?
if(keyedVal < 3276) // Prevent overflow
keyedVal = keyedVal * 10 + ((int)typeLetter - '0'); // Make it the next digit in an input number
keyingInProg = true;
showTimer = animationOn = false;
numberInColor(keyedVal * keyedSign, colorTable[activeColor]); // Display it
} else if(typeLetter == 'D') { // Minus key pressed?
keyedSign *= -1; // Toggle the sign
keyingInProg = true;
showTimer = animationOn = false;
numberInColor(keyedVal * keyedSign, colorTable[activeColor]);
} else if(typeLetter == 'P') { // Previous (backspace) key pressed?
keyedVal /= 10; // Knock off the smallest digit
keyingInProg = true;
showTimer = animationOn = false;
numberInColor(keyedVal * keyedSign, colorTable[activeColor]);
} else if(typeLetter == 'E') { // Enter key pressed?
colorScores[activeColor] = keyedVal * keyedSign; // Place finished number into color tally array
keyedVal = 0;
keyedSign = 1;
dispString = String(colorScores[activeColor]);
swapYB = (activeColor > 1);
keyingInProg = showTimer = animationOn = false;
refreshDisplay = multiColors = true;
typeLetter = 0;
} else if(typeLetter == 'S') { // Show/change countdown?
if(keyedVal * keyedSign > 0) { // Positive integer?
newCode = keyedVal < 9000 ? keyedVal : 9000; // 90 minutes max-ish in a 32-bit variable
countDown = millis() + (newCode % 100) * ONE_SEC + (newCode / 100) * 60 * ONE_SEC + ONE_SEC;
}
keyedVal = 0;
keyedSign = 1;
keyingInProg = multiColors = false;
showTimer = refreshDisplay = true;
typeLetter = 0;
} else if(typeLetter != 0) { // Some other key interrupting? Abort numeric input
keyedVal = 0;
keyedSign = 1;
keyingInProg = false;
}
digitalWrite(LED_PIN, LOW); // LED off, button-press processing done
}
myReceiver.enableIRIn(); // Restart receiver, resume IR listening
}
timeHack = millis();
if(countDown > timeHack) { // Countdown clock not expired yet?
timeHack = (countDown - timeHack) / ONE_SEC; // Compute remaining seconds
if(timeHack != lastClock) { // Has a full second elapsed?
lastClock = timeHack;
if(showTimer) { // Is countdown currently on display?
dispString = String(lastClock / 60); // Build a string from minutes:seconds remaining
dispString += String(':');
dispString += String((lastClock % 60) / 10);
dispString += String(lastClock % 10);
refreshDisplay = !animationOn;
}
}
} else
lastClock = 0;
if(animationOn && (millis() > effectWait)) { // Animated backdrop in progress with update due?
effectWait = millis() + FX_DELAY; // Schedule next update
++effectAdvance;
if(millis() & 2048L) { // Alternate between 2 patterns
for(int counter1 = 0; counter1 < 32; ++counter1)
matrix.drawFastVLine(31 - counter1, 0, 8, colorTable[(effectAdvance + counter1) & 3]); // Marching vertical color stripes
} else {
for(int counter1 = 0; counter1 < 8; ++counter1)
matrix.drawFastHLine(0, 7 - counter1, 32, colorTable[(effectAdvance + counter1) & 3]); // Horizontal stripes
}
if(effectAdvance & 14) // Overprint a flashing text string
matrix.setTextColor(0xFFFF); // White on xparent
else
matrix.setTextColor(0x0000); // Black on xparent
matrix.setTextSize(1);
matrix.setTextWrap(false);
matrix.setCursor(16 - dispString.length() * 3, 0); // Center justify, top row
matrix.print(dispString);
matrix.show();
refreshDisplay = false;
}
if(refreshDisplay) { // Shall we repaint the display?
if(multiColors) { // with current color tally numbers?
matrix.setTextColor(0xFFFF); // Text will be white on xparent
matrix.setTextSize(1);
matrix.setTextWrap(false);
if(swapYB) { // Are yellow/blue the currently visible pair?
matrix.fillRect( 0, 0, 16, 8, colorTable[2]); // Yellow left half BG
matrix.fillRect(16, 0, 16, 8, colorTable[3]); // Blue Right half
dispString = String(colorScores[2]); // Yellow tally
matrix.setCursor(0, 0); // Left justified
matrix.print(dispString);
dispString = String(colorScores[3]); // Blue tally
matrix.setCursor(33 - dispString.length() * 6, 0); // Right justified
matrix.print(dispString);
} else { // Repaint the red/green tally numbers
matrix.fillRect( 0, 0, 16, 8, colorTable[0]); // Red left half BG
matrix.fillRect(16, 0, 16, 8, colorTable[1]); // Green Right half
dispString = String(colorScores[0]); // Red tally
matrix.setCursor(0, 0); // Left justified
matrix.print(dispString);
dispString = String(colorScores[1]); // Green tally
matrix.setCursor(33 - dispString.length() * 6, 0); // Right justified
matrix.print(dispString);
}
matrix.show();
refreshDisplay = false;
dispString = String(totalScore);
} else { // Show a simple string over black
matrix.fillScreen(0); // Black the BG
matrix.setTextColor(0xFFFF); // White on xparent
matrix.setTextSize(1);
matrix.setTextWrap(false);
matrix.setCursor(16 - dispString.length() * 3, 0); // Center justified
matrix.print(dispString);
matrix.show();
refreshDisplay = false;
}
}
}
void numberInColor(int keyedVal, uint16_t matrixColor) { // Display a decimal number in color over black, left justified
matrix.fillScreen(0x0000);
matrix.setTextColor(matrixColor, 0x0000); // Active color on black
matrix.setTextSize(1);
matrix.setTextWrap(false);
matrix.setCursor(0, 0);
matrix.print(keyedVal);
matrix.show();
}
void colorWipeOut(uint16_t matrixColor) { // Two-part motion effect indicating change of active color
int counter1;
for(counter1 = 0; counter1 < 8; ++counter1) { // Vertical wipe color in from top
matrix.drawFastHLine(0, counter1, 32, matrixColor);
matrix.show();
delay(10);
}
for(counter1 = 0; counter1 < 16; ++counter1) { // Horiz wipe black from center out
matrix.drawFastVLine(15 - counter1, 0, 8, 0x0000);
matrix.drawFastVLine(16 + counter1, 0, 8, 0x0000);
matrix.show();
delay(3);
}
}
void colorRectPush(boolean swapYB) { // 2 color rectangles push 2 others offscreen
int counter1, colorIndex = 2;
if(swapYB)
colorIndex ^= 2;
if(!swapYB) {
for(counter1 = 0; counter1 < 32; ++counter1) { // Left-to-right push
matrix.fillRect(counter1 , 0, 16, 8, colorTable[colorIndex ]); // Departing
matrix.fillRect(counter1 + 16, 0, 16, 8, colorTable[colorIndex + 1]); // colors
matrix.fillRect(counter1 - 31, 0, 16, 8, colorTable[(colorIndex ^ 2) ]); // Arriving
matrix.fillRect(counter1 - 15, 0, 16, 8, colorTable[(colorIndex ^ 2) + 1]); // colors
matrix.show();
delay(1);
}
} else {
for(counter1 = 31; counter1 >= 0; --counter1) { // Right-to-left push
matrix.fillRect(counter1 - 31, 0, 16, 8, colorTable[colorIndex ]); // Departing
matrix.fillRect(counter1 - 15, 0, 16, 8, colorTable[colorIndex + 1]); // colors
matrix.fillRect(counter1 , 0, 16, 8, colorTable[(colorIndex ^ 2) ]); // Arriving
matrix.fillRect(counter1 + 16, 0, 16, 8, colorTable[(colorIndex ^ 2) + 1]); // colors
matrix.show();
delay(1);
}
}
}
void showTellBanner(int pauseTime) {
int offSet = 152;
matrix.setTextColor(0x441F); // Adafruit blue on xparent
matrix.setTextSize(1);
matrix.setTextWrap(false);
for(int counter1 = 32; counter1 > -170; --counter1) { // Draw the Show & Tell icons
matrix.fillScreen(0x0000);
matrix.fillRoundRect(counter1 , 0, 11, 7, 2, colorTable[0]);
matrix.fillRoundRect(counter1 + offSet, 0, 11, 7, 2, colorTable[0]);
matrix.fillRect(counter1 + 1 , 1, 6, 5, 0xFFFF);
matrix.fillRect(counter1 + 1 + offSet, 1, 6, 5, 0xFFFF);
matrix.fillTriangle(counter1 + 7 , 3, counter1 + 9 , 1, counter1 + 9 , 5, 0xFFFF);
matrix.fillTriangle(counter1 + 7 + offSet, 3, counter1 + 9 + offSet, 1, counter1 + 9 + offSet, 5, 0xFFFF);
matrix.drawLine(counter1 + 2, 3, counter1 + 5, 3, colorTable[0]);
matrix.drawLine(counter1 + 2 + offSet, 3, counter1 + 5 + offSet, 3, colorTable[0]);
matrix.drawLine(counter1 + 4, 2, counter1 + 4, 4, colorTable[0]);
matrix.drawLine(counter1 + 4 + offSet, 2, counter1 + 4 + offSet, 4, colorTable[0]);
matrix.drawPixel(counter1 + 3 , 4, colorTable[0]);
matrix.drawPixel(counter1 + 3 + offSet, 4, colorTable[0]);
matrix.setCursor(counter1 + 16, 0);
matrix.print("AS SEEN ON SHOW & TELL");
matrix.show();
delay(pauseTime);
}
}
These are the functions of the buttons on a RC66RX:
- Color buttons - Four color-coded tally numbers are available for counting audience votes. Two can be displayed at any time, either the red/green pair or the yellow/blue pair, but only one color can be active at a time. A color button will display its color tally and make it active.
- Numeric buttons - These key in a number when needed. They appear in the currently active color.
- DASH - Change the sign of the number keyed in.
- PREV - Backspace, remove a digit from the number keyed in.
- ENTER - Move the keyed-in number into the active color's tally, replacing the previous value.
- SELECT - Parse the keyed-in number as MM:SS, limiting it to 90 minutes max, move it into the countdown clock and start the countdown. If no number is keyed in, display the latest running countdown, if any.
- CHAN UP - Add the active color's tally into the total score and display it.
- CHAN DOWN - Deduct the active color's tally from the total score and display it.
- BACK - display (but don't change) one of the color tally pairs.
- MENU - display the game title string.
- INFO - display (but don't change) the total score.
- Left arrow & Right arrow - Switch between displaying red/green pair and yellow/blue pair.
- REC - Reset all tallies to zero and display the most recent color pair.
- Up arrow - Increase brightness
- Down arrow - Decrease brightness
- Play - Start/stop a colorful animated backdrop to the game title, countdown clock, or total score.
- Loop back - Flip the display 180 degrees
- Entering numbers is a little unusual. The sketch doesn't assemble a string of digits. It adds each digit into a running value and displays that. Backspacing the digits away doesn't clear the number being entered, it just reduces it to zero, so an unwanted zero can be entered by accident sometimes. Be careful.
- A NeoMatrix of 32x8 pixels has enough width for only 5 characters. More than that will overlap or extend offscreen. Not pretty.
- The countdown clock just counts to zero and stops. If it's stopped at zero, the SELECT button won't do anything until a new countdown number is entered.
- The clock runs by counting millis(), which can have quite a range of inaccuracy. If you can determine how many millis() on your hardware are in a true second, you can replace #define ONE_SEC 1000 with your number and possibly make your timer more accurate.
- The total score has no button to zero it. You can deduct the total amount, or add a negative number to reset the total to zero. (I wanted no accidental resets during a game.)
- Adapting the sketch to respond to other IR remotes is possible, involving mainly replacing the hex codes for each button. Remotes with fewer buttons may have to sacrifice some functions, like brightness control or some animated effects.
- Mr. LadyAda challenged me to add an Easter egg to the scoreboard. Can you find it?
Edit: The code received a little polish, more streamlined, more tidier.
Hallelujah!
Disciple