/* * Board: Teensy 3.6 * Target: Countdown Timer * Author: Clive "Max" Maxfield (max@clivemaxfield.com) * License: The MIT License (See full license at the bottom of this file) * * Notes: V01 Just get the Teensy to talk to the Lixies * V02 Generate random values * V03 Add first rollover effect * V04 Change from counting 0 to 9 or 9 to 0 and generate random values instead * V05 Add counting 0 to 9 or 9 to 0 as an option * V06 Add color fade to rollover effect */ // LIBRARIES ============================================================================================== #include // Library to drive the NeoPixels // DEFINITIONS ============================================================================================ //#define DEBUG 1 // Lixie and NeoPixel Stuff #define NUM_WS2812_PER_DIGIT 2 // WS2812 tri-color LEDs (like Adafruit NeoPixels) #define NUM_DIGITS_PER_LIXIE 10 #define NUM_LIXIES_PER_CLUSTER 2 #define NUM_CLUSTERS 6 #define NUM_LIXIES 12 // NUM_LIXIES_PER_CLUSTER * NUM_CLUSTERS #define NUM_LIXIES_PER_STRIP 40 // NUM_WS2812_PER_DIGIT * NUM_DIGITS_PER_LIXIE * NUM_LIXIES_PER_CLUSTER // Colors #define WHITE 0xFFFFFFU #define BLACK 0x000000U #define NA 0x000000U #define RED 0xFF0000U #define GREEN 0x00FF00U #define BLUE 0x0000FFU #define YELLOW 0xFFFF00U #define CYAN 0x00FFFFU #define HOT_PINK 0xFF00FFU #define DARK_ORANGE 0xFF8000U #define AMBER 0xFFC000U #define LIME_GREEN 0xC0FF00U #define CHARTREUSE 0x80FF00U #define SPRING_GREEN 0x00FFC0U #define AZURE 0x0080FFU #define ELECTRIC_VIOLET 0x8000FFU // Rollover Effect (One Digit at a Time) #define LSD_TO_MSD 0 // LSD/MSD = Least/Most Siginificant Digit #define MSD_TO_LSD 1 #define ROLL_WITH_SPECIAL_COLORS 0 #define ROLL_WITH_EXISTING_COLORS 1 #define ROLL_WITH_RANDOM_VALUES 0 #define ROLL_WITH_COUNT_VALUES 1 #define NUM_ROLLS_TOTAL 6 // Number of 0 to 9 (or 9 to 0) rolls per digit #define NUM_ROLLS_TO_FLIP 2 // Number of rolls on current digit before next one starts //#define NUM_ROLLS_TOTAL 1 // Number of 0 to 9 (or 9 to 0) rolls per digit //#define NUM_ROLLS_TO_FLIP 1 // Number of rolls on current digit before next one starts #define ROLL_WAITING 0 // Used to define the status of each digit #define ROLL_ROLLING 1 #define ROLL_DONE 2 #define ROLL_INTERVAL 50 // Miliseconds // More Stuff #define CYCLE_TIME 1000 // Milliseconds #define UPLOAD_TIME 1250 // Microseconds #define MULTIPLIER 1 // Used to slow transitions (recommend 1, 2, 4, 8, etc.) // INSTANTIATIONS ========================================================================================= DMAMEM int32_t DisplayMemory[NUM_LIXIES_PER_STRIP * 6]; //int32_t DrawingMemory[NULL]; const int32_t OctoConfig = WS2811_GRB | WS2811_800kHz; OctoWS2811 Neos(NUM_LIXIES_PER_STRIP, DisplayMemory, NULL, OctoConfig); // OctoWS2811 Neos(NUM_LIXIES_PER_STRIP, DisplayMemory, DrawingMemory, config); // GLOBAL VARIABLES ======================================================================================= struct DigitData_t { int32_t leftNeo; int32_t rightNeo; int32_t nextUp; int32_t nextDown; }; DigitData_t DigitData[NUM_DIGITS_PER_LIXIE] { { .leftNeo = 13, .rightNeo = 3, .nextUp = 1, .nextDown = 9}, // Digit 0 { .leftNeo = 14, .rightNeo = 4, .nextUp = 2, .nextDown = 0}, // Digit 1 { .leftNeo = 12, .rightNeo = 2, .nextUp = 3, .nextDown = 1}, // Digit 2 { .leftNeo = 10, .rightNeo = 0, .nextUp = 4, .nextDown = 2}, // Digit 3 { .leftNeo = 18, .rightNeo = 8, .nextUp = 5, .nextDown = 3}, // Digit 4 { .leftNeo = 16, .rightNeo = 6, .nextUp = 6, .nextDown = 4}, // Digit 5 { .leftNeo = 15, .rightNeo = 5, .nextUp = 7, .nextDown = 5}, // Digit 6 { .leftNeo = 17, .rightNeo = 7, .nextUp = 8, .nextDown = 6}, // Digit 7 { .leftNeo = 19, .rightNeo = 9, .nextUp = 9, .nextDown = 7}, // Digit 8 { .leftNeo = 11, .rightNeo = 1, .nextUp = 0, .nextDown = 8} // Digit 9 }; struct LixieData_t { int32_t oldValue; int32_t newValue; int32_t firstNeo; int32_t rollStatus; int32_t rollCount; }; LixieData_t LixieData[NUM_LIXIES] { {.oldValue = 0, .newValue = 0, .firstNeo = 0}, // Lixie 0 Seconds 0 LSD {.oldValue = 0, .newValue = 0, .firstNeo = 20}, // Lixie 1 Seconds 1 {.oldValue = 0, .newValue = 0, .firstNeo = 40}, // Lixie 2 Minutes 0 {.oldValue = 0, .newValue = 0, .firstNeo = 60}, // Lixie 3 Minutes 1 {.oldValue = 0, .newValue = 0, .firstNeo = 80}, // Lixie 4 Hours 0 {.oldValue = 0, .newValue = 0, .firstNeo = 100}, // Lixie 5 Hours 1 {.oldValue = 0, .newValue = 0, .firstNeo = 120}, // Lixie 6 Days 0 {.oldValue = 0, .newValue = 0, .firstNeo = 140}, // Lixie 7 Days 1 {.oldValue = 0, .newValue = 0, .firstNeo = 160}, // Lixie 8 Months 0 {.oldValue = 0, .newValue = 0, .firstNeo = 180}, // Lixie 9 Months 1 {.oldValue = 0, .newValue = 0, .firstNeo = 200}, // Lixie 10 Years 0 {.oldValue = 0, .newValue = 0, .firstNeo = 220} // Lixie 11 Years 1 MSD }; // MAIN FUNCTIONS ========================================================================================= void setup() { #ifdef DEBUG Serial.begin(9600); delay(2000); #endif // Initialize all NeoPixels to 'off' Neos.begin(); Neos.show(); delay(10); // Generate and display some initial random values LoadRandomValues(); CopyNewValuesToOldValues(); for (int iLix = 0; iLix < NUM_LIXIES; iLix++) { Neos.setPixel( (DigitData[ LixieData[iLix].oldValue ].leftNeo + LixieData[iLix].firstNeo), YELLOW); Neos.setPixel( (DigitData[ LixieData[iLix].oldValue ].rightNeo + LixieData[iLix].firstNeo), YELLOW); } Neos.show(); delay(3000); } void loop() { // LoadNullValues(); LoadRandomValues(); PerformRollover(LSD_TO_MSD, ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_COUNT_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_COUNT_VALUES, 1, 1, // NUM_ROLLS_TOTAL, NUM_ROLLS_TO_FLIP WHITE, WHITE, YELLOW, YELLOW, DARK_ORANGE, DARK_ORANGE); // Old Left, Old Right, New Left, New Right delay(3000); // LoadNullValues(); LoadRandomValues(); PerformRollover(MSD_TO_LSD, ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_COUNT_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_COUNT_VALUES, 1, 1, // NUM_ROLLS_TOTAL, NUM_ROLLS_TO_FLIP, WHITE, WHITE, DARK_ORANGE, DARK_ORANGE, YELLOW, YELLOW); delay(3000); // LoadNullValues(); LoadRandomValues(); PerformRollover(LSD_TO_MSD, ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_COUNT_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_COUNT_VALUES, 6, 2, // NUM_ROLLS_TOTAL, NUM_ROLLS_TO_FLIP WHITE, WHITE, YELLOW, YELLOW, HOT_PINK, HOT_PINK); // Old Left, Old Right, New Left, New Right delay(3000); // LoadNullValues(); LoadRandomValues(); PerformRollover(MSD_TO_LSD, // ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_RANDOM_VALUES, ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_COUNT_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_COUNT_VALUES, 6, 2, // NUM_ROLLS_TOTAL, NUM_ROLLS_TO_FLIP, WHITE, WHITE, HOT_PINK, HOT_PINK, LIME_GREEN, LIME_GREEN); delay(3000); // LoadNullValues(); LoadRandomValues(); PerformRollover(LSD_TO_MSD, ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_COUNT_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_COUNT_VALUES, 1, 1, // NUM_ROLLS_TOTAL, NUM_ROLLS_TO_FLIP WHITE, WHITE, BLACK, BLACK, BLACK, BLACK); // Old Left, Old Right, New Left, New Right delay(3000); // LoadNullValues(); LoadRandomValues(); PerformRollover(LSD_TO_MSD, ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_COUNT_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_COUNT_VALUES, 4, 2, // NUM_ROLLS_TOTAL, NUM_ROLLS_TO_FLIP WHITE, WHITE, BLACK, BLACK, RED, BLUE); // Old Left, Old Right, New Left, New Right delay(3000); // LoadNullValues(); LoadRandomValues(); PerformRollover(MSD_TO_LSD, ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_SPECIAL_COLORS, ROLL_WITH_COUNT_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_RANDOM_VALUES, // ROLL_WITH_EXISTING_COLORS, ROLL_WITH_COUNT_VALUES, 4, 2, // NUM_ROLLS_TOTAL, NUM_ROLLS_TO_FLIP WHITE, WHITE, BLACK, BLACK, BLUE, GREEN); // Old Left, Old Right, New Left, New Right delay(3000); } void PerformRollover (int32_t whichDirection, int32_t whichRollColors, int32_t whichRollValues, int32_t numRollsTotal, int32_t numRollsToFlip, uint32_t rollColorLeft, uint32_t rollColorRight, uint32_t oldColorLeft, uint32_t oldColorRight, uint32_t newColorLeft, uint32_t newColorRight) { int32_t firstLixie; int32_t lastLixie; int32_t iLixie; int32_t terminatingLixie; int32_t deltaLixie; int32_t tmpValue; int32_t numSteps; int32_t flipStep; uint32_t oldRollColorLeft; uint32_t oldRollColorRight; uint32_t newRollColorLeft; uint32_t newRollColorRight; uint32_t tmpRollColorLeft; uint32_t tmpRollColorRight; uint32_t startTime; uint32_t currentTime; uint32_t rollInterval; bool rolloverFinished; numSteps = numRollsTotal * NUM_DIGITS_PER_LIXIE; flipStep = numRollsToFlip * NUM_DIGITS_PER_LIXIE; if (whichDirection == LSD_TO_MSD) { firstLixie = 0; lastLixie = NUM_LIXIES - 1; deltaLixie = 1; } else { firstLixie = NUM_LIXIES - 1; lastLixie = 0; deltaLixie = -1; } terminatingLixie = lastLixie + deltaLixie; if (whichRollColors == ROLL_WITH_SPECIAL_COLORS) { // Use special colors for the roll effect oldRollColorLeft = rollColorLeft; oldRollColorRight = rollColorRight; newRollColorLeft = rollColorLeft; newRollColorRight = rollColorRight; } else { // Use the original colors for the roll effect oldRollColorLeft = oldColorLeft; oldRollColorRight = oldColorRight; newRollColorLeft = newColorLeft; newRollColorRight = newColorRight; } for (int iLix = 0; iLix < NUM_LIXIES; iLix++) { LixieData[iLix].rollCount = 0; LixieData[iLix].rollStatus = ROLL_WAITING; } rolloverFinished = false; startTime = millis(); rollInterval = ROLL_INTERVAL * MULTIPLIER; LixieData[firstLixie].rollStatus = ROLL_ROLLING; while (rolloverFinished == false) { currentTime = millis(); if ( (currentTime - startTime) > rollInterval ) { startTime = currentTime; iLixie = firstLixie; while (iLixie != terminatingLixie) { if (LixieData[iLixie].rollStatus == ROLL_ROLLING) { // Turn old digit off Neos.setPixel( (DigitData[ LixieData[iLixie].oldValue ].leftNeo + LixieData[iLixie].firstNeo), BLACK); Neos.setPixel( (DigitData[ LixieData[iLixie].oldValue ].rightNeo + LixieData[iLixie].firstNeo), BLACK); // Set next Lixie into rolling mode if it's time if ( (LixieData[iLixie].rollCount == flipStep) && (iLixie != lastLixie) ) { LixieData[ (iLixie + deltaLixie) ].rollStatus = ROLL_ROLLING; } // Stop current Lixie rolling if its time if ( LixieData[iLixie].rollCount == numSteps) { LixieData[iLixie].rollStatus = ROLL_DONE; // Turn new digit on (unless we want to leave it off) if ( (LixieData[iLixie].newValue >= 0) && (LixieData[iLixie].newValue < NUM_DIGITS_PER_LIXIE) ) { Neos.setPixel( (DigitData[ LixieData[iLixie].newValue ].leftNeo + LixieData[iLixie].firstNeo), newColorLeft); Neos.setPixel( (DigitData[ LixieData[iLixie].newValue ].rightNeo + LixieData[iLixie].firstNeo), newColorRight); } // Update pointer LixieData[iLixie].oldValue = LixieData[iLixie].newValue; } // If we're still rolling, then roll baby, roll if (LixieData[iLixie].rollStatus == ROLL_ROLLING) { if (whichRollValues == ROLL_WITH_RANDOM_VALUES) { // Generate new random digit and save in old digit do { tmpValue = random(0, NUM_DIGITS_PER_LIXIE); } while ( (tmpValue == LixieData[iLixie].oldValue) || (tmpValue == LixieData[iLixie].newValue) ); } else { // Generate new 0 to 9 or 9 to 0 count value and save in old digit if (whichDirection == LSD_TO_MSD) { // Roll from 0 to 9 tmpValue = LixieData[iLixie].rollCount % NUM_DIGITS_PER_LIXIE; } else { // Roll from 9 to 0 tmpValue = (numSteps - LixieData[iLixie].rollCount - 1) % NUM_DIGITS_PER_LIXIE; } } LixieData[iLixie].oldValue = tmpValue; // Generate faded value of roll color // tmpRollColorLeft = newRollColorLeft; // tmpRollColorRight = newRollColorRight; tmpRollColorLeft = FadeColor(oldRollColorLeft, newRollColorLeft, numSteps, LixieData[iLixie].rollCount); tmpRollColorRight = FadeColor(oldRollColorRight, newRollColorRight, numSteps, LixieData[iLixie].rollCount); // Turn new digit on Neos.setPixel( (DigitData[ LixieData[iLixie].oldValue ].leftNeo + LixieData[iLixie].firstNeo), tmpRollColorLeft); Neos.setPixel( (DigitData[ LixieData[iLixie].oldValue ].rightNeo + LixieData[iLixie].firstNeo), tmpRollColorRight); // Increment roll count for this Lixie LixieData[iLixie].rollCount = LixieData[iLixie].rollCount + 1; } } if ( (iLixie == lastLixie) && (LixieData[iLixie].rollStatus == ROLL_DONE) ) { rolloverFinished = true; } iLixie = iLixie + deltaLixie; } Neos.show(); } } } // GENERAL-PURPOSE FUNCTIONS ============================================================================== void LoadRandomValues () { for (int iLix = 0; iLix < NUM_LIXIES; iLix++) { // Generate a new digit that's different to the old digit do { LixieData[iLix].newValue = random(0, NUM_DIGITS_PER_LIXIE); } while (LixieData[iLix].newValue == LixieData[iLix].oldValue); } } void LoadNullValues () { for (int iLix = 0; iLix < NUM_LIXIES; iLix++) { LixieData[iLix].newValue = -1; } } void CopyNewValuesToOldValues () { for (int iLix = 0; iLix < NUM_LIXIES; iLix++) { LixieData[iLix].oldValue = LixieData[iLix].newValue; } } void WaitTillNextCycle () { // Wait till it's time to load the next digit delay( CYCLE_TIME * MULTIPLIER ); } // LOW-LEVEL UTILITY FUNCTIONS ============================================================================ // Returns the Red component of a 32-bit color uint32_t GetRed (uint32_t tmpColor) { return (tmpColor >> 16) & 0xFF; } // Returns the Green component of a 32-bit color uint32_t GetGreen (uint32_t tmpColor) { return (tmpColor >> 8) & 0xFF; } // Returns the Blue component of a 32-bit color uint32_t GetBlue (uint32_t tmpColor) { return tmpColor & 0xFF; } // Returns a 32-bit color built out of reg, green, and blue elements uint32_t BuildColor (uint32_t red, uint32_t green, uint32_t blue) { return ((red << 16) | (green << 8) | blue) & 0xFFFFFF; } // Returns a 32-bit color generated as a fade between a start color and an end color uint32_t FadeColor (uint32_t startColor, uint32_t endColor, uint32_t numSteps, uint32_t currentStep) { uint32_t tmpRed; uint32_t tmpGreen; uint32_t tmpBlue; tmpRed = ( ( GetRed(startColor) * (numSteps - currentStep) ) + ( GetRed(endColor) * currentStep) ) / numSteps; tmpGreen = ( (GetGreen(startColor) * (numSteps - currentStep) ) + (GetGreen(endColor) * currentStep) ) / numSteps; tmpBlue = ( ( GetBlue(startColor) * (numSteps - currentStep) ) + ( GetBlue(endColor) * currentStep) ) / numSteps; return BuildColor(tmpRed, tmpGreen, tmpBlue); } /* * Copyright (c) 2019 Clive "Max" Maxfield * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHOR(S) OR COPYRIGHT HOLDER(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */