Timer Code

Now that you system is assembled, it's time to upload the timer code.

First, you'll need to download the TaskScheduler library, which is used by the Arduino sketch to do multiple things at once (such as run the timer while listening for button presses). Go here and download the zip file. Uncompress and move the TaskScheduler folder in your libraries folder
https://github.com/arkhipenko/TaskScheduler

Then, download the file below, upzip it, and place the directory in your Arduino sketches folder. Open it into Arduino IDE, plug in your Arduino to your computer, and then upload the code to the Arduino.

Download: file
//Ninja Timer
//Course timer for Ultimate Ninja Athlete Association course
//by John Park
//for Adafruit Industries
//Timer logic by Tod Kurt
//
////MIT License
////////////////////////////////////////////////////////////////////////////////
/*
Hardware:
-Arduino M0 Pro
-NeoPixel 60 strips, x5 meters
-2x quad Logic Level Shifters with 1k resistors
-Relay Feather Wing
-12V Car Horn
-Arcade buttons x6
-Patch Shield and CAT5 connectors/cables for buttons

Functions:
-Power on, goes to 00:00.0 in minutes, seconds, tenths
-Press start button, the digits blink four times
-This includes three short and one long beep from the horn
-Timer starts counting up
-If course stop button is pressed, stop the time and hold, blast the horn
-If desk stop button is pressed, stop the time and hold, blast horn
-If desk pause is pressed, stop the time but allow resume on second press
-If desk reset is pressed, reset to 00:00.0
*/
////////////////////////////////////////////////////////////////////////////////

#include <Adafruit_NeoPixel.h>
#include <TaskScheduler.h>


#define NUMPIXELS 57 // # of LEDs in a strip (some are actually 56, some 57
// due to dots), each strip is a digit 
#define DATAPIN0 4 //digit 0 NeoPixel 60 strip far RIGHT
#define DATAPIN1 5 //digit 1 plus lower colon dot
#define DATAPIN2 6 //digit 2 plus upper colon dot
#define DATAPIN3 7 //digit 3 plus decimal dot
#define DATAPIN4 8 //digit 4 far LEFT

Adafruit_NeoPixel strip[] = { //here is the variable for the multiple strips
  Adafruit_NeoPixel(NUMPIXELS, DATAPIN0, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUMPIXELS, DATAPIN1, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUMPIXELS, DATAPIN2, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUMPIXELS, DATAPIN3, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUMPIXELS, DATAPIN4, NEO_GRB + NEO_KHZ800)
};
const int bright = 200; //brightness for all pixels 0-255 range, 32 being dim

#define HORNPIN 10 //car horn: pin 10 on Arduino runs to Power Relay Feather 
//pin 10 (jumpered underneath relay board)

#define CSTARTPIN 11    //course start button pin using internal pullup resistor
//connected via RJ-45 jack J1, conductor 2 (GND conductor 1)
#define CSTOPPIN  12    //course stop button pin  "
//jack J2, conductor 2 (GND conductor 1)

#define DRESETPIN A0    //desk RESET button pin   
//jack J3, conductor 1 (GND conductor 8)
#define DSTARTPIN A1    //desk START button pin   
//jack J3, conductor 2 (GND conductor 8)
#define DPAUSEPIN A2    //desk PAUSE button pin   
//jack J3, conductor 3 (GND conductor 8)
#define DSTOPPIN  A3    //desk STOP button pin    
//jack J3, conductor 4 (GND conductor 8)



//Button states for reading the pushbuttons, since pullup: off = HIGH, on = LOW
int cStartState = 1;
int cStopState =  1;
int dStartState = 1;
int dStopState =  1;
int dPauseState = 1;
int dResetState = 1;
//variables for debouncing
int cStartLastState = 1;
int cStopLastState =  1;
int dStartLastState = 1;
int dStopLastState =  1;
int dPauseLastState = 1;
int dResetLastState = 1;

int runningMode=0; //counter to tell overall state since reset
//0 means it is a clean start, 1 means it's already running or ran
int pauseMode=0;//counter for using pause to stop and start timer
int stoppedMode=0;
int go = 0; //start counter once blinking/beeping is done
int dispColor = 3; //color to call out for display, feeds other voids


volatile bool isNinjaCounting = false;  // true when start button pressed
volatile uint32_t ninjaMillisTime = 0;  // the actual counter value

// how often the inputs are readInputs
const int inputsMillis = 10;
// how often the counter is updated
const int counterMillis = 10;
// how often the display is updated
const int displayMillis = 100;

// task prototypes
void readInputs();
void updateCounter();
void updateDisplay();
void debugDisplay();
//void startSequence();
//void stopSequence();

Scheduler runner;

// change these repeat times to appropriate ones for real application
Task t1(inputsMillis, TASK_FOREVER, &readInputs);
Task t2(counterMillis, TASK_FOREVER, &updateCounter);
Task t3(displayMillis, TASK_FOREVER, &updateDisplay);
Task t4(1000, TASK_FOREVER, &debugDisplay);
//Task t5();
//Task t6();

////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
void setup() {
  delay(2000);
  //while(!Serial); //for Leonardo & similar

  //initialize serial for debugging
  Serial.begin(115200);
  Serial.println("NinjaTimer TEST");
  Serial.println("'s' to start timer");
  Serial.println("'x' to stop timer");
  Serial.println("'p' to pause timer");
  Serial.println("'r' to restart timer");

  runner.init();
  runner.addTask(t1);
  runner.addTask(t2);
  runner.addTask(t3);
  runner.addTask(t4);
  t1.enable();
  // t2.enable();
  t2.disable();
  t3.enable();
  t4.enable();

  //NeoPixel array setup
  for(int s=0;s<5;s++){
    strip[s].begin(); // Initialize pins for output
    strip[s].setBrightness(bright); //full brightness 255
    strip[s].show();  // Turn all LEDs off 
    delay(200);
  }
  //flash dashes
    for(int t=0;t<5;t++){
       digitWrite(t,8,0);//blank
       strip[t].show();
       segLight(t, 7, dispColor);
       strip[t].show();
    }

  delay(1000);

  //Relay pin setup
  pinMode(HORNPIN, OUTPUT);

  //Button pin setup
  pinMode(CSTARTPIN, INPUT_PULLUP);
  pinMode(CSTOPPIN, INPUT_PULLUP);
  pinMode(DSTARTPIN, INPUT_PULLUP);
  pinMode(DSTOPPIN, INPUT_PULLUP);
  pinMode(DPAUSEPIN, INPUT_PULLUP);
  pinMode(DRESETPIN, INPUT_PULLUP);

}
//END void setup()
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
void loop() {
  runner.execute();
}
//END void loop()
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// read all the button inputs, change state of program appropriately
// task called periodically by TaskScheduler
void readInputs() {

  //check the state of the buttons
  cStartState=  digitalRead(CSTARTPIN);
  cStopState =  digitalRead(CSTOPPIN);
  dStartState = digitalRead(DSTARTPIN);
  dStopState =  digitalRead(DSTOPPIN);
  dPauseState = digitalRead(DPAUSEPIN);
  dResetState = digitalRead(DRESETPIN); 

  
  //Course Start
  //
  if(cStartState!=cStartLastState){ //debounce  -  state has changed
    if (cStartState==LOW) { //the course start button is pushed
      if (runningMode==0){//normal start
        dispColor=2;    //color is green
        
        //sound the horn four times
        delay(100);//slight delay after pressing
        for(int h = 0; h < 3; h++){  //three horn blasts
          //flash the display and beep the horn
          for(int t=0;t<5;t++){
            digitWrite(t,8,0);//blank
            strip[t].show();
           }
           delay(100);

          for(int t=0;t<5;t++){
             digitWrite(t,0,dispColor);//0s
            strip[t].show();
          }
            digitalWrite(HORNPIN, HIGH);
            delay(250);

            for(int t=0;t<5;t++){
              digitWrite(t,8,0);//blank
              strip[t].show();
            }
          digitalWrite(HORNPIN,LOW);
          delay(750);
        }
      
      startCounter();

      digitalWrite(HORNPIN, HIGH);
      delay(500);
      digitalWrite(HORNPIN,LOW);

      runningMode = 1;//indicates it is running or has run
      }

      else if(runningMode==1){
        //start button disabled
        Serial.println("already started");
      }
    }
  cStartLastState = cStartState; //for debounce
  }

  //Course Stop
  //
  if(cStopState!=cStopLastState){
   if (runningMode==1){ //only stop if started
    if (cStopState==LOW) { //the course stop button is pushed
      dispColor=1; //red
      stopCounter();

      digitalWrite(HORNPIN, HIGH);
      delay(1500);
      digitalWrite(HORNPIN,LOW);

      stoppedMode=1;
      }
    cStopLastState=cStopState;
    }
  }

  //Desk control box Start
  //
    if(dStartState != dStartLastState){
      if (dStartState==LOW) { //the desk start button is pushed
        if (runningMode==0){//normal start
          dispColor=2;    //color is green

          //sound the horn four times
          delay(100);
          for(int h = 0; h < 3; h++){  
            //flash the display and beep the horn
            for(int t=0;t<5;t++){
              digitWrite(t,8,0);//blank
              strip[t].show();
             }
             delay(100);

            for(int t=0;t<5;t++){
               digitWrite(t,0,dispColor);//0s
              strip[t].show();
            }
              digitalWrite(HORNPIN, HIGH);
              delay(250);

              for(int t=0;t<5;t++){
                digitWrite(t,8,0);//blank
                strip[t].show();
              }
            digitalWrite(HORNPIN,LOW);
            delay(750);
          }

        startCounter();

        digitalWrite(HORNPIN, HIGH);
        delay(500);
        digitalWrite(HORNPIN,LOW);

        runningMode = 1;//indicates it is running or has run
        }
        else if(runningMode==1){
          //start button disabled
          Serial.println("nope start");
        }
      }
    dStartLastState=dStartState;
    }

  //Desk Stop
  //
  if(dStopState!=dStopLastState){
    if (runningMode==1){//only can stop if started
      if (dStopState==LOW) { //the desk stop button is pushed
        dispColor=1; //red
        stopCounter();

        digitalWrite(HORNPIN, HIGH);
        delay(1500);
        digitalWrite(HORNPIN,LOW);

        stoppedMode=1;
      }
    }
  dStopLastState=dStopState;
  }

  //Pause
  //
  if(dPauseState!=dPauseLastState){ //for debouncing   
    if(runningMode==1&&stoppedMode==0){//only makes sense to pause if running
      if (dPauseState==LOW) { //the pause button is pushed
        if (pauseMode==0) { //it was previously running
          dispColor=5; //yellow
          pauseCounter(); 
          pauseMode=1;
          Serial.println("paused");
        }
        else if (pauseMode==1){//it was previously pause, start up again
          dispColor=2; //green
          startCounter();
          pauseMode=0;
          Serial.println("restarted");
        }
      }
    }
  dPauseLastState=dPauseState; //for debouncing      
  }

  //Reset
  //
    if(dResetState!=dResetLastState){ //for debouncing   
      if (dResetState==LOW) { //the reset button is pushed
        dispColor=3;//blue
        //flash dashes
      for(int t=0;t<5;t++){
         digitWrite(t,8,0);//blank
         strip[t].show();
         segLight(t, 7, 4);//white dashes
         strip[t].show();
         delay(170);
      }
      runningMode=0;
      pauseMode=0;
      stoppedMode=0;
      resetCounter();
      }  
    dResetLastState=dResetState; //for debouncing   
    }

    if( Serial.available() > 0 ) {
        int ch = Serial.read();
        if( ch == 's' ) {
            startCounter();
        }
        else if( ch == 'x' ) {
            stopCounter();
        }
        else if( ch == 'p' ) {
            pauseCounter();
        }
        else if( ch == 'r' ) {
            resetCounter();
        }
    }

}
//END void readInputs()
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// task called periodically by TaskScheduler
void updateCounter() {
    if( isNinjaCounting ) {
        // FIXME: this doesn't take into account overhead
        ninjaMillisTime += counterMillis;
    }
}
//END void updateCounter()
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// Actually update the display.
// May also do other effects like blinking
// task called periodically by TaskScheduler
void updateDisplay() {
    int tenths = (ninjaMillisTime / 100) % 10;
    int secs = (ninjaMillisTime / 1000) % 60;
    int mins = (ninjaMillisTime / 1000 / 60) % 60 ;

    int secsTens = secs/10; //get the tens place of seconds
    int secsOnes = secs%10; //get the ones place of seconds
    int minsTens = mins/10; //get the tens place of minutes
    int minsOnes = mins%10; //get the ones place of minutes

    //write to NeoPixel seven segments, colors:
    //0 = off
    //1 = red
    //2 = green
    //3 = blue
    //4 = white
    //5 =  yellow
    //
    digitWrite(0, tenths, dispColor); 
    strip[0].show();
    digitWrite(1, secsOnes, dispColor);
    strip[1].show();
    digitWrite(2, secsTens, dispColor);
    strip[2].show();
    digitWrite(3, minsOnes, dispColor);
    strip[3].show();
    digitWrite(4, minsTens, dispColor);
    strip[4].show();


    char dispbuf[10];
    sprintf(dispbuf, "%02d:%02d.%01d", mins, secs, tenths);
    Serial.print("DISPLAY: "); Serial.print(ninjaMillisTime);
    Serial.print(" mm:ss.t: "); Serial.println(dispbuf);


}
//END void updateDisplay()
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// A slower-updating display that logs to the serial port,
// useful for debugging
// task called periodically by TaskScheduler
void debugDisplay() {
    Serial.print("DEBUG: time:"); Serial.print(ninjaMillisTime);
    Serial.print(" isNinjaCounting:"); Serial.println(isNinjaCounting);
}
//END void debugDisplay()
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// starts the counter running
// via START button
void startCounter() {
    t2.enable();//this needs to be enabled here to deal with beep delay
    Serial.println("START");
    isNinjaCounting = true;
}
//END void startCounter()
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// stops the counter running
// via STOP button
// does NOT reset display
void stopCounter() {
    Serial.println("STOP");
    isNinjaCounting = false;
}
//END void stopCounter()
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// pauses counter
// via PAUSE button
void pauseCounter() {
    Serial.println("PAUSE");
    isNinjaCounting = false;
}
//END void pauseCounter()
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// reset counter to zero, stops counter
// via RESET button (or boot)
void resetCounter() {
    Serial.println("RESET");
    ninjaMillisTime = 0;
    stopCounter();
}
//END void resetCounter()
////////////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////////////
void digitWrite(int digit, int val, int col){

  //use this to light up a digit
  //'digit' is which one (right to left, 0 indexed)
  //'val' is the value to set on the digit
  //'col' is the color to use, R,G,B or W
  //example: 
  //        digitWrite(0, 4, 2); 
  //this would set the digit
  //on the far right to a "4" in green.

/*
// Letters are the standard naming, numbers are based upon the wiring sequence

          A 2     
     ----------
    |          |
    |          |
F 1 |          | B 3
    |          |
    |     G 7  |
     ----------
    |          |
    |          |
E 6 |          | C 4
    |          |
    |     D 5  |
     ----------    dp 8

*/
//these are the numeric character definitions, 
//if last argument is a 0, the segment is off
  if (val==0){ // "0"
    //segments A,B,C,D,E,F
    segLight(digit,1,col);
    segLight(digit,2,col);
    segLight(digit,3,col);
    segLight(digit,4,col);
    segLight(digit,5,col);
    segLight(digit,6,col);
    segLight(digit,7,0);
    segLight(digit,8,col);
  }
  if (val==1){ // "1"
    //segments A,B,C,D,E,F
    segLight(digit,1,0);
    segLight(digit,2,0);
    segLight(digit,3,col);
    segLight(digit,4,col);
    segLight(digit,5,0);
    segLight(digit,6,0);
    segLight(digit,7,0);
    segLight(digit,8,col);
  }
  if (val==2){ // "2"
    //segments A,B,C,D,E,F
    segLight(digit,1,0);
    segLight(digit,2,col);
    segLight(digit,3,col);
    segLight(digit,4,0);
    segLight(digit,5,col);
    segLight(digit,6,col);
    segLight(digit,7,col);
    segLight(digit,8,col);
  }
  if (val==3){ // "3"
    //segments A,B,C,D,E,F
    segLight(digit,1,0);
    segLight(digit,2,col);
    segLight(digit,3,col);
    segLight(digit,4,col);
    segLight(digit,5,col);
    segLight(digit,6,0);
    segLight(digit,7,col);
    segLight(digit,8,col);
  }
  if (val==4){ // "4"
    //segments A,B,C,D,E,F
    segLight(digit,1,col);
    segLight(digit,2,0);
    segLight(digit,3,col);
    segLight(digit,4,col);
    segLight(digit,5,0);
    segLight(digit,6,0);
    segLight(digit,7,col);
    segLight(digit,8,col);
  }
  if (val==5){ // "5"
    //segments A,B,C,D,E,F
    segLight(digit,1,col);
    segLight(digit,2,col);
    segLight(digit,3,0);
    segLight(digit,4,col);
    segLight(digit,5,col);
    segLight(digit,6,0);
    segLight(digit,7,col);
    segLight(digit,8,col);
  }
  if (val==6){ // "6"
    //segments A,B,C,D,E,F
    segLight(digit,1,col);
    segLight(digit,2,col);
    segLight(digit,3,0);
    segLight(digit,4,col);
    segLight(digit,5,col);
    segLight(digit,6,col);
    segLight(digit,7,col);
    segLight(digit,8,col);
  }          
  if (val==7){ // "7"
    //segments A,B,C,D,E,F
    segLight(digit,1,0);
    segLight(digit,2,col);
    segLight(digit,3,col);
    segLight(digit,4,col);
    segLight(digit,5,0);
    segLight(digit,6,0);
    segLight(digit,7,0);
    segLight(digit,8,col);
  }
  if (val==8){ // "8"
    //segments A,B,C,D,E,F
    segLight(digit,1,col);
    segLight(digit,2,col);
    segLight(digit,3,col);
    segLight(digit,4,col);
    segLight(digit,5,col);
    segLight(digit,6,col);
    segLight(digit,7,col);
    segLight(digit,8,col);
  }
  if (val==9){ // "9"
    //segments A,B,C,D,E,F
    segLight(digit,1,col);
    segLight(digit,2,col);
    segLight(digit,3,col);
    segLight(digit,4,col);
    segLight(digit,5,col);
    segLight(digit,6,0);
    segLight(digit,7,col);
    segLight(digit,8,col);
  }    
}
//END void digitWrite(int digit, int val, int col)
////////////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////////////
void segLight(char digit, int seg, int col){ 

  //'digit' picks which neopixel strip
  //'seg' calls a segment
  //'col' is color
  /*
  //always blank the LEDs first
  for(int i = 0; i<56; i++){
    strip[0].setPixelColor(i, 0, 0, 0); 
  }
*/
  int color[3];

  //color sets
    if (col==0){ //off
      color[0]={0};
      color[1]={0};
      color[2]={0};
    }
    if (col==1){ //red
      color[0]={255};
      color[1]={0};
      color[2]={0};
    }
    if (col==2){ //green
      color[0]={0};
      color[1]={255};
      color[2]={0};
    }
    if (col==3){ //blue
      color[0]={0};
      color[1]={0};
      color[2]={255};
    }
    if (col==4){ //white -- careful with this one, 3x power consumption
      color[0]={255};
      color[1]={255};
      color[2]={255};
    }
     if (col==5){ //yellow
      color[0]={200};
      color[1]={120};
      color[2]={0};
    }

  

  //sets are 0-7, 
  //8-15, 16-23, 24-31, 32-39, 40-47, 48-55 

  //seg F
  if(seg==1){
    //light first 8
    for(int i=0; i<8; i++){
      strip[digit].setPixelColor(i,color[0],color[1],color[2]);
    }  
    //strip(digit).show();
  }
  //seg A
  if(seg==2){
      //light second 8
      for(int i=8; i<16; i++){
      strip[digit].setPixelColor(i,color[0],color[1],color[2]);
      } 
      //strip(digit).show();
  }
  //seg B
  if(seg==3){
      for(int i=16; i<24; i++){
      strip[digit].setPixelColor(i,color[0],color[1],color[2]);
      }   
      //strip(digit).show();
  }
  //seg C
  if(seg==4){
      for(int i=24; i<33; i++){
      strip[digit].setPixelColor(i,color[0],color[1],color[2]);
      }   
      //strip.show();
  }
  //seg D
  if(seg==5){
      for(int i=32; i<41; i++){
      strip[digit].setPixelColor(i,color[0],color[1],color[2]);
      }   
      //strip(digit).show();
  }
  //seg E
  if(seg==6){
      for(int i=40; i<49; i++){
      strip[digit].setPixelColor(i,color[0],color[1],color[2]);
      }   
      //strip(digit).show();
  }
  //seg G
  if(seg==7){
      for(int i=48; i<57; i++){
      strip[digit].setPixelColor(i,color[0],color[1],color[2]);
      }   
      //strip(digit).show();
  }
  //seg dp
  if(seg==8){
      for(int i=56; i<57; i++){
      strip[digit].setPixelColor(i,color[0],color[1],color[2]);
      }   
      //strip.show();
  }
}
//END void segLight(char digit, int seg, int col)
////////////////////////////////////////////////////////////////////////////////


Now, you can put your timer to the test! Unplug it from your computer, and then connect the box cables to your digit panels, car horn, and three button Cat 5e/Cat 6 cables.

Next, plug the AC power cord into an outlet. It will power on and display all "0"s in blue after a couple of seconds.

Press course start button or desk control start button to begin your run. The horn will sounds four times, and then start counting.

At the end of your run, dive for the course stop button. The horn will sound and your time will be displayed in red.

At any time, the desk controller box can be used to pause a run by pressing the yellow button, and restarting the run by pressing it a second time. This is useful if a dog, toddler, or knife wielding actual honest to goodness ninja shows up.

Finally, the blue button on the desk controller box can be pressed to reset the timer in anticipation of the next course run!

This guide was first published on Mar 15, 2017. It was last updated on Mar 15, 2017. This page (Timer Code) was last updated on Aug 21, 2019.