Counting and Timing a shock sensor

dkulinski:
The CED 7000 is pretty nice but it uses audio to determine the shot? It doesn’t have a hit sensor? Dan

Correct. It’s like mine (but soooo much more complicated) and all the others. They listen for the sound of a shot and record that time. Most these days have a stop plate sensor to stop the timer, and that’s a wired connection to a switch. Using sensors on the targets allows this project timer to do real A vs B competitions, as any single normal timer can’t tell one shot from another.

FWIW with 6 diodes, 6 switches and 3 I/O pins, you can Charlieplex the I/O lines so as to read those 6 switches. Frankly it might be easier to code to read pin 6 as the “Enter” button and then set up a resistor divider on the A/D A0 line to read 4 more switches to get the left/right/up/down functions. The only real drawback to this latter approach is one of power. With 5V always across the R’s you’ll be drawing ~200 uA all the time.

Using the below, you’d set thresholds for the A/D reading such that:

686 = S1 pushed

572 - 686 = S2 pushed

451 - 571 = nothing pushed

377 - 450 = S3 pushed

< 337 = S4 pushed

So long as the A/D has noise less than perhaps 30 or so counts, it should read w/o error.

I was looking for 4 (or 5) way switch and saw this on SFE …

http://www.sparkfun.com/products/11160

Seems a might small (IMO) and would require 5 input pins (not sure if the BoB could be charlieplexed).

And at Mouser (didn’t see anything reasonable @ Digi orJameco)

http://www.mouser.com/catalog/645/usd/1789.pdf

The snap-in with center push, a lot like SFE one above but not SMD and not needing a BoB to easily use.

But this also caught my attention as well.

http://www.sparkfun.com/products/9426

Read the description but it’s a cheap 2 axis pot so switching would be 2 analog reads on 2 analog input pins with thresholding akin to the above analog scheme to determine left/right and up/down. What I like is the reasonable size, small but not too so, and that you could navigate left/right/up/down w/o ever having to take your thumb (or finger) off the “switch”. If only it had a center push button as well, I’d think it perfect.

I like that bottom button, perhaps it isn’t so bad that it doesn’t have a center push. I don’t know that id want to have to deal with trying to push it in while it’s on my belt. Maybe we just get a big ole start button thats easy to reach down and push without looking.

Ran your code in the different modes. The A and A vs B modes worked good. The other two were doing some funky stuff like not resetting properly when additional rounds were started. The quickdraw went to 6 hits and the 21 was different every time.

I don’t know what you’re planning for a product design of this timer, whether it’s to be clipped to your belt (might be hard with wires to targets) or put on a table/stand/whatever near you or ??? I would imagine the display would have the matrix of 4 navigation buttons, or the slidey joystick or some alternative, plus perhaps an “enter” button near it (the display) … all on one side of the timer so the controls and display can be seen all at once. That may not lend itself to being seen while clipped. The large push button you had mentioned waaaay back when would seem to be a good one for the start/stop and perhaps it’s mounted where it’s easily pushable when clipped or sitting on a table (if desired 2 such buttons, wired in parallel could be done). Some sort of power switch is also needed, put someplace where it can’t be accidentally moved.

As to the “QD” and “21” modes … I just did a quick look and don’t see why they’d act any differently than AvsB, but with differing stop criteria (# hits and/or time). Perhaps if the hits got > StopHits, due to the switch bounce issue we’ve seen before, that might screwup the review display, but the start button for the next string should basically reset that. Guess I don’t know what to think …

Then again perhaps I just messed up my interpretation of the QD and “21” CoFs. QD was coded to stop at 1.5 secs and allow any number of hits (up to the present max of 20) and “21” was to stop at 6 hits (my choice ATM) and/or 10 secs, whichever came first. The number of hits for “21” would be one of those sub-options to be setup if “21” were choosen. Perhaps the timer did this but they’re the wrong limits ?

My thinking is, akin to “21” above, that you’d have a few custom modes called Custom1, Custom2, etc that you’d specify differing hits and times for (and perhaps number of shooters as well) and then save them as Custom1, Custom2, etc. In general once saved you’d wouldn’t modify them (although you could), they’d just be handy presets for drills or CoFs you often use. Eventually it would be good if you could customize a general CoF and then save those options/presets, naming it what ever you wish, for later usage w/o having to recode the Arduino. I just don’t know how to do that ATM.

ps - I take it the funky up and down scrolling worked ? :?:

I didn’t get a chance to look too close at the new sketch last night. I look at it more in depth tonight and post up a vid as well. The funky scrolling did work. The old scroll button did nothing but when I hit the shock sensor it scrolled through. Did you have one interrupt pin going forward and one going back? The one shock sensor was tied to both interrupt pins so I’m not sure if that messed with things. It did scroll through everything though.

As far as clipping on a belt that was my dream when it goes wireless. For now lets just get it working with wires and sitting on a table. With that in mind for when I’m putting together hardware, what is the wire going to be transmitting? That’s going to be a long distance for any low voltage signal is it not?

sspbass:
I didn’t get a chance to look too close at the new sketch last night. I look at it more in depth tonight and post up a vid as well. The funky scrolling did work. The old scroll button did nothing but when I hit the shock sensor it scrolled through. Did you have one interrupt pin going forward and one going back? The one shock sensor was tied to both interrupt pins so I’m not sure if that messed with things. It did scroll through everything though.

Yes, the shock sensor was to scroll one way and the prior B button, the other. The old scroll button does nothing in that code. Good to know it works. Having the shock sensor tied to both at the same time should result in a scroll in one direction as the code looks for it, “sees it”, does the scroll calcs and then does a 1 sec wait before it looks for the next thing. The PW out of the sensor isn’t long enough to be seen that 2’nd time. You can put that B button back in, it’ll be in parallel with the sensor and won’t affect it while it’s not pushed. When pushed it’ll cause a hit (or 5) if the timer is running or a scroll when it’s not. Since the sensor pulse stretcher has an open collector output, the short caused by the B button push won’t hurt the pulse stretcher.

sspbass:
For now lets just get it working with wires and sitting on a table. With that in mind for when I’m putting together hardware, what is the wire going to be transmitting? That’s going to be a long distance for any low voltage signal is it not?

IIRC you have the piezo shock sensor triggering a 1 shot timer. The timer is the CMOS cousin of the venerable 555. Again from memory, the PW of the pulse is ~1.7 msec nominally. What the 1 shot does is pull the line connected to it’s output pin to ground via a saturated transistor (or FET). I think you had a pull-up resistor at the 1 shot end and, with the Arduino setup the way it is, a weak (>10 k) pull-up at the Arduino end. I don’t know that this is perfect for transmitting a 1.7 msec pulse down XX ft of unkown wire but it’s a start and should work at least well enough to trigger the Arduino interrupt(s).

How many targets are you eventually thinking of having ? How far away from the Arduino timer will they be ? What kind of wiring were you thinking of using ? On this last part, I might suggest plain ole Cat5 cabling with the normal RJ45 connectors and jacks. They’re cheap and easy to plug in and anything you put down range will get hit at some point in time. Count on replacing a cable every so often. You won’t be pushing much current so Cat5 might work well, even to carry power to the sensor electronics from the timer or it’s power supply. Given all the A targets share the same Arduino pin input and the B’s do likewise with their input pin, I could see 2 RJ45 jacks on the timer, connecting to 2 Cat5 cables, each one running to a daisy chain of A or B targets. 3 pairs of the 4 pairs of wires could be used to carry Vcc and ground to the targets and the remaining pair the hit pulse signal back to the Arduino timer. You’d have a longish Cat5 cable from the timer to A target #1 and then shorter Cat5 cables from A target #1 to A target #2 to A target #3 to … Similarly you’d repeat that set-up with another set of cables for any B targets.

So I think it’s decision time for you … to decide on how to implement the user buttons to do the 4 or 5 way navigation (scrolling) and selection talked about above. With switches and diodes I can [Charlieplex the 3 digital inputs (pins 2, 3 and 6) to get 5 input buttons … or do the analog scheme in my schematic to get 4 buttons plus use pin 6 for a “select” button … or do the slidey joystick analog scheme plus pin 6. If you have more of the same pushbutton switches you’ve been using and some resistors, we can mimic the behavior of the slidey joystick so progress can be made while you wait for parts.](Charlieplexing - Wikipedia)

I’m not sure why the “21” and QD modes worked in some wonky fashion wrt to starting after stopping, I guess I’ll wait to see a vid or more description from you, before I can debug that. In the meanwhile I was thinking about how to store the user custom settings and the settings/options for the various modes or CoFs. Writing to EEPROM seems to be the way to do that. While the Arduino let’s you write and read frmo EEPROM, it’s done byte by byte … which is OK but I believe people have already defined a structured way of doing it so mixed type (longs, ints, bytes) variables can be stored. Given that’s do-able we need to define what’s to be stored.

Obviously the # of hits to stop and a stop time are the 2 key variables that distinguish one CoF from another. Do we want to add 1 or 2 shooters to that list ? Perhaps a name to be used to check that the correct info has been retrieved (and paves the way for later use) ? What else ? Random vs fixed countdown delay ? (Are you going to have user selectable fixed delay times or just a choice btw an X sec fixed delay or a 3-5 sec random delay ?)

Here are the issues I was referring to. http://www.youtube.com/watch?v=ol-YRLuI … e=youtu.be

As for the buttons I’m still tossing it around. What are your thoughts?

sspbass:
As for the buttons I’m still tossing it around. What are your thoughts?

I kinda like the slidey 2 axis joystick thingee. I just don’t know how it’ll feel to use.

Let me look at the new video.

OK, that’s oddness in that vid. I deduce from the beep … beeeep in the 2’nd string that the timer was running in “21” mode (1.5 sec to get as many hits as you can). I’ll see what I can find in the code. That there is a non-zero B count but the last B hit time isn’t displaying but there were 3 B hit times (for the 1’st string) gives me a clue where to look. The 62+ sec time displayed in the 2’nd string has got to be a good clue too. And 28 hit count in 1.5 sec … hmmmm. Obviously something is goofy in the zero out code.

Did you notice if you got the normal countdown display, “Timer is running, shooter STANDBY” after the start and before the “go” beep ?

Did the serial monitor say “timer is running” for these 2 strings ?

And this all still works fine, just as it used to, in the A vs B mode ?

ps - If you can train Athena to get you a beer, you can train Athena to help you here !! :smiley:

I found a few things, at least one of which explains what was seen in the video.

  1. The lack of a B hit time in the “running” display is due to a copy’n’paste goof. I swear I’ve seen and fixed this before. If you look for …

TimeConvert(B_Times[A_count-1]); // Now for B times

… you’ll find it. See the problem … B times, A count. :doh: This would be wrong in the AvsB mode as well.

  1. The zero routine was only doing to the number dictated by StopHits. In real life this might be OK but the code should always zero out the whole memory, just in case. And as we’ve seen the switch bounce can cause hits to be > than the stop number. That said I don’t think this, or some other niggling details, can account for the number of A hits before you’ve even had a chance to do something. Or the large A hit time for the very first hit (8 and 62 secs). I note the other A hit times were not unreasonable given the test.

  2. I think there’s a wiring error or something odd about how the button is tied to the A interrupt pin. The wierd times and counts are only in the A channel. I note that in the sequence to start the timer timing, the ISRs are enable and then the start time is set. If there are “hits” before the start time is set, they will be recorded with a large time while any hits after the initialization will be OK. Probably not a problem in real life but I will reverse the order. What I suggest you do is tie the sensor back to A only and the button to B only and check that the start button is all good wiring-wise. Buttons should be connect to their Arduino input pin and to ground and that’s it (now).

So the above just confirms to me that the present coding is a bit unwieldy. Since it has to change to do the displays for the user interface (UI) and since I’ve got a start on that now, I’m not going to fix the above. Instead I’ll replace it. Let me post a new project that will fix the above problems and also allow for the UI expansion. It’ll also allow for 1 or 2 shooters with all CoFs (preset but changable via the UI) instead of having an A mode and an AvsB mode.

I like the slidy joystick as well, just as long as it isn’t hard to get it to increment only one spot. i.e. you just push it a little bit and it continues to scroll indefinitely and it’s so fast that it’s difficult to just increment one time.

sspbass:
I like the slidy joystick as well, just as long as it isn’t hard to get it to increment only one spot. i.e. you just push it a little bit and it continues to scroll indefinitely and it’s so fast that it’s difficult to just increment one time.

My thinking is that the code initially does a single threshold test for each “push”. Read the X pot with the A/D, X > threshHi means the right “button” was pushed. X < threshLo means a left. Read the Y pot with the A/D, Y > threshHi means a right and Y < threshLo meas a left. Just as we do now, there’s a delay btw each detected “push” and the next reading so things can’t get too fast. Later we could experiment with a 2 stage, normal and “hurry up”, thresholding to see if that works or not. I don’t anticipate any of the UI displays being so long to scroll through that a “hurry up” push is needed but …

We can come up with a test sketch that just reads the slidey joystick and reports to the serial monitor the values and if a “button” was pushed (assuming you get it). That way is can be tuned before it gets incorporated into the timer project.

Here’s a new way of doing the old FormatData(), which is now replaced with UpdateLCD(), a more proper description of what it really does. Now the code does one “timer is running” display for a single shooter (same ole hit count, hit and split times) and a different one for 2 shooters (same ole A & B hit counts and times). There’s no difference for the “mode” or CoF choosen when the timer is running or stopped.

The CoF choice determines just (ATM) the stop time and allowed number of hits. So look where you did before to change “mode” and you’ll see commented out init’s for number of shooters and CoF. These are now the things to change to alter the timer’s functioning. Later on they will be settable via the new UI. As said before you should now be able to have 1 or 2 shooters for any CoF choosen.

I’ve returned to the single forward-only scroll action using the old pin 6 button. The shock sensor and other button should only do hits now. It might be a good idea to verify that with the timer stopped. That is, with the number of shooters = 2, and the timer stopped, whack the sensor and push the B button, and then start the timer and let’s make sure the hit counts and times are not AFU from the git-go, like there were last time (as seen in the last video).

Other than the above, this new code should act the same was as the old (but w/o the mistakes I hope). I’ve got rid of most of the old bones lying about. If this works the next rev will reduce the memory footprint by getting rid of split time storage and just computing them as/when needed for display. Then we can add the slidey pots, or an emulation of them, depending on what you have for parts, to get the UI up and running. I think work will be slow again tonight so I’ll try to post something coherent about that.

Give the new stuff a good look over and a work out when you can. See if it doesn’t make more sense to read now.

#include <LiquidCrystal.h>

// initialize the library with the numbers of the interface pins
LiquidCrystal 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
//const int ButtonInPin = A0;   // the pin used for the analog buttons

// initialize the constants
const unsigned long MaxTime = 30000;     // the max time the timer can run for = 30 secs
const unsigned long WaitTime = 2000;     // wait time btw start and timer running = 2 secs 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 = 20;                 // Maximum number if hits allowed

// 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
byte DisplayIndex = 0;                   // variable for controlling what's displayed
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 long A_Times[MaxHits];          // array to hold up to MaxHits hit times for target A
volatile long B_Times[MaxHits];          // array to hold up to MaxHits hit times for target B
unsigned long A_splits[MaxHits];
unsigned long B_splits[MaxHits];
long AB_splits[MaxHits];
float A_Hit_Factor = 0;
float B_Hit_Factor = 0;
byte diMax = 99;                         // Display index maximum
volatile int secs = 0;
volatile int frac = 0;

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


void setup()
{
  // set up the LCD's number of columns and rows: 
  lcd.begin(20, 4);
  lcd.clear();
  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       ");
}


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)
    {
      CalcTimes();
      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 change the display
      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
      }
      // change to new display
      UpdateLCD();
      delay(DB_delay);
    }
    //if (digitalRead(TargetAPin) == LOW)
    //{
      //DisplayIndex--;             // decrement DisplayIndex upon push
      //if (DisplayIndex < 0)
      //{
        //DisplayIndex = diMax;     // wrap around of scrolling action
      //}
      // change to new 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);
      
      // clear all the prior runs data, reset the display
      ClearData();
      lcd.clear();
      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 = 10000;
          StopHits = 6;
          break;
        case 2:
          //this is for 21 mode
          StopTime = 1500;
          StopHits = MaxHits;
          break;
        default: 
          // the default = standard timer mode
          StopTime = MaxTime;
          StopHits = MaxHits;
      }
      
      // delay the Wait Time from start button push
      // this delay is for debounce purposes and should stay
      delay(DB_delay);
      // this delay will change to random later on    
      delay(WaitTime);      
      
      // save the starting time of this run
      StartTime = millis();
      
      // set state of timer to running
      TimerState = 1;
      
      // turn on interrupts as needed for 1 or 2 shooters
      attachInterrupt(0, ISR_A, FALLING);
      if (NumShooters > 1)
      {
        attachInterrupt(1, ISR_B, FALLING);
      }
      
      // 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;
    A_splits[i] = 0;
    B_splits[i] = 0;
    AB_splits[i] = 0;
   }
  A_Hit_Factor = 0;
  B_Hit_Factor = 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;
  for (i=0; i < A_count; i++)
  {
    Serial.print(i+1);
    Serial.print("\t");
    Serial.print(A_Times[i]);
    Serial.print("\t");
    Serial.println(A_splits[i]);
  }
  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");
    Serial.println(B_splits[i]);
  }
}


void ISR_A()
{
  // 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()
{
  // 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 CalcTimes()
{
// routine to calculate all data and declare winner
// not all calcs having meaning for uses of timer
  // calculate A splits and cumlative hit factor
  if (A_count > 1)
  {
    for (int i=1; i < A_count ; i++)
    {
      A_splits[i] = A_Times[i] - A_Times[i-1];
    }
  }
  else
  {
    A_splits[0] = A_Times[0];
  }  
  A_Hit_Factor = A_Times[A_count - 1]/A_count;
  // calculate B splits and cumlative hit factor
  if (B_count > 1)
  {
    for (int i=1; i < B_count ; i++)
    {
      B_splits[i] = B_Times[i] - B_Times[i-1];
    }
  }
  else
  {
    B_splits[0] = B_Times[0];
  }  
  B_Hit_Factor = B_Times[B_count - 1]/B_count;
  // Calculate A - B times just in case
  int Min_count = min(A_count, B_count);
  for (int i=0; i < Min_count ; i++)
    {
      AB_splits[i] = A_Times[i] - B_Times[i];
    }
// add more here for A vs B modes
}


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
    if(NumShooters == 1)
    {
      // this is for single A shooter running display
      // display number of hits so far
      lcd.clear();
      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(" Hit  time = ");
      TimeConvert(A_Times[A_count-1]); // convert hit time to XX.xx format
      // now display the split time btw last hit and prior hit
      lcd.setCursor(0, 2);
      lcd.print("Split time = ");
      TimeConvert(A_splits[A_count-1]); // convert split time
    }      
    else
    {
      // this is for 2 shooter running display
      // display number of hits so far
      lcd.clear();
      lcd.print(" A: # hits = ");
      if(A_count < 10);
      {
        lcd.print(' ');
      } 
      lcd.print(A_count);
      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);
      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[A_count-1]); // Now for B times
      }    
  }
  else
  {
  // timer is stopped
    if(NumShooters == 1)
    {
      // this is single shooter review display
      lcd.clear();
      byte k = 0;
      byte j = 8*DisplayIndex;   // DI goes 0 to number of displays - 1
      if (DisplayIndex == diMax)  // diMax indicates last screen of hits
      {
        byte jj = j + max((A_count % 8),4); // limit lines per number of hits
      }
      else
      {
        byte 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 ");
        lcd.print(i+1);            // send hit # to LCD
        lcd.print(": ");
        TimeConvert(A_Times[i]);   // convert hit time to XX.xx format
        lcd.print("  ");
        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();
      byte k = 0;
      byte j = 4*DisplayIndex;   // DI goes 0 to number of displays - 1
      if (DisplayIndex == diMax)  // diMax indicates last screen of hits
      {
        byte jj = j + max((A_count,B_count) % 4); // limit lines per number of hits
      }
      else
      {
        byte 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);
        lcd.print(" A: ");
        TimeConvert(A_Times[i]); // convert hit time to XX.xx format
        lcd.print(" B: ");
        TimeConvert(B_Times[i]); // convert hit time to XX.xx format
      }  
    }
  }
}


void TimeConvert(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
  }
}

ps - WRT reducing memory usage, what do you think is a real upper time limit for any string or CoF ? Right now it’s 99 secs because if time has to be displayed to 3 digits plus .xx (ie -100.xx), you’ll run out of room on the display. If the time limit can be further reduce to < 65 secs, say 1 min = 60 secs, then the times saved can be 2 bytes long instead of 4, thus halving the memory used to store them. I think you could then have up to 99 hits,though i don’t see you ever really needing that high a number.

While you’re having fun with the new code, here a diagram of my thoughts re: a UI. The concept is you press the Configure/Select button (pin 6) and hold it for ~1 sec. You get a beep and a new display. That display is the “Select CoF :” display below. Using the up or down “buttons” you rotate the possible CoFs into the display, with one “highlighted” via the arrows. If you then press the Select button, that CoF becomes loaded into the timer, with it’s presets for # hits, time out, 1 vs 2 shooters and countdown delay now in effect. Using the left and right “buttons” you can examine these presets and change them (using the up/down “buttons” and select button) if desired. All the “buttons” operate as a ring where continually pressing one will eventually get you around the ring and back to where you started (the diagram is prolly easier to follow). If you make changes to any preset, you have the option to exit the Configuration process w/o using any of the changes, or exit and use the changes but they’ll be lost if you power off before saving them or exit and save them to the last highlighted CoF. You may choose to change presets and save them back into the same CoF or change the presets and then change the CoF selected and then “Exit and save”, and so save them to a “new” CoF. This is the idea behind the Custom CoFs, they provide a place to save these “customized” CoFs. At present I don’t expect to be able to create new CoF names via the timer. That can be done via a new/revised sketch.

As long as we don’t add too many changable presets, I think this scheme works w/o too much scrolling induced pain.

Thoughts ?

(Naturally the up/down “buttons” will scroll the stop time, and stop # of hits, through all their possible values, not just the ones I show above, wrapping over at their 0 and max values)

I like it but the order seems a bit odd.

Shouldn’t the Save menu be last in the sequence?

Also, shouldn’t the number of hits and the time out menus be after the CoF is selected as well as only displaying the one that is needed.

i.e. if I’m setting the number of hits I want to time to there’s no need to select the time out time, it should just be set to the max.

The code above is having some compile issues and I got stuck on one.

It’s acting as if max were a variable and not a function. I don’t know what gives.

I see you’ve moved the declaration of jj to outside of the if…else. This is a good thing as I’ll guess the compiler was complaining about jj not being declared in the following for loop. But having done that you need to also remove the declarations of byte jj = that were/are in the if…else.

That might be enough to resolve the problem above but let’s try to kill all possible compiler complaints. The max() looks to be OK to me but sometimes compilers can get confused when you do too much on a single line so lets divide that up into 2 parts. Change the declaration of byte jj = 0; to do the max() we want and then later in the if…else we’ll redefine jj to be the end result desired. The code prolly makes more sense when you look at it than my explanation.

Now aware of the problem above, I see there’s a similar issue further below in the code and a true mistake in the usage of max() as well. I wanted the max() of the A and B hit counts and then wanted to take the modulo 4 of that max. So I fixed that mistake in a fashion similar to the above. See the following and then give it a try.

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
    if(NumShooters == 1)
    {
      // this is for single A shooter running display
      // display number of hits so far
      lcd.clear();
      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(" Hit  time = ");
      TimeConvert(A_Times[A_count-1]); // convert hit time to XX.xx format
      // now display the split time btw last hit and prior hit
      lcd.setCursor(0, 2);
      lcd.print("Split time = ");
      TimeConvert(A_splits[A_count-1]); // convert split time
    }      
    else
    {
      // this is for 2 shooter running display
      // display number of hits so far
      lcd.clear();
      lcd.print(" A: # hits = ");
      if(A_count < 10);
      {
        lcd.print(' ');
      } 
      lcd.print(A_count);
      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);
      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();
      byte k = 0;
      byte j = 8*DisplayIndex;   // DI goes 0 to number of displays - 1
      byte jj = A_count % 8;        // halfway step to final jj desired
      if (DisplayIndex == diMax)  // diMax indicates last screen of hits
      {
        jj = j + max(jj, 4);           // limit lines per number of hits
      }
      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 ");
        lcd.print(i+1);            // send hit # to LCD
        lcd.print(": ");
        TimeConvert(A_Times[i]);   // convert hit time to XX.xx format
        lcd.print("  ");
        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();
      byte k = 0;
      byte j = 4*DisplayIndex;                // DI goes 0 to number of displays - 1
      byte jj = max(A_count,B_count) ;  // halfway step to final jj desired
      if (DisplayIndex == diMax)             // diMax indicates last screen of hits
      {
        jj = j + (jj % 4);                          // limit lines per number of hits
      }
      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);
        lcd.print(" A: ");
        TimeConvert(A_Times[i]);         // convert hit time to XX.xx format
        lcd.print(" B: ");
        TimeConvert(B_Times[i]);          // convert hit time to XX.xx format
      }  
    }
  }
}

I don’t know if you found and fixed any other coding errors in the above, so look it over and re-fix the above if you did.

ps - I’m unsure about the multiply by 4 and multiply by 8 operations in the above. I want the whole number result only, not a FP number. I think the usage let’s the compiler know that, but if not, then the byte() conversion function should do the trick. As in :

byte j = 8*DisplayIndex;

… becomes …

byte j = byte(8*DisplayIndex);

… and similarly for the 4*DisplayIndex case. I’d just put 'em in but I don’t like to add stuff if it’s really not needed. Let me know if it is needed.

http://arduino.cc/en/Reference/Max