Counting and Timing a shock sensor

In case the above code works as hoped for and you get bored after running it through all it’s modes and paces, here’s a 1’st cut at some test code for an OLED compatible user interface (UI). It doesn’t store nor retrieve info from EEPROM nor control any timer functions. All it does is allow you to make choices using the select button on pin6 and slidey 2 axis pot on the analog inputs, A0 and A1. Push the select button for 1 sec and it’ll enter UI mode. Navigate among the choices (using the pot up/down,left/right) and change what you can. Upon exit (use select button again) you can “save” the setup (or not) and the new CoF and parameters will print to the serial monitor and display on the LCD.

Hopefully

But I wouldn’t hold my breath

#include <OLEDFourBit.h>

// initialize the library with the numbers of the interface pins
OLEDFourBit lcd(12, 11, 5, 4, 9, 8);

// set pin numbers
const byte SelectPin = 6;                // the select button pin for this test
const byte LEDPin = 13;                  // the number of the LED output pin
const byte Vert = 0;                     // the analog input for up or down button
const byte Horz = 1;                     // the analog input for left or right button

// initialize the constants
const unsigned long DB_delay = 300;      // set a debounce wait time of 300 msecs
const unsigned long MaxTime = 60001;     // the max time the timer can run for = 60 secs
const byte MaxHits = 24;                 // Maximum number if hits allowed
const int LoThrshld = 255;               // Low threshold for L/D button push
const int HiThrshld = 767;               // High threshold for R/U button push

char* MenuList[] ={"    Select CoF:     "," Select # shooters: ","   Select Delay:    ",
" Select # of hits:  "," Select stop time:  ","   Save options:    "}

char* CoFlist[] = {"Standard 1","QuickDraw1","    21    "," Custom 1 ",
" Custom 2 "," Custom 3 ","  A vs B  "}

char* Savelist[] = {"     Exit    ","Exit and Use ","Exit and Save"}


// initialize global variables
int CoF = 0;                             // initialize the mode to be standard
byte NumShooters = 1;                    // initialize the number of shooters to 1
byte RanDly = 0;                         // set random to disabled
byte StopHits = MaxHits;                 // Number if hits allowed before stopping
unsigned long StopTime = MaxTime;        // Time that the timer will stop at
byte UI_mode = 0;                        // set the user interface to off
byte UI_init = 0;                        // set the user interface to not initialized

int tmpCoF = CoF;                        // temp version of timer control
byte tmpRanDly = RanDly;                 // temp version of timer control
byte tmpNumShooters = NumShooters;       // temp version of timer control
byte tmpStopHits = StopHits;             // temp version of timer control
unsigned long tmpStopTime = StopTime;    // temp version of timer control
unsigned long LastTime = 0;              // last time LCD was updated
 
int 4Way[] = {0, 0, 0, 0};
int indxMenu = 0;                        // preset to land on CoF menu
int indxSave = 1;                        // preset to exit and use
int numCoF = 7;                          // # of CoFs, 7 for now

char Line3[21];


void setup()
{
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("   User Interface   ");
  lcd.setCursor(0, 1);
  lcd.print("    Initializing    ");
  delay(2000);
  
  // initialize output pins
  pinMode(LEDPin, OUTPUT);
  digitalWrite(LEDPin, LOW);
   
  // initialize the input pins with internal pullups
  pinMode(SelectPin, INPUT);
  digitalWrite(SelectPin, HIGH);
  
  // opens serial port, sets data rate to 9600 bps
  Serial.begin(9600);
  
  lcd.setCursor(0, 1);
  lcd.print("      Ready       ");
  
  // Send this data to the serial monitor for debug purposes only
  Serial.println("Initial values are :");
  Serial.print("UI_mode is : ");
  Serial.println(UI_mode);
  Serial.print("CoF is : ");
  Serial.println(CoFlist[indxCoF]);
  Serial.print("# Shooters is : ");
  Serial.println(NumShooters);
  Serial.print("Random/Fixed delay is : ");
  Serial.println(RanDly);
  Serial.print("Stop hits # is : ");
  Serial.println(StopHits);
  Serial.print("Stop time is : ");
  Serial.println(StopTime/1000);
  
  delay(2000);
}


void loop()
{
  // read the Select button and see if is been pushed 1 sec
  if (digitalRead(SelectPin) == LOW)
  {
    delay(1000);
    if (digitalRead(SelectPin) == LOW)
    {
      UI_mode = 1;
      Serial.print("UI_mode is : ");
      Serial.println(UI_mode);
    }
  }
  // now see if the UI is enabled
  if (UI_mode)
  {
    // UI enabled, read the buttons and set 4Way(LRUD)
    GetButtons()
    
    if(UI_init == 0)
    {
      // first pass since enabled so initialize the indices etc etc
      UI_init = 1;
      indxMenu = 0;                 // land on CoF menu
      indxSave = 1;                 // be on exit n use
      tmpCoF = CoF;                 // copy control to UI temp
      tmpRanDly = RanDly;           // copy control to UI temp
      tmpNumShooters = NumShooters; // copy control to UI temp
      tmpStopHits = StopHits;       // copy control to UI temp
      tmpStopTime = StopTime;       // copy control to UI temp
    }
    
    // set menu to determine which menu item is displayed
    if(4Way[1])     // right button pressed
    {
      indxMenu++;
    }
    if(4Way[0])    // left button pressed
    {
      indxMenu--;
    }
    if(indxMenu<0)  // wrap around
    {
      indxMenu=5;   // presently 6 menu items 0-5
    }
    if(indxMenu>5)  // wrap around
    {
      indxMenu=0;
    }
    
    switch (indxMenu)  // display menu item set via pointer
    {
      case 0:  // CoF menu
        if(4Way[2])
        {
          tmpCoF++;
        }
        if(4Way[3])
        {
          tmpCoF--;
        }
        if(tmpCoF<0)
        {
          tmpCoF=numCoF;
        }
        if(tmpCoF>numCoF)
        {
          tmpCoF=0;
        Line3 = CoFlist[tmpCoF];
        break;
      case 1: // number of shooters menu
        if(4Way[2])
        {
          tmpNumShooters++;
        }
        if(4Way[3])
        {
          tmpNumShooters++;
        }
        if(tmpNumShooters>2)
        {
          tmpNumShooters=1;
        }
        char buf[21];
        PString(buf, sizeof(buf), tmpNumShooters);  // convert to char
        Line3 = buf;
        break;
      case 2: // countdown delay menu
        if(4Way[2])
        {
          tmpRanDly++;
        }
        if(4Way[3])
        {
          tmpRanDly++;
        }
        if(tmpRanDly>1)
        {
          tmpRanDly=0;
        }
        char buf[21];
        PString(buf, sizeof(buf), tmpRanDly);  // convert to char
        Line3 = buf;
        break;
      case 3: // Stop hits menu
        if(4Way[2])
        {
          tmpStopHits++;
        }
        if(4Way[3])
        {
          tmpStopHits--;
        }
        if(tmpStopHits<1)
        {
          tmpStopHits=MaxHits;
        }
        if(tmpStopHits>MaxHits)
        {
          tmpStopHits=1;
        }
        char buf[21];
        PString(buf, sizeof(buf), tmpStopHits);  // convert to char
        Line3 = buf;
        break;
      case 4: // Stop time menu
        if(4Way[2])
        {
          tmpStopTime += 1000;      // add 1000 msec to stoptime if up is pushed
        }
        if(4Way[3])
        {
          tmpStopTime -= 1000;      // subtract 1000 msec to stoptime if down is pushed
        }
        if(tmpStopTime<1000)        // min stop time is 1 sec
        {
          tmpStopTime=MaxTime;      // wrap around to max stop time
        }
        if(tmpStopTime>MaxTime)
        {
          tmpStopTime = 1001;       // wrap around to min stop time, 1 sec
        }
        char buf[21];
        PString(buf, sizeof(buf), tmpStopTime/1000);  // convert to char
        Line3 = buf;
        break;
      case 5: // Save options menu
        if(4Way[2])
        {
          indxSave++;
        }
        if(4Way[3])
        {
          indxSave--;
        }
        if(indxSave<0)
        {
          indxSave=2;
        }
        if(indxSave>2)
        {
          indxSave=0;
        Line3 = Savelist[indxSave];
        break;
      default: // error
        Line3 = "       Error        ";
    }
    // update LCD if something has possibly changed
    if(4Way[])                          // if any button pushed
    {
      lcd.clear();
      lcd.setCursor(0,0);               // go to line 1
      lcd.print(MenuList[indxMenu]);    // Show the Menu
      lcd.setCursor(0,2);               // go to line 3
      lcd.print(Line3);                 // show what Menu item
    }
    
    // now check if select has been pushed again to exit UI
    if (digitalRead(SelectPin) == LOW)
    {
      // reset UI controls
      UI_mode = 0;
      UI_init = 0;
      // print to monitor exitting UI
      Serial.println("    ");
      Serial.print("UI_mode is : ");
      Serial.println(UI_mode);
      Serial.print("Exit option was : ");
      Serial.println(Savelist[indxSave]);
      Serial.print("CoF is : ");
      Serial.println(CoFlist[indxCoF]);
      Serial.print("# Shooters is : ");
      Serial.println(NumShooters);
      Serial.print("Random/Fixed delay is : ");
      Serial.println(RanDly);
      Serial.print("Stop hits # is : ");
      Serial.println(StopHits);
      Serial.print("Stop time is : ");
      Serial.println(StopTime/1000);

      // copy new settings to timer controls is selected
      if(indxSave)   // exit and use
      {
        CoF = tmpCoF;
        NumShooters = tmpNumShooters;
        RanDly = tmpRanDly;
        StopHits = tmpStopHits;
        StopTime = tmpStopTime;
      }
    }
  }
  else
  {
    // UI is not enabled
    if(millis() - LastTime > 2000)
    {
      LastTime - millis();
      // show results of UI interaction on LCD
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print(CoFlist[CoF]);
      lcd.setCursor(0,1);
      lcd.print("Stop hits = ");
      lcd.print(StopHits);
      lcd.setCursor(0,2);
      lcd.print("Stop time is : ");
      lcd.print(StopTime/1000);
      lcd.setCursor(0,3);
      lcd.print("1 or 2 : ");
      lcd.print(NumShooters);
      lcd.print(" Delay: ");
      lcd.print(RanDly);
    }
  }
}
// end of loop


void GetButtons()
{
  // this version uses the analog inputs
  4Way[] = {0, 0, 0, 0};
  digitalWrite(LEDPin, LOW);
  if(analogRead(Horz) < LoThrshld)
  {
    4Way[0] = 1;
    Serial.println("Left button pushed"); 
  }
  if(analogRead(Horz) > HiThrshld)
  {
    4Way[1] = 1;
    Serial.println("Right button pushed");
  }
  if(analogRead(Vert) > HiThrshld)
  {
    4Way[2] = 1;
    Serial.println("Up button pushed");
  }
  if(analogRead(Vert) < LoThrshld)
  {
    4Way[3] = 1;
    Serial.println("Down button pushed");
  }
  if(4Way[])   // if button pushed debounce it by waiting
  {
    delay(DB_delay);
    digitalWrite(LEDPin, HIGH);  // turn on LED
  }
}

http://www.youtube.com/watch?v=SttyREV9 … e=youtu.be

Mee_n_Mac:
I found the problem with the blank screens when the number of hits would just exactly fill the last screen. So that’s fixed and I added the aforemention ISR fix for the too many hits. Right now I can only GUESS that the discrepancy between the last hit shown in the running display and the last one shown in the review display is due to a hit coming in after the max number has been reached but while the LCD is getting updated for the now-next-to-last hit. What the code does to keep the last hit running display shown after the timer “stops” (for any reason) is to hold off actually stopping the timer even though one of the stop conditions has been met. The code then calls for an update to the LCD and then actually stops the timer and shuts off the ISRs (which count the hits). So if another hit (#25) or two, come in while the LCD is being updated for #24, then you’ll have that discrepancy … with the prior code. I believe the fix for too many hits will also fix this.

What I’ve yet to figure out is why the running display showed 0 hits with blanks times (that last part is OK) on 2 occasions in your last vid. It should have waited for the 1’st A hit to come in before switching from the “Standby” display to the running display. Therefore in 1 shooter mode the very 1’st running display should always show a hit count of 1 and it’s time and a blank for the split time. And on the other 2 trials it did just that, just as it should. It’s almost if the B hit line caused the LCD update even though there was no A hit … but the B ISR is shut off when in 1 shooter mode ???

In any case the code below should fix 3 of the 4 issues noted and the last issue just looks odd, there’s no wrong data ever displayed or otherwise recorded (the review display looked fine).

#include <OLEDFourBit.h>

// initialize the library with the numbers of the interface pins
// with the OLED library this also seems to do the init and begin calls
OLEDFourBit lcd(12, 11, 5, 4, 9, 8);

// set pin numbers
const int StartPin = 7; // the number of the Start pushbutton pin
const int TargetAPin = 2; // the number of the A targets input pin
const int TargetBPin = 3; // the number of the B targets input pin
const int BuzzerPin = 10; // the number of the buzzer output pin
const int LEDPin = 13; // the number of the LED output pin
const int ScrollPin = 6; // the number of the scroll button input pin

// initialize the constants
const unsigned long MaxTime = 60001; // the max time the timer can run for = 60 secs
const unsigned long WaitTime = 1000; // wait time btw start and timer running = 1 sec plus debounce
const unsigned long DB_delay = 1000; // set a debounce wait time of 1000 msecs
const unsigned long BuzzTime5 = 500; // set the on time for the buzzer, 500 msecs
const unsigned long BuzzTime2 = 200; // set the on time for the buzzer, 200 msecs
const unsigned long BuzzTime10 = 1000; // set the on time for the buzzer, 1000 msecs
const unsigned int FreqHi = 2000; // High frequency of buzzer tone
const unsigned int FreqLo = 1000; // Low frequency of buzzer tone
const byte MaxHits = 24; // Maximum number of hits allowed per shooter

// initialize global variables
volatile byte TimerState = 0; // variable for state of timer, running or not running
volatile byte AupDtFlag = 0; // variable indication an A hit has occurred
volatile byte BupDtFlag = 0; // variable indication a B hit has occurred
volatile int secs = 0; // whole seconds of time for display
volatile int frac = 0; // fractional part of time for display
unsigned long StartTime = 0; // variable to hold the start time
unsigned long BuzzTime = 500; // variable to hold the buzzer on time
unsigned int Freq = 2000; // variable for high or low buzzer tone
volatile byte A_count = 0; // variable to hold the number of A hits
volatile byte B_count = 0; // variable to hold the number of B hits
volatile unsigned long A_Times[MaxHits]; // array to hold up to MaxHits hit times for target A
volatile unsigned long B_Times[MaxHits]; // array to hold up to MaxHits hit times for target B
byte DisplayIndex = 0; // variable for controlling what’s displayed
byte diMax = 99; // Display index maximum

byte StopHits = MaxHits; // Number if hits allowed before stopping
unsigned long StopTime = MaxTime; // Time that the timer will stop at
byte CoF = 0; // initialize the mode to be standard
//byte CoF = 1; // initialize the mode to be quick draw
//byte CoF = 2; // initialize the mode to be 21
byte NumShooters = 1; // initialize the number of shooters to 1
//byte NumShooters = 2; // initialize the number of shooters to 2
byte RanDly = 0; // random delay disabled
//byte RanDly = 1; // random delay enabled

void setup()
{
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" Shot Timer 1 “);
lcd.setCursor(0, 1);
lcd.print(” Initializing ");
delay(3000);

// initialize output pins
pinMode(BuzzerPin, OUTPUT);
pinMode(LEDPin, OUTPUT);
digitalWrite(BuzzerPin, LOW);
digitalWrite(LEDPin, LOW);

// initialize the input pins with internal pullups
pinMode(StartPin, INPUT);
pinMode(TargetAPin, INPUT);
pinMode(TargetBPin, INPUT);
digitalWrite(StartPin, HIGH);
digitalWrite(TargetAPin, HIGH);
digitalWrite(TargetBPin, HIGH);

// for now use pin 6 as button input
pinMode(ScrollPin, INPUT);
digitalWrite(ScrollPin, HIGH);

// opens serial port, sets data rate to 9600 bps
Serial.begin(9600);

// setup ext pins as interrupts
//attachInterrupt(0, ISR_A, FALLING);
//attachInterrupt(1, ISR_B, FALLING);

lcd.setCursor(0, 1);
lcd.print(" Ready ");

ClearData();
}

void loop()
{
if (TimerState) // is timer running
{
// timer is running

// send data to display if there have been any hits
if (AupDtFlag == 1 || BupDtFlag == 1)
{
  UpdateLCD();
}

// timer is running so now check for any stop conditions
if ((digitalRead(StartPin) == LOW) || ((millis() - StartTime) >= StopTime) || A_count >= StopHits || B_count >= StopHits)
{
  // update the LCD running display one last time
  UpdateLCD();
  
  // stop the timer
  StopTimer();
  tone(BuzzerPin, FreqLo, BuzzTime10);
  
  // just for the moment send times to PC for debug
  SendTimes();
  
  // delay enough to debounce stop button
  delay(DB_delay);
  
  // Setup the review-stopped state display based on number shooters and hits
  if(NumShooters == 1)
  {
    diMax = byte(A_count/8) - 1;   // DI goes 0 to number of displays - 1
    if (A_count % 8)                 // diMax indicates last screen of hits
    {
      diMax ++;                      // limit lines per number of hits
    }
    DisplayIndex = diMax;            // display will show list of hit times
  }
  else
  {
    diMax = byte(max(A_count,B_count)/4) - 1; // find how many whole screens
    if (max(A_count,B_count) % 4)
    {
      diMax++ ;              // add another screen if partial exists
    }
    DisplayIndex = diMax;    // display will show list of hit times
  }
}

}
else
{
// timer is stopped look for start button or review button pushes

// check for scroll button pushes, change display state as reqd
if (digitalRead(ScrollPin) == LOW)
{
  DisplayIndex++;             // increment DisplayIndex upon push
  if (DisplayIndex > diMax)
  {
    DisplayIndex = 0;         // wrap around of scrolling action
  }
  // update display
  UpdateLCD();
  delay(DB_delay);
}

// check to see if start button has been pushed
if (digitalRead(StartPin) == LOW)
{
  // start button pushed
  // Do the following debug code only to get system running
  // This will send message to PC to show button was pushed
  Serial.println("Timer is running");
  
  // turn on the LED to show timer is running
  digitalWrite(LEDPin, HIGH);
  
  // show the timer has been started
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("    Shot Timer v1   ");
  lcd.setCursor(0, 1);
  lcd.print("     is running     ");
  lcd.setCursor(0, 2);
  lcd.print("      Shooter:      ");
  lcd.setCursor(0, 3);
  lcd.print("    --STANDBY--     ");
  
  // this will change when config mode is added ?
  switch (CoF)
  {
    case 1:
      //this is for quick draw mode
      StopTime = 10001;
      StopHits = 6;
      break;
    case 2:
      //this is for 21 mode
      StopTime = 1501;
      StopHits = MaxHits;
      break;
    default: 
      // the default = standard timer mode
      StopTime = MaxTime;
      StopHits = MaxHits;
  }
  
  // delay the Wait Time from start button push
  randomSeed(millis());
  // delay is min delay plus random if enabled
  delay(WaitTime);               // 1 sec min delay
  if(RanDly)
  {
    delay(random(2000, 4000));   // total delay btw 3 and 5 secs
  }
  
  // turn on interrupts as needed for 1 or 2 shooters
  attachInterrupt(0, ISR_A, FALLING);
  if (NumShooters > 1)
  {
    attachInterrupt(1, ISR_B, FALLING);
  }
  
  // clear all the prior runs data
  ClearData();      
  
  // save the starting time of this run
  StartTime = millis();
  
  // set state of timer to running
  TimerState = 1;
  
  // now buzz the speaker for 0.5 secs
  tone(BuzzerPin, FreqHi, BuzzTime5);
}

}
}

void StopTimer()
{
// turn off interrupts
detachInterrupt(0);
detachInterrupt(1);
TimerState = 0;
// turn the LED off to show timer is stopped
digitalWrite(LEDPin, LOW);
}

void ClearData()
{
A_count = 0;
B_count = 0;
for (byte i = 0; i < MaxHits; i++)
{
A_Times[i] = 0;
B_Times[i] = 0;
}
}

void SendTimes()
// temp routine to send data to serial monitor
{
Serial.println(“Timer is stopped”);
Serial.println(“Here are the times for Shooter A”);
Serial.print(“Number of hits : “);
Serial.print(”\t”);
Serial.println(A_count);
Serial.println(“A Hit and split times are : “);
byte i = 0;
unsigned long Split = 0;
for (i = 0; i < A_count; i++)
{
Serial.print(i+1);
Serial.print(”\t”);
Serial.print(A_Times[i]);
Serial.print(“\t”);
if (i)
{
Split = A_Times[i] - A_Times[i-1];
Serial.print(Split);
}
Serial.println(“\t”);
}
Serial.println(“Here are the times for Shooter B”);
Serial.print(“Number of hits : “);
Serial.print(”\t”);
Serial.println(B_count);
Serial.println(“B Hit and split times are : “);
for (i = 0; i < B_count; i++)
{
Serial.print(i+1);
Serial.print(”\t”);
Serial.print(B_Times[i]);
Serial.print(“\t”);
if (i)
{
Split = B_Times[i] - B_Times[i-1];
Serial.print(Split);
}
Serial.println(“\t”);
}
}

void ISR_A()
{
if(TimerState)
{
if(A_count < StopHits)
{
// store the hit time
A_Times[A_count] = millis() - StartTime;
// increment the hit count and array index
A_count++;
// set the Hit flag so other stuff will update
AupDtFlag = 1;
}
}
}

void ISR_B()
{
if(TimerState)
{
if(B_count < StopHits)
{
// store the hit time
B_Times[B_count] = millis() - StartTime;
// increment the hit count and array index
B_count++;
// set the Hit flag so other stuff will update
BupDtFlag = 1;
}
}
}

void UpdateLCD()
{
AupDtFlag = 0;
BupDtFlag = 0;
// routine to format lines of data to be sent to LCD
// presently assume 4 lines of 20 characters
// formatting based number of shooters and timer state
if(TimerState)
{
// timer is running
// display number of hits so far
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" A: # hits = “);
if(A_count < 10)
{
lcd.print(’ ‘);
}
lcd.print(A_count);
// now display the time of last hit
lcd.setCursor(0, 1);
lcd.print("A Hit time = ");
if(A_count)
{
TimeConvert(A_Times[A_count-1]); // convert hit time to XX.xx format
}
lcd.setCursor(0, 2);
// Chose 1 or 2 shooter portion of display
if(NumShooters == 1)
{
// this is for single A shooter running display
// display the split time btw last hit and prior hit
unsigned long Split = 0;
if (A_count > 1)
{
Split = A_Times[A_count-1] - A_Times[A_count-2];
}
lcd.print("Split time = ");
TimeConvert(Split); // convert split time
lcd.setCursor(0, 3);
// compute A hit factor
//float HF = A_count/A_Times[A_count - 1];
//lcd.print("Hit factor = “);
//char buf[6];
//PString(buf, sizeof(buf), HF); // convert FP to char
//lcd.print(buf);
}
else
{
// this is for 2 shooter running display
lcd.print(” B: # hits = ");
if(B_count < 10)
{
lcd.print(’ ‘);
}
lcd.print(B_count);
lcd.setCursor(0, 3);
lcd.print("B Hit time = ");
if(B_count)
{
TimeConvert(B_Times[B_count-1]); // Now for B times
}
}
}
else
{
// timer is stopped
if(NumShooters == 1)
{
// this is single shooter review display
lcd.clear();
lcd.setCursor(0, 0);
byte k = 0; // k will hold LCD line number
byte j = DisplayIndex << 3; // index for first hit time displayed
byte jj = (A_count % 8); // jj holds number hits on last display
if (jj == 0)
{
jj = 8; // acount for 8 hits on last screeen
}
if (DisplayIndex == diMax) // diMax indicates last screen of hits
{
jj = j + min(jj,4); // display 1 line per hit up to 4 lines
}
else
{
jj = j + 4; // there are enough hits to fill 4 lines
}
for (byte i = j; i < jj; i++)
{
k = (i % 4);
lcd.setCursor(0,k);
lcd.print(‘A’);
if(i<9)
{
lcd.print(’ '); // add a space for single digit numbers
}
lcd.print(i+1); // send hit # to LCD
lcd.print(”: “);
TimeConvert(A_Times[i]); // convert hit time to XX.xx format
if(A_Times[i+4]) // if there’s a non-zero time
{
lcd.print(’ ‘); // print a space
if((i+4)<9)
{
lcd.print(’ '); // add a space for single digit numbers
}
lcd.print(i+5); // send hit # to LCD
lcd.print(”: “);
TimeConvert(A_Times[i+4]); // convert hit time to XX.xx format
}
}
}
else
{
// this is 2 shooter review display
lcd.clear();
lcd.setCursor(0, 0);
byte k = 0; // k will hold LCD line number
byte j = DisplayIndex << 2; // index for first hit time displayed
byte jj = max(A_count,B_count); // init jj to max hit count
jj = jj % 4; // jj holds number hits on last display
if (jj == 0)
{
jj = 4; // acount for 4 hits on last screeen
}
if (DisplayIndex == diMax) // diMax indicates last screen of hits
{
jj = j + min(jj,4); // display 1 line per hit up to 4 lines
}
else
{
jj = j + 4; // there are enough hits to fill 4 lines
}
for (byte i = j; i < jj; i++)
{
k = (i % 4);
lcd.setCursor(0,k);
if(i<9)
{
lcd.print(’ '); // for hits 1-9 put in a leading space
}
lcd.print(i+1); // print hit number
lcd.print(” A: “);
TimeConvert(A_Times[i]); // convert hit time to XX.xx format
if(B_Times[i]);
{
lcd.print(” B: ");
TimeConvert(B_Times[i]); // convert hit time to XX.xx format
}
}
}
}
}

void TimeConvert(unsigned long input)
{
// takes the msecs as argument and sets the time for that hit as a XX.xxs string

if (input != 0) // Make sure there’s a value…
{
secs = int((input + 5)/1000L); // round msecs into whole secs
frac = int(((input + 5) % 1000L)/10L); // find fractional rounded secs

if (secs < 10)
{
  lcd.print(' ');               // for sub 10 sec times add leading space
}
lcd.print(secs);                // now add whole secs 
lcd.print('.');                 // now add period

if (frac < 10)
{
  lcd.print('0');               // If frac < 10 then pad with a zero
}
lcd.print(frac);                // now add fractional secs

}
else
{
lcd.print(" "); // If the input variable is zero set time to 6 blank spaces
}
}



So do a "slow" trial as before and an crazed trial as before. There shouldn't be any difference btw the two now and all the hits should be displayed in review mode, with no hits > than the stop limit for the CoF in use.

Looks like progress !! The QD mode as coded runs for 6 hits or 10 secs, whichever comes 1’st. If that’s not what’s desired then let me know. As for “21” my initial take is that the last “running” display isn’t getting updated when the timer stops and it should be. The result is that while there’s 8 hits recorded, the loop only got around to updating the display for 6 or 7. That disparity doesn’t bother me, one channel will get displayed before the other but that last display, before switching to the “review” displays, should show all the A and B hits. I’ll look into that as I did take specific measures to make sure that update happens and apparently those measures are insufficient.

But other than that, am I to believe that all the display oddities are gone ! I didn’t see any formatting errors; all the A’s and B’s and numbers and times all looked to be in their proper places and justified correctly. The OLED certainly lools sharp, clearer than the display you had borrowed. No problems with too many hits ? Or other weird behavior ? Have you tried the random delay time (for countdown) ?

I’ll have to start figuring out how to store CoFs to EEPROM now !

The extra hits in “21” mode is similar in nature to the “too many hits” problem we had before. What I think is happening is the hits are coming in fast and the display takes a relatively long time to update. When the timer is truely stopped, the next LCD update pushes it into the review mode display. What the code does at present (to get the last “running” display) is to hold off stopping the timer even when the time condition is met (in this case time > 1.5 secs) and it calls the UpdateLCD function one last time. Alas there are hits coming in during this update (see the times displayed for hits 6-8 in review mode) and since the timer isn’t really stopped just yet, they get recorded too. Funny thing Is when I was fixing the last problem I thought this might be an issue but was too brain dead to figure out what to do about it.

The answer is too stop the timer ontime and figure out another way to keep the display in “running mode” for that last update. Alas even then it’s possible to get a hit in at 0.01 secs prior to the stop time, spend 0.5 secs updating the LCD and then have another hit come in while that update is happening and be after the stop time. Less than perfect. I think the only way to absolutely fix this is to add another condition in the ISR (for Stoptime) just like I did for hit count. I’ll “sleep on it” but that’s my thinking ATM.

Can you describe what the QD CoF was intended to do ? We can fix that at the same time.

Here’s a fix for the “hits past the stop time” problem. QD remains as it was.

#include <OLEDFourBit.h>

// initialize the library with the numbers of the interface pins
// with the OLED library this also seems to do the init and begin calls
OLEDFourBit lcd(12, 11, 5, 4, 9, 8);

// set pin numbers
const int StartPin = 7;                  // the number of the Start pushbutton pin
const int TargetAPin = 2;                // the number of the A targets input pin
const int TargetBPin = 3;                // the number of the B targets input pin
const int BuzzerPin = 10;                // the number of the buzzer output pin
const int LEDPin = 13;                   // the number of the LED output pin
const int ScrollPin = 6;                 // the number of the scroll button input pin

// initialize the constants
const unsigned long MaxTime = 60001;     // the max time the timer can run for = 60 secs
const unsigned long WaitTime = 1000;     // wait time btw start and timer running = 1 sec plus debounce
const unsigned long DB_delay = 1000;     // set a debounce wait time of 1000 msecs
const unsigned long BuzzTime5 = 500;     // set the on time for the buzzer, 500 msecs
const unsigned long BuzzTime2 = 200;     // set the on time for the buzzer, 200 msecs
const unsigned long BuzzTime10 = 1000;   // set the on time for the buzzer, 1000 msecs
const unsigned int FreqHi = 2000;        // High frequency of buzzer tone
const unsigned int FreqLo = 1000;        // Low frequency of buzzer tone
const byte MaxHits = 24;                 // Maximum number of hits allowed per shooter

// initialize global variables
volatile byte TimerState = 0;            // variable for state of timer, running or not running
volatile byte AupDtFlag = 0;             // variable indication an A hit has occurred
volatile byte BupDtFlag = 0;             // variable indication a B hit has occurred
volatile int secs = 0;                   // whole seconds of time for display
volatile int frac = 0;                   // fractional part of time for display
unsigned long StartTime = 0;             // variable to hold the start time
unsigned long BuzzTime = 500;            // variable to hold the buzzer on time
unsigned int Freq = 2000;                // variable for high or low buzzer tone
volatile byte A_count = 0;               // variable to hold the number of A hits
volatile byte B_count = 0;               // variable to hold the number of B hits
volatile unsigned long A_Times[MaxHits]; // array to hold up to MaxHits hit times for target A
volatile unsigned long B_Times[MaxHits]; // array to hold up to MaxHits hit times for target B
byte DisplayIndex = 0;                   // variable for controlling what's displayed
byte diMax = 99;                         // Display index maximum
byte LastUpdt = 0;                       // Flag to allow last update of LCD after timer stopped

byte StopHits = MaxHits;                 // Number if hits allowed before stopping
unsigned long StopTime = MaxTime;        // Time that the timer will stop at 
byte CoF = 0;                            // initialize the mode to be standard
//byte CoF = 1;                          // initialize the mode to be quick draw
//byte CoF = 2;                          // initialize the mode to be 21
byte NumShooters = 1;                    // initialize the number of shooters to 1
//byte NumShooters = 2;                  // initialize the number of shooters to 2
byte RanDly = 0;                         // random delay disabled
//byte RanDly = 1;                       // random delay enabled

void setup()
{
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("    Shot Timer 1    ");
  lcd.setCursor(0, 1);
  lcd.print("    Initializing    ");
  delay(3000);

  // initialize output pins
  pinMode(BuzzerPin, OUTPUT);
  pinMode(LEDPin, OUTPUT);
  digitalWrite(BuzzerPin, LOW);
  digitalWrite(LEDPin, LOW);
   
  // initialize the input pins with internal pullups
  pinMode(StartPin, INPUT);
  pinMode(TargetAPin, INPUT);
  pinMode(TargetBPin, INPUT);
  digitalWrite(StartPin, HIGH);   
  digitalWrite(TargetAPin, HIGH);
  digitalWrite(TargetBPin, HIGH);

  // for now use pin 6 as button input
  pinMode(ScrollPin, INPUT);
  digitalWrite(ScrollPin, HIGH);

  // opens serial port, sets data rate to 9600 bps
  Serial.begin(9600);

  // setup ext pins as interrupts
  //attachInterrupt(0, ISR_A, FALLING);
  //attachInterrupt(1, ISR_B, FALLING);

  lcd.setCursor(0, 1);
  lcd.print("      Ready       ");

  ClearData();
}


void loop()
{
  if (TimerState) // is timer running
  {
    // timer is running
    
    // send data to display if there have been any hits
    if (AupDtFlag == 1 || BupDtFlag == 1)
    {
      UpdateLCD();
    }
    
    // timer is running so now check for any stop conditions
    if ((digitalRead(StartPin) == LOW) || ((millis() - StartTime) >= StopTime) || A_count >= StopHits || B_count >= StopHits)
    {
      // stop the timer
      StopTimer();
      LastUpdt = 1;
      tone(BuzzerPin, FreqLo, BuzzTime10);
      
      // update the LCD running display one last time
      UpdateLCD();
      
      // just for the moment send times to PC for debug
      SendTimes();
      
      // delay enough to debounce stop button
      delay(DB_delay);
      
      // Setup the review-stopped state display based on number shooters and hits
      if(NumShooters == 1)
      {
        diMax = byte(A_count/8) - 1;   // DI goes 0 to number of displays - 1
        if (A_count % 8)                 // diMax indicates last screen of hits
        {
          diMax ++;                      // limit lines per number of hits
        }
        DisplayIndex = diMax;            // display will show list of hit times
      }
      else
      {
        diMax = byte(max(A_count,B_count)/4) - 1; // find how many whole screens
        if (max(A_count,B_count) % 4)
        {
          diMax++ ;              // add another screen if partial exists
        }
        DisplayIndex = diMax;    // display will show list of hit times
      }
    }
  }
  else
  {
  // timer is stopped look for start button or review button pushes

    // check for scroll button pushes, change display state as reqd
    if (digitalRead(ScrollPin) == LOW)
    {
      DisplayIndex++;             // increment DisplayIndex upon push
      if (DisplayIndex > diMax)
      {
        DisplayIndex = 0;         // wrap around of scrolling action
      }
      // update display
      UpdateLCD();
      delay(DB_delay);
    }

    // check to see if start button has been pushed
    if (digitalRead(StartPin) == LOW)
    {
      // start button pushed
      // Do the following debug code only to get system running
      // This will send message to PC to show button was pushed
      Serial.println("Timer is running");
      
      // turn on the LED to show timer is running
      digitalWrite(LEDPin, HIGH);
      
      // show the timer has been started
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("    Shot Timer v1   ");
      lcd.setCursor(0, 1);
      lcd.print("     is running     ");
      lcd.setCursor(0, 2);
      lcd.print("      Shooter:      ");
      lcd.setCursor(0, 3);
      lcd.print("    --STANDBY--     ");
      
      // this will change when config mode is added ?
      switch (CoF)
      {
        case 1:
          //this is for quick draw mode
          StopTime = 10001;
          StopHits = 6;
          break;
        case 2:
          //this is for 21 mode
          StopTime = 1501;
          StopHits = MaxHits;
          break;
        default: 
          // the default = standard timer mode
          StopTime = MaxTime;
          StopHits = MaxHits;
      }
      
      // delay the Wait Time from start button push
      randomSeed(millis());
      // delay is min delay plus random if enabled
      delay(WaitTime);               // 1 sec min delay
      if(RanDly)
      {
        delay(random(2000, 4000));   // total delay btw 3 and 5 secs
      }
      
      // turn on interrupts as needed for 1 or 2 shooters
      attachInterrupt(0, ISR_A, FALLING);
      if (NumShooters > 1)
      {
        attachInterrupt(1, ISR_B, FALLING);
      }
      
      // clear all the prior runs data
      ClearData();      
      
      // save the starting time of this run
      StartTime = millis();
      
      // set state of timer to running
      TimerState = 1;
      
      // now buzz the speaker for 0.5 secs
      tone(BuzzerPin, FreqHi, BuzzTime5);
    }
  }
}


void StopTimer()
{
  // turn off interrupts
  detachInterrupt(0);
  detachInterrupt(1);
  TimerState = 0;
  // turn the LED off to show timer is stopped
  digitalWrite(LEDPin, LOW);
}


void ClearData()
{
  A_count = 0; 
  B_count = 0;
  for (byte i = 0; i < MaxHits; i++)
  {
    A_Times[i] = 0;
    B_Times[i] = 0;
  }
}


void SendTimes()
// temp routine to send data to serial monitor
{
  Serial.println("Timer is stopped");
  Serial.println("Here are the times for Shooter A");
  Serial.print("Number of hits : ");
  Serial.print("\t");
  Serial.println(A_count);
  Serial.println("A Hit and split times are : ");
  byte i = 0;
  unsigned long Split = 0;
  for (i = 0; i < A_count; i++)
  {
    Serial.print(i+1);
    Serial.print("\t");
    Serial.print(A_Times[i]);
    Serial.print("\t");
    if (i)
    {
      Split = A_Times[i] - A_Times[i-1];
      Serial.print(Split);
    }
    Serial.println("\t");
  }
  Serial.println("Here are the times for Shooter B");
  Serial.print("Number of hits : ");
  Serial.print("\t");
  Serial.println(B_count);
  Serial.println("B Hit and split times are : ");
  for (i = 0; i < B_count; i++)
  {
    Serial.print(i+1);
    Serial.print("\t");
    Serial.print(B_Times[i]);
    Serial.print("\t");
    if (i)
    {
      Split = B_Times[i] - B_Times[i-1];
      Serial.print(Split);
    }
    Serial.println("\t");
  }
}


void ISR_A()
{
  if(TimerState)
  {
    if(A_count < StopHits)
    {
      if((millis() - StartTime) < StopTime)
      {
        // store the hit time 
        A_Times[A_count] = millis() - StartTime;
        // increment the hit count and array index
        A_count++;
        // set the Hit flag so other stuff will update
        AupDtFlag = 1;
      }
    }
  }
}


void ISR_B()
{
  if(TimerState)
  {
    if(B_count < StopHits)
    {
      if((millis() - StartTime) < StopTime)
      {
        // store the hit time 
        B_Times[B_count] = millis() - StartTime;
        // increment the hit count and array index
        B_count++;
        // set the Hit flag so other stuff will update
        BupDtFlag = 1;
      }
    }
  }
}


void UpdateLCD()
{
AupDtFlag = 0;
BupDtFlag = 0;
// routine to format lines of data to be sent to LCD
// presently assume 4 lines of 20 characters
// formatting based number of shooters and timer state
  if(TimerState || LastUpdt)
  {
    // timer is running or this is last pass through before review
    // display number of hits so far
    LastUpdt = 0;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(" A: # hits = ");
    if(A_count < 10)
    {
      lcd.print(' ');
    } 
    lcd.print(A_count);
    // now display the time of last hit
    lcd.setCursor(0, 1);
    lcd.print("A Hit time = ");
    if(A_count)
    {
      TimeConvert(A_Times[A_count-1]); // convert hit time to XX.xx format
    }
    lcd.setCursor(0, 2);
    // Chose 1 or 2 shooter portion of display
    if(NumShooters == 1)
    {
      // this is for single A shooter running display
      // display the split time btw last hit and prior hit
      unsigned long Split = 0;
      if (A_count > 1)
      {
        Split = A_Times[A_count-1] - A_Times[A_count-2];
      }
      lcd.print("Split time = ");
      TimeConvert(Split); // convert split time
      lcd.setCursor(0, 3);
      // compute A hit factor
      //float HF = A_count/A_Times[A_count - 1];
      //lcd.print("Hit factor = ");
      //char buf[6];
      //PString(buf, sizeof(buf), HF);  // convert FP to char
      //lcd.print(buf);
    }      
    else
    {
      // this is for 2 shooter running display
      lcd.print(" B: # hits = ");
      if(B_count < 10)
      {
        lcd.print(' ');
      } 
      lcd.print(B_count);
      lcd.setCursor(0, 3);
      lcd.print("B Hit time = ");
      if(B_count)
      {
        TimeConvert(B_Times[B_count-1]); // Now for B times
      }
    }    
  }
  else
  {
  // timer is stopped
    if(NumShooters == 1)
    {
      // this is single shooter review display
      lcd.clear();
      lcd.setCursor(0, 0);
      byte k = 0;                    // k will hold LCD line number
      byte j = DisplayIndex << 3;    // index for first hit time displayed
      byte jj = (A_count % 8);       // jj holds number hits on last display
      if (jj == 0)
      {
        jj = 8;                      // acount for 8 hits on last screeen
      }
      if (DisplayIndex == diMax)     // diMax indicates last screen of hits
      {
        jj = j + min(jj,4);          // display 1 line per hit up to 4 lines
      }
      else
      {
        jj = j + 4;                  // there are enough hits to fill 4 lines
      }
      for (byte i = j; i < jj; i++)
      {
        k = (i % 4);
        lcd.setCursor(0,k);
        lcd.print('A');
        if(i<9)
        {
         lcd.print(' ');             // add a space for single digit numbers
        }
        lcd.print(i+1);              // send hit # to LCD
        lcd.print(": ");
        TimeConvert(A_Times[i]);     // convert hit time to XX.xx format
        if(A_Times[i+4])             // if there's a non-zero time
        {
          lcd.print(' ');            // print a space
          if((i+4)<9)
          {
           lcd.print(' ');           // add a space for single digit numbers
          }          
          lcd.print(i+5);            // send hit # to LCD
          lcd.print(": ");
          TimeConvert(A_Times[i+4]); // convert hit time to XX.xx format
        }
      }
    }      
    else
    {
      // this is 2 shooter review display
      lcd.clear();
      lcd.setCursor(0, 0);
      byte k = 0;                     // k will hold LCD line number
      byte j = DisplayIndex << 2;     // index for first hit time displayed
      byte jj = max(A_count,B_count); // init jj to max hit count
      jj = jj % 4;                    // jj holds number hits on last display
      if (jj == 0)
      {
        jj = 4;                       // acount for 4 hits on last screeen
      }
      if (DisplayIndex == diMax)      // diMax indicates last screen of hits
      {
        jj = j + min(jj,4);           // display 1 line per hit up to 4 lines
      }
      else
      {
        jj = j + 4;                   // there are enough hits to fill 4 lines
      }
      for (byte i = j; i < jj; i++)
      {
        k = (i % 4);
        lcd.setCursor(0,k);
        if(i<9)
        {
          lcd.print(' ');               // for hits 1-9 put in a leading space
        }
        lcd.print(i+1);                 // print hit number
        lcd.print(" A: ");
        TimeConvert(A_Times[i]);        // convert hit time to XX.xx format
        if(B_Times[i]);
        {
          lcd.print(" B: ");
          TimeConvert(B_Times[i]);      // convert hit time to XX.xx format
        }
      }  
    }
  }
}


void TimeConvert(unsigned long input)
{
// takes the msecs as argument and sets the time for that hit as a XX.xxs string

  if (input != 0)         // Make sure there's a value...
  {
    secs = int((input + 5)/1000L);         // round msecs into whole secs
    frac = int(((input + 5) % 1000L)/10L); // find fractional rounded secs

    if (secs < 10)
    {
      lcd.print(' ');               // for sub 10 sec times add leading space
    }
    lcd.print(secs);                // now add whole secs 
    lcd.print('.');                 // now add period

    if (frac < 10)
    {
      lcd.print('0');               // If frac < 10 then pad with a zero
    }
    lcd.print(frac);                // now add fractional secs
  }
  else
  {
    lcd.print("     ");         // If the input variable is zero set time to 6 blank spaces
  }
}

Awesome, I put it through some of its paces briefly and everything worked and looked great!

My thoughts for QD (quick draw) was basically a 1 hit round. Just a race to see who can hit the target the quickest.

sspbass:
Awesome, I put it through some of its paces briefly and everything worked and looked great!

My thoughts for QD (quick draw) was basically a 1 hit round. Just a race to see who can hit the target the quickest.

That makes sense given the name. So the number of hits would be 1. But I wonder if the coding shouldn't allow both competitors to get to that 1 hit vs stopping at the first hit (which is what it would do now) ? That way there's a time for both shooters. Let me think about how that can be done.

EDIT : Look at the snippet of code below…

  if (TimerState) // is timer running
  {
    // timer is running
    // send data to display if there have been any hits
    if (AupDtFlag == 1 || BupDtFlag == 1)
    {
      UpdateLCD();
    }
  
    // timer is running so now check for any stop conditions
    if ((digitalRead(StartPin) == LOW) || ((millis() - StartTime) >= StopTime) || A_count >= StopHits || B_count >= StopHits)
    {
      // stop the timer
     etc, etc, etc ....

There’s no reason there couldn’t be a A_StopHits and a B_StopHits and have the condition be an “and” rather than the present “or”. The question then becomes whether you want this for every shooter vs shooter CoF or just some ? The code added to the ISRs to fix the “too many hits” problem will stop either channel (A or B) from recording “excess” hits when it’s quota is reached while waiting for the other channel to reach it’s limit. Having different limits (A vs B) would allow “handicapping” though that’s not strictly needed. You could have the timer time until both reach the single StopHits limit with code like …

    // timer is running so now check for any stop conditions
    if ((digitalRead(StartPin) == LOW) || ((millis() - StartTime) >= StopTime) || (A_count >= StopHits && B_count >= StopHits))
    {
      // stop the timer
     etc, etc, etc ....

… though you’d have to do something else for the single shooter case.

Here’s a version that only stops the timer when :

  • you push the stop button or …

  • the time limit is reached or …

  • with a single shooter, the A channel reaches the hit limit or …

  • with dual shooters, both the A and B channels reach the hit limit.

Is this better than stopping when the 1’st person gets to the hit limit ? I dunno but perhaps the way to figure it out is to play with both versions, so here’s the other version.

#include <OLEDFourBit.h>

// initialize the library with the numbers of the interface pins
// with the OLED library this also seems to do the init and begin calls
OLEDFourBit lcd(12, 11, 5, 4, 9, 8);

// set pin numbers
const int StartPin = 7;                  // the number of the Start pushbutton pin
const int TargetAPin = 2;                // the number of the A targets input pin
const int TargetBPin = 3;                // the number of the B targets input pin
const int BuzzerPin = 10;                // the number of the buzzer output pin
const int LEDPin = 13;                   // the number of the LED output pin
const int ScrollPin = 6;                 // the number of the scroll button input pin

// initialize the constants
const unsigned long MaxTime = 60001;     // the max time the timer can run for = 60 secs
const unsigned long WaitTime = 1000;     // wait time btw start and timer running = 1 sec plus debounce
const unsigned long DB_delay = 1000;     // set a debounce wait time of 1000 msecs
const unsigned long BuzzTime5 = 500;     // set the on time for the buzzer, 500 msecs
const unsigned long BuzzTime2 = 200;     // set the on time for the buzzer, 200 msecs
const unsigned long BuzzTime10 = 1000;   // set the on time for the buzzer, 1000 msecs
const unsigned int FreqHi = 2000;        // High frequency of buzzer tone
const unsigned int FreqLo = 1000;        // Low frequency of buzzer tone
const byte MaxHits = 24;                 // Maximum number of hits allowed per shooter

// initialize global variables
volatile byte TimerState = 0;            // variable for state of timer, running or not running
volatile byte AupDtFlag = 0;             // variable indication an A hit has occurred
volatile byte BupDtFlag = 0;             // variable indication a B hit has occurred
volatile int secs = 0;                   // whole seconds of time for display
volatile int frac = 0;                   // fractional part of time for display
unsigned long StartTime = 0;             // variable to hold the start time
unsigned long BuzzTime = 500;            // variable to hold the buzzer on time
unsigned int Freq = 2000;                // variable for high or low buzzer tone
volatile byte A_count = 0;               // variable to hold the number of A hits
volatile byte B_count = 0;               // variable to hold the number of B hits
volatile unsigned long A_Times[MaxHits]; // array to hold up to MaxHits hit times for target A
volatile unsigned long B_Times[MaxHits]; // array to hold up to MaxHits hit times for target B
byte DisplayIndex = 0;                   // variable for controlling what's displayed
byte diMax = 99;                         // Display index maximum
byte LastUpdt = 0;                       // Flag to allow last update of LCD after timer stopped
boolean Stop = FALSE;                    // Flag to indicate hit condition to stop timer has been met
byte StopHits = MaxHits;                 // Number if hits allowed before stopping
unsigned long StopTime = MaxTime;        // Time that the timer will stop at 

byte CoF = 0;                            // initialize the mode to be standard
//byte CoF = 1;                          // initialize the mode to be quick draw
//byte CoF = 2;                          // initialize the mode to be 21
byte NumShooters = 1;                    // initialize the number of shooters to 1
//byte NumShooters = 2;                  // initialize the number of shooters to 2
byte RanDly = 0;                         // random delay disabled
//byte RanDly = 1;                       // random delay enabled


void setup()
{
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("    Shot Timer 1    ");
  lcd.setCursor(0, 1);
  lcd.print("    Initializing    ");
  delay(3000);

  // initialize output pins
  pinMode(BuzzerPin, OUTPUT);
  pinMode(LEDPin, OUTPUT);
  digitalWrite(BuzzerPin, LOW);
  digitalWrite(LEDPin, LOW);
   
  // initialize the input pins with internal pullups
  pinMode(StartPin, INPUT);
  pinMode(TargetAPin, INPUT);
  pinMode(TargetBPin, INPUT);
  digitalWrite(StartPin, HIGH);   
  digitalWrite(TargetAPin, HIGH);
  digitalWrite(TargetBPin, HIGH);

  // for now use pin 6 as button input
  pinMode(ScrollPin, INPUT);
  digitalWrite(ScrollPin, HIGH);

  // opens serial port, sets data rate to 9600 bps
  Serial.begin(9600);

  // setup ext pins as interrupts
  //attachInterrupt(0, ISR_A, FALLING);
  //attachInterrupt(1, ISR_B, FALLING);

  lcd.setCursor(0, 1);
  lcd.print("      Ready       ");

  ClearData();
}


void loop()
{
  if (TimerState) // is timer running
  {
    // timer is running
    
    // send data to display if there have been any hits
    if (AupDtFlag == 1 || BupDtFlag == 1)
    {
      UpdateLCD();
    }
    
    // timer is running so now check for any stop conditions
    if (NumShooters == 1)
    {
      if (A_count >= StopHits)
      {
        Stop = TRUE;
      }
    }
    else
    {
      if (A_count >= StopHits && B_count >= StopHits)
      {
        Stop = TRUE;
      }
    }
    if ((digitalRead(StartPin) == LOW) || ((millis() - StartTime) >= StopTime) || Stop)
    {
      // stop the timer
      StopTimer();
      LastUpdt = 1;
      tone(BuzzerPin, FreqLo, BuzzTime10);
      
      // update the LCD running display one last time
      UpdateLCD();
      
      // just for the moment send times to PC for debug
      SendTimes();
      
      // delay enough to debounce stop button
      delay(DB_delay);
      
      // Setup the review-stopped state display based on number shooters and hits
      if(NumShooters == 1)
      {
        diMax = byte(A_count/8) - 1;   // DI goes 0 to number of displays - 1
        if (A_count % 8)                 // diMax indicates last screen of hits
        {
          diMax ++;                      // limit lines per number of hits
        }
        DisplayIndex = diMax;            // display will show list of hit times
      }
      else
      {
        diMax = byte(max(A_count,B_count)/4) - 1; // find how many whole screens
        if (max(A_count,B_count) % 4)
        {
          diMax++ ;              // add another screen if partial exists
        }
        DisplayIndex = diMax;    // display will show list of hit times
      }
    }
  }
  else
  {
  // timer is stopped look for start button or review button pushes

    // check for scroll button pushes, change display state as reqd
    if (digitalRead(ScrollPin) == LOW)
    {
      DisplayIndex++;             // increment DisplayIndex upon push
      if (DisplayIndex > diMax)
      {
        DisplayIndex = 0;         // wrap around of scrolling action
      }
      // update display
      UpdateLCD();
      delay(DB_delay);
    }

    // check to see if start button has been pushed
    if (digitalRead(StartPin) == LOW)
    {
      // start button pushed
      // Do the following debug code only to get system running
      // This will send message to PC to show button was pushed
      Serial.println("Timer is running");
      
      // turn on the LED to show timer is running
      digitalWrite(LEDPin, HIGH);
      
      // show the timer has been started
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("    Shot Timer v1   ");
      lcd.setCursor(0, 1);
      lcd.print("     is running     ");
      lcd.setCursor(0, 2);
      lcd.print("      Shooter:      ");
      lcd.setCursor(0, 3);
      lcd.print("    --STANDBY--     ");
      
      // this will change when config mode is added ?
      switch (CoF)
      {
        case 1:
          //this is for quick draw mode
          StopTime = 10001;
          StopHits = 1;
          break;
        case 2:
          //this is for 21 mode
          StopTime = 1501;
          StopHits = MaxHits;
          break;
        default: 
          // the default = standard timer mode
          StopTime = MaxTime;
          StopHits = MaxHits;
      }
      
      // delay the Wait Time from start button push
      randomSeed(millis());
      // delay is min delay plus random if enabled
      delay(WaitTime);               // 1 sec min delay
      if(RanDly)
      {
        delay(random(2000, 4000));   // total delay btw 3 and 5 secs
      }
      
      // turn on interrupts as needed for 1 or 2 shooters
      attachInterrupt(0, ISR_A, FALLING);
      if (NumShooters > 1)
      {
        attachInterrupt(1, ISR_B, FALLING);
      }
      
      // clear all the prior runs data
      ClearData();      
      
      // save the starting time of this run
      StartTime = millis();
      
      // set state of timer to running
      TimerState = 1;
      
      // now buzz the speaker for 0.5 secs
      tone(BuzzerPin, FreqHi, BuzzTime5);
    }
  }
}


void StopTimer()
{
  // turn off interrupts
  detachInterrupt(0);
  detachInterrupt(1);
  TimerState = 0;
  Stop = FALSE;
  // turn the LED off to show timer is stopped
  digitalWrite(LEDPin, LOW);
}


void ClearData()
{
  A_count = 0; 
  B_count = 0;
  for (byte i = 0; i < MaxHits; i++)
  {
    A_Times[i] = 0;
    B_Times[i] = 0;
  }
}


void SendTimes()
// temp routine to send data to serial monitor
{
  Serial.println("Timer is stopped");
  Serial.println("Here are the times for Shooter A");
  Serial.print("Number of hits : ");
  Serial.print("\t");
  Serial.println(A_count);
  Serial.println("A Hit and split times are : ");
  byte i = 0;
  unsigned long Split = 0;
  for (i = 0; i < A_count; i++)
  {
    Serial.print(i+1);
    Serial.print("\t");
    Serial.print(A_Times[i]);
    Serial.print("\t");
    if (i)
    {
      Split = A_Times[i] - A_Times[i-1];
      Serial.print(Split);
    }
    Serial.println("\t");
  }
  Serial.println("Here are the times for Shooter B");
  Serial.print("Number of hits : ");
  Serial.print("\t");
  Serial.println(B_count);
  Serial.println("B Hit and split times are : ");
  for (i = 0; i < B_count; i++)
  {
    Serial.print(i+1);
    Serial.print("\t");
    Serial.print(B_Times[i]);
    Serial.print("\t");
    if (i)
    {
      Split = B_Times[i] - B_Times[i-1];
      Serial.print(Split);
    }
    Serial.println("\t");
  }
}


void ISR_A()
{
  if(TimerState)
  {
    if(A_count < StopHits)
    {
      if((millis() - StartTime) < StopTime)
      {
        // store the hit time 
        A_Times[A_count] = millis() - StartTime;
        // increment the hit count and array index
        A_count++;
        // set the Hit flag so other stuff will update
        AupDtFlag = 1;
      }
    }
  }
}


void ISR_B()
{
  if(TimerState)
  {
    if(B_count < StopHits)
    {
      if((millis() - StartTime) < StopTime)
      {
        // store the hit time 
        B_Times[B_count] = millis() - StartTime;
        // increment the hit count and array index
        B_count++;
        // set the Hit flag so other stuff will update
        BupDtFlag = 1;
      }
    }
  }
}


void UpdateLCD()
{
AupDtFlag = 0;
BupDtFlag = 0;
// routine to format lines of data to be sent to LCD
// presently assume 4 lines of 20 characters
// formatting based number of shooters and timer state
  if(TimerState || LastUpdt)
  {
    // timer is running or this is last pass through before review
    // display number of hits so far
    LastUpdt = 0;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(" A: # hits = ");
    if(A_count < 10)
    {
      lcd.print(' ');
    } 
    lcd.print(A_count);
    // now display the time of last hit
    lcd.setCursor(0, 1);
    lcd.print("A Hit time = ");
    if(A_count)
    {
      TimeConvert(A_Times[A_count-1]); // convert hit time to XX.xx format
    }
    lcd.setCursor(0, 2);
    // Chose 1 or 2 shooter portion of display
    if(NumShooters == 1)
    {
      // this is for single A shooter running display
      // display the split time btw last hit and prior hit
      unsigned long Split = 0;
      if (A_count > 1)
      {
        Split = A_Times[A_count-1] - A_Times[A_count-2];
      }
      lcd.print("Split time = ");
      TimeConvert(Split); // convert split time
      lcd.setCursor(0, 3);
      // compute A hit factor
      //float HF = A_count/A_Times[A_count - 1];
      //lcd.print("Hit factor = ");
      //char buf[6];
      //PString(buf, sizeof(buf), HF);  // convert FP to char
      //lcd.print(buf);
    }      
    else
    {
      // this is for 2 shooter running display
      lcd.print(" B: # hits = ");
      if(B_count < 10)
      {
        lcd.print(' ');
      } 
      lcd.print(B_count);
      lcd.setCursor(0, 3);
      lcd.print("B Hit time = ");
      if(B_count)
      {
        TimeConvert(B_Times[B_count-1]); // Now for B times
      }
    }    
  }
  else
  {
  // timer is stopped
    if(NumShooters == 1)
    {
      // this is single shooter review display
      lcd.clear();
      lcd.setCursor(0, 0);
      byte k = 0;                    // k will hold LCD line number
      byte j = DisplayIndex << 3;    // index for first hit time displayed
      byte jj = (A_count % 8);       // jj holds number hits on last display
      if (jj == 0)
      {
        jj = 8;                      // acount for 8 hits on last screeen
      }
      if (DisplayIndex == diMax)     // diMax indicates last screen of hits
      {
        jj = j + min(jj,4);          // display 1 line per hit up to 4 lines
      }
      else
      {
        jj = j + 4;                  // there are enough hits to fill 4 lines
      }
      for (byte i = j; i < jj; i++)
      {
        k = (i % 4);
        lcd.setCursor(0,k);
        lcd.print('A');
        if(i<9)
        {
         lcd.print(' ');             // add a space for single digit numbers
        }
        lcd.print(i+1);              // send hit # to LCD
        lcd.print(": ");
        TimeConvert(A_Times[i]);     // convert hit time to XX.xx format
        if(A_Times[i+4])             // if there's a non-zero time
        {
          lcd.print(' ');            // print a space
          if((i+4)<9)
          {
           lcd.print(' ');           // add a space for single digit numbers
          }          
          lcd.print(i+5);            // send hit # to LCD
          lcd.print(": ");
          TimeConvert(A_Times[i+4]); // convert hit time to XX.xx format
        }
      }
    }      
    else
    {
      // this is 2 shooter review display
      lcd.clear();
      lcd.setCursor(0, 0);
      byte k = 0;                     // k will hold LCD line number
      byte j = DisplayIndex << 2;     // index for first hit time displayed
      byte jj = max(A_count,B_count); // init jj to max hit count
      jj = jj % 4;                    // jj holds number hits on last display
      if (jj == 0)
      {
        jj = 4;                       // acount for 4 hits on last screeen
      }
      if (DisplayIndex == diMax)      // diMax indicates last screen of hits
      {
        jj = j + min(jj,4);           // display 1 line per hit up to 4 lines
      }
      else
      {
        jj = j + 4;                   // there are enough hits to fill 4 lines
      }
      for (byte i = j; i < jj; i++)
      {
        k = (i % 4);
        lcd.setCursor(0,k);
        if(i<9)
        {
          lcd.print(' ');               // for hits 1-9 put in a leading space
        }
        lcd.print(i+1);                 // print hit number
        lcd.print(" A: ");
        TimeConvert(A_Times[i]);        // convert hit time to XX.xx format
        if(B_Times[i]);
        {
          lcd.print(" B: ");
          TimeConvert(B_Times[i]);      // convert hit time to XX.xx format
        }
      }  
    }
  }
}


void TimeConvert(unsigned long input)
{
// takes the msecs as argument and sets the time for that hit as a XX.xxs string

  if (input != 0)         // Make sure there's a value...
  {
    secs = int((input + 5)/1000L);         // round msecs into whole secs
    frac = int(((input + 5) % 1000L)/10L); // find fractional rounded secs

    if (secs < 10)
    {
      lcd.print(' ');               // for sub 10 sec times add leading space
    }
    lcd.print(secs);                // now add whole secs 
    lcd.print('.');                 // now add period

    if (frac < 10)
    {
      lcd.print('0');               // If frac < 10 then pad with a zero
    }
    lcd.print(frac);                // now add fractional secs
  }
  else
  {
    lcd.print("     ");         // If the input variable is zero set time to 6 blank spaces
  }
}

The quick and easy way was to add a new variable, “Stop”, that indicates when the appropriate hit limit has been reached. Appropriate being different for 1 vs 2 shooters. And I set the max number of hits in QD = 1. Give it a try and see what flavor of “hit stoppage logic” seems best, and not just for QD. And again I supposed we could make the logic be either both or first (to the limit), choosable for any dual shooter CoF, if that’s deemed necessary.

NOOOOOOOOOOOOOOOO!!!

Try using true and false (lower case) and if that doesn’t compile change the boolean to byte and use 0 instead of false and 1 instead of true.

I really like the new method. I didn’t really think much about it before but it does make sense.

If I’m competing against someone I would like to be able to compare time for a full round even if I’m slower.

If I’m getting spanked and don’t want to finish when beat you can always just hit the start/stop button.

It seems to be working great now! I can’t think of anything to change at the moment.

Kewl ! Stop after both hits are completed it is.

So what was it, lower vs upper case ?

Syntax, I hate SYNTAX !!! Do what I mean not what I type ! Arrrrrggghhhhhhh …

Time to wire up the 2 axis pot and get to making the UI work.

Yep, just needed to be lower case.

That was the first thing I tried but I must have accomplished the amazing feat of spelling “false” wrong and didn’t catch it.

Which is why I thought it was something else.

I’ve been vascillating on how best to store CoFs to EEPROM so I guess it’s time to decide and get a trial version of that ready for test.

BTW the race is on ! I ordered a new Ivy Bridge laptop, due in in 1 week. At which time I can test this stuff myself (except for the LCD parts). Can it be “finished” before then ? Time will tell …

Congrats on getting this to work. I like the look of the OLED module, but I am not sure the price is right for me. I am going to put a display on some lasertag equipment and I don’t want to deal with an expensive broken OLED even if it has superior outdoor readability!

I was just posting to ask, from a HCI (human computer interaction) perspective, what exactly the joystick is doing to add over something like a 4 way directional pad? I would think that the d-pad would allow for a more hands off tactile feel. This type of control may be more appropriate for a remote control. Congrats again, a nice project and I hope to see videos of it in action soon. I might have to build one myself.

Dan

dkulinski:
I was just posting to ask, from a HCI (human computer interaction) perspective, what exactly the joystick is doing to add over something like a 4 way directional pad? I would think that the d-pad would allow for a more hands off tactile feel. This type of control may be more appropriate for a remote control. Congrats again, a nice project and I hope to see videos of it in action soon. I might have to build one myself.

Dan

I don’t know that the 2 axis pot has any advantage (perhaps 1) over a more-typical-these-days d-pad. The one place where it may is that you can have speed control. Say you’re scrolling the “Number of hits to stop” and you want to go from 2 to 22. You would press a d-pad and hold it there while the software scolled the numbers upward. Perhaps, like your digital alarm clock, after a set time the rate of number change would speed up so you get there faster. We all know what happens next … you overshoot your target number. (Or mebbe that’s just me. Being a geek I have no co-ordination :mrgreen: ) With a pot you can have speed control that varies with displacement off center. The speed change is under control of your thumb, literally.

Other than that, this pot seemed to be more “buttonlike” than most and it was less $$s than the d-pads I saw … which is rather amazing in itself. It also had the advantage of using 2 analog inputs, which is good as the project is out of digital ones.

FWIW the project ain’t quite done yet. I’m struggling with how to get the CoF name stored and retrieved from EEPROM. I have a long round-a-bout way to do it but I know there’s got to be a better way. Let me post some more on that later tonight and perhaps you or someone else can make a recommendation as to how to proceed.

That OLED is pretty and it’s got me wondering if there isn’t a larger version of it. What would be ideal is to have a 20 or so characters wide display that has more rows, so all the data could be seen a one glance. Seems the only way to make that happen is via a graphical display, which is more work to get working and the affordable ones seem to be postage stamp size.

Agreed, larger OLEDs quickly run up the cost and as you mentioned are graphical rather than character based. Why not just chain a couple of these things together using a latched shift register? It won’t be seamless but you can show more data.

As for the joystick I can see the advantage of using the analog pins, but here is a secret straight out of the Arduino reference:

The analog input pins can be used as digital pins, referred to as A0, A1, etc.

There are 6 more digital pins for you.

EDIT: And when I say used as digital pins I mean like this:

digitalRead("A0");

Dan

On the subject of EEPROM storage. I think I mentioned this in another thread but you really want to use a couple of circular buffers. One tells you where to read from/write to next and the other contains the data. This should maximize the life of your EEPROM. Of course with a 100k write lifecycle even writing repeatedly to the same spot is going to take a long time to kill the flash cells.

What considerations are you taking into account for this data storage? I would have a rigid structure for this one, a specific field length for the name and the score can probably be easily stored in a couple of bytes. Of course you could read it and restore it into a proper C++ object. I have done that with regular file IO on the PC and I don’t see why it isn’t doable in the Arduino environment.

Dan

dkulinski:
As for the joystick I can see the advantage of using the analog pins, but here is a secret straight out of the Arduino reference:

The analog input pins can be used as digital pins, referred to as A0, A1, etc.

There are 6 more digital pins for you.

EDIT: And when I say used as digital pins I mean like this:

digitalRead("A0");

Dan

I didn’t know that ! Thx !! Good to know if sspbass doesn’t like the slidey 2 axis pot thingee, there’s an option to go back to switches. It also opens up the option of using the rotary encoder … though I don’t know that it’s really any better … I just want to use one. :mrgreen:

ps - let me compose a, sadly, lenghty, reply re: EEPROM storage.

dkulinski:
On the subject of EEPROM storage. I think I mentioned this in another thread but you really want to use a couple of circular buffers. One tells you where to read from/write to next and the other contains the data. This should maximize the life of your EEPROM. Of course with a 100k write lifecycle even writing repeatedly to the same spot is going to take a long time to kill the flash cells.

What considerations are you taking into account for this data storage? I would have a rigid structure for this one, a specific field length for the name and the score can probably be easily stored in a couple of bytes. Of course you could read it and restore it into a proper C++ object. I have done that with regular file IO on the PC and I don’t see why it isn’t doable in the Arduino environment.

Dan

First let me start by detailing what I think should be stored in EEPROM. I figured it would be nice to have the timer recall the last Course of Fire (CoF) used, even after it was powered off/on or reset. Addditionally it would be nice to be able to select a CoF, with all it’s preset parameters, instead of having to use a “lo power” user interface (UI) to modify those parameters to setup a CoF every time something different is desired. Beyond that, I’m not planning to use EEPROM for anything else … ATM. So EEPROM will be used to store CoFs and the user will be able to select what CoF to use and modify, if desired, some of it’s parameters and store that away (perhaps under a different name) for later use. What constitutes a CoF and it’s parameters ?

Presently there’s going to be a 10 character name, the number of hits required to stop/finish the CoF, a max time allowed to run, whether it’s a single or dual shooter CoF and, because sometimes you might have an RO running the show, whether the timer is using a fixed countdown delay or a random one. In the future these might be expanded upon and perhaps some other timer functions might want to be stored as well, but these are a good start. Almost all of these are bytes, which is nice as the present simple Arduino support for EEPROM is to read or write a byte at a time. The stop time is an unsigned long (4 bytes) and the name is a character array (11 bytes). I’m also going to add another byte, a “magic number”, that will allow the software to know if the following block of EEPROM holds a CoF and whether writes to (mods of) the CoF are allowed, or whether it’s a read-only affair.

Now there are C templated functions that allow the user to read or write a defined structure of a mix of any variable types to/from EEPROM but that’s all a bit more complicated to me, and more than I think I need for the above. I can break down the time and reconstruct it easily enough (I think) and a character array is also easily accessed one byte at a time. Byte by byte reads and writes should suffice. So, for the moment, I’m thinking of eschewing what any real programmer would do and stick to orangutan methods.

In use the EEPROM would be accessed after reset and the last CoF used downloaded, the parameters setting the variables used:

NumShooters

RanDly

StopHits

StopTime

The user could choose to alter these parameters and just use them or save them and then use them. I’m not worried about too many EEPROM write cycles as a result as I expect the parameters to settle to some fixed set(s) and those sets to be stored away. There isn’t, so far as I know, a wear’n’tear problem with EEPROM reads.

My dilemma comes from how I was going to allow the user to choose amongst the CoFs in the UI. My first thought (and perhaps this needs to change) was to have an array of CoFs (an array of the names, an array of char arrays) and use an index to have the software switch from one to the next. In the UI, the user would increment/decrement the index (within preset limits) and so an index of 0 would result in the display, recall, usage, etc, etc of the 1’st CoF in the array of arrays. An index of 1 would use the second CoF, etc, etc. I’d build the array of arrays by reading EEPROM after reset and recalling the CoF names. Now perhaps this isn’t the best way to do it, but it’s what came to mind when I was goofing around with the UI. I’d have a list of CoFs :

char* CoFlist = {"Last Used “,“Standard 1”,“QuickDraw2”,” 21 “,” Custom 1 ", " Custom 2 “,” Custom 3 “,” A vs B "}

where CoFlist[1] would be the char array = {Standard 1}. I could construct the CoFlist from EEPROM by using loops to; first read the CoFname from EEPROM, byte by byte, concatentating each byte to a temp variable, to eventually get a char array … and then setting CoFlist = the temp variable = the CoFname from EEPROM.
Thinking some more, perhaps I should forgo building that array and instead just count, after a reset, the number of CoFs in EEPROM via the magic number(s) and then access the EEPROM as needed in the UI software ??
In any case I have to get the CoF name into EEPROM in the first place. I wasn’t going to have the UI allow the user to name, or change the name of an existing CoF, as that’s just too messy to do with a button and a slidey 2 axis pot thingee (or even a d-pad). So there needs to be a utility program that loads EEPROM with all the CoFs and their parameters. And that’s where I’m stuck ATM. How do I access, character by character (byte by byte) the components of, say, CoFlist[1] ? What’s the way to get the ‘S’, the ‘t’, the ‘a’, etc, etc so each can be written to EEPROM ? I understand the concept or pointers and referencing(*) and dereferencing(&) … somewhat … but for arrays of char arrays it seems there’s “stuff” going on in the background that hides the & operator. I could brute force the CoFnames in the utility program but accessing the char by char names would seem to be a simple thing to do … if I weren’t an orangutan. :cry:
snippet of code to read from EEPROM, the CoF parameters
```
*const unsigned long maskHigh = 0xF000;
const unsigned long maskMidU = 0x0F00;
const unsigned long maskMidL = 0x00F0;
const unsigned long maskLow = 0x000F;

// function to recall a CoF from EEPROM
void get_CoF(byte indx)
{
int base = indx * EEblocksize;
char temp1[11];
magicNumber = EEPROM.read(base);
for (int i = 0; i < NameLength; i++)
{
temp1[i] = EEPROM.read(base + 1 + i);
}
CoFlist(indx) = temp1;
NumShooters = EEPROM.read(base + NameLength + 2);
RanDly = EEPROM.read(base + NameLength + 3);
StopHits = EEPROM.read(base + NameLength + 4);
unsigned long temp2 = EEPROM.read(base + NameLength + 5) << 24;
temp2 &= EEPROM.read(base + NameLength + 6) << 16;
temp2 &= EEPROM.read(base + NameLength + 7) << 8;
StopTime = temp2 & EEPROM.read(base + NameLength + 8);
}*
* *similar snippet for writing CoF to EEPROM* *
void save_CoF(byte indx)
{
int base = indx * EEblocksize;
EEPROM.write(base, magicNumber);
for (int i = 0; i < NameLength; i++)
{
EEPROM.write(base + 1 + i, wCoFname[i]);
}
EEPROM.write(base + NameLength + 2, wNumShooters);
EEPROM.write(base + NameLength + 3, wRanDly);
EEPROM.write(base + NameLength + 4, wStopHits);
byte temp = byte((wStopTime & maskHigh) >> 24);
EEPROM.write(base + NameLength + 5, temp);
temp = byte((wStopTime & maskMidU) >> 16);
EEPROM.write(base + NameLength + 6, temp);
temp = byte((wStopTime & maskMidL) >> 8);
EEPROM.write(base + NameLength + 7, temp);
temp = byte(wStopTime & maskLow);
EEPROM.write(base + NameLength + 8, temp);
}

```
The question is how do I get a member, a name of a CoF, from CoFlist[] into CoFname[], so I can do CoFname[0] to get the 1’st character in that name ? This is probably a simpleton question.
ps - the “w’s” in the above are because I’m going to have (in the utility program) separate write variables and then read them back to verify they got into EEPROM.