Pepisan
Published © GPL3+

Airgun chronograph

Projectile speed measurement with arduino!

IntermediateFull instructions provided5,606
Airgun chronograph

Things used in this project

Hardware components

Arduino UNO
Arduino UNO
×1
Photodiode, 45 °
Photodiode, 45 °
Or use what you can find but you will need to adapt the circuit original parts used were (TSHF5410 LED’s and LTR-323DB photodiodes)
×1
Standard LCD - 16x2 White on Blue
Adafruit Standard LCD - 16x2 White on Blue
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Mastech MS8217 Autorange Digital Multimeter
Digilent Mastech MS8217 Autorange Digital Multimeter

Story

Read more

Schematics

Sensor circuit

The design is for 4 x 8 diodes, I used one lm 339 so I made just one 1/2 :)

Code

Chronograph v2.0

Arduino
Sketch provides basic chronograph functionality by measuring time between trigerring both sensors and displaying calculated m/s and fps.
Menu functionality has been added for setig up pellet weight needed for energy calculation.
TO DO, add additional functionalities, such as average speed based on no. of shots etc.
Chronograph 
 
Version 2.00

Sketch provides basic chronograph functionality by measuring time between trigerring both sensors and displaying calculated m/s and fps.
Menu functionality has been added for setig up pellet weight needed for energy calculation.
TO DO, add additional functionalities, such as average speed based on no. of shots etc.
 
 
Thanks to Phil Grant, polepole for providing parts of the code

Petar Janevski 27/04/2020

*/


#include <EEPROM.h>
//#include <EEPROMAnything.h>
#include <avr/sleep.h>
#include <LiquidCrystal.h>
#define Trip_in_1 8  //set light Trigger 1 to pin 8  
#define Trip_in_2 9  //set light Trigger 2 to pin 9
#define UP 14  //Set the menu up button to input 14
#define DOWN 15  //Set the menu down button to input 15
#define OK_BTN 16 //Set menu button OK to input 16
#define WAIT_STATE 0
#define MONITOR_STATE 1
#define OUTPUT_STATE 2
#define MENU_STATE 3

unsigned long Trigger_1 = 0;
unsigned long Trigger_2 = 0;
//volatile bool adcDone;
//volatile boolean is_timeout = false;
volatile unsigned int current_state = WAIT_STATE;
//unsigned long my_timeout = 5000000;
//char my_oldSREG;    //to hold status register while ints are disabled
unsigned long SensDist = 100000; //Distance between sensors in meters*1000000 (328084)
float pWeight; //Pellet weight in grains
int weightTens; //first two digits of pWeight
int weightDec; //Decimal part of Pellet weight
float mps = 0; //storage for meter per sec value
float fps = 0; //storage for feet per sec value
float joules = 0; //storage for feet per sec value
unsigned long duration = 0; // time between triger 1 and 2
//boolean firstShot = false; //flag to detect that a shot has been fired. remove adj. pellet weight
unsigned long timeout = 0; //timer to return to main screen after measurement 
const int menuTimeout1 = 20000; // time of inactivity to return to main screen after measurement

/*
MENU REQUIRED GLOBAL VARIABLES & DEFINITIONS
*/
#define MOVECURSOR 1  // constants for indicating whether cursor should be redrawn
#define MOVELIST 2  // constants for indicating whether cursor should be redrawn
byte totalRows = 2;  // total rows of LCD
byte totalCols = 16;  // total columns of LCD
unsigned long timeoutTime = 0;  // this is set and compared to millis to see when the user last did something.
const int menuTimeout = 5000; // time to timeout in a menu when user doesn't do anything.
volatile unsigned int menu_state;
unsigned long lastButtonPressed; // this is when the last button was pressed. It's used to debounce.
const int debounceTime = 300; //this is the debounce and hold delay. 


// initialize the LCD library with the numbers of the interface pins

LiquidCrystal lcd(5, 6, 7, 10, 11, 12);

void setup () {
  Serial.begin(9600);
  Serial.println("Chrono v2.00");
  //Retreive pellet weight
  weightTens = EEPROM.read(0);
  weightDec = EEPROM.read(1);
  pWeight = weightTens + float(weightDec)/100;
  
//   EEPROM_readAnything(0, Config);
   Serial.print("EEPROM 0 = ");
   Serial.println(EEPROM.read(0));
   Serial.print("EEPROM 1/100 = ");
   Serial.println(float(EEPROM.read(1))/100);
   Serial.print("pWeight  = ");
   Serial.println(pWeight);
   Serial.print("weightTens = ");
   Serial.println(weightTens);
   Serial.print("weightDec = ");
   Serial.println(weightDec);
   
  pinMode(Trip_in_1, INPUT); //declare pins in use
  pinMode(Trip_in_2, INPUT);
  pinMode(UP, INPUT);
  pinMode(DOWN, INPUT);
  pinMode(OK_BTN, INPUT);
  digitalWrite(UP, HIGH);  //turn on pullup resistor for buttons
  digitalWrite(DOWN, HIGH);
  digitalWrite(OK_BTN, HIGH);
   
  lcd.begin(16, 2); //2 line 16 character LCD
  lcd.clear();
    
  lcd.setCursor(5, 0);  //Draw splash screen
  lcd.print("Chrono");
  lcd.setCursor(4, 1);
  lcd.print("Ver 2.00");
  delay (3000); //show splash screen for 5 seconds
  defaultScreen();
  reset_variables();

}


void loop () {
  
  switch (current_state) {
  case WAIT_STATE:
    if(digitalRead(Trip_in_1)==LOW) {
      Trigger_1 = micros();
      current_state = MONITOR_STATE;
      }

    if (digitalRead(OK_BTN)==LOW) {

        basicMenu();
        delay(200);
        Serial.println("back in main loop");
        defaultScreen();

      }


   if (timeout != 0 && timeout<millis()){  // user hasn't done anything in awhile
      defaultScreen();
      timeout = 0;  
      }
      
    break;
    
  case MONITOR_STATE:
    while(digitalRead(Trip_in_2)==HIGH);  //loop until the Trigger goes LOW  && !is_timeout
    
      Trigger_2 = micros();
      //Serial.println("Detected exit");
    current_state = OUTPUT_STATE;
    break;
  case OUTPUT_STATE:
    
    output_serial_info();
    reset_variables();
    timeout = millis() + menuTimeout1;
    current_state = WAIT_STATE;
    
    break;
  }
}

void output_serial_info() {

  //firstShot = true; //First shot has been fired so clear pellet weight adj. message
  lcd.clear();
  mps = float(SensDist)/(Trigger_2 - Trigger_1);
  mps = constrain(mps,0,999); //limit mps tp plus value upto 999
  fps = mps*3.28084;
  joules = ((pWeight*0.00006479)*(mps*mps))/2;
  //Send data to LCD
  
  if (mps<999.9) {  //Check for large no.
    
    Serial.print("m/s: ");
    Serial.println(int(mps));
    Serial.print("fps: ");
    Serial.println(int(fps));
    
    lcd.setCursor(0, 0);
    lcd.print("m/s:");
    lcd.print(int(mps));
    
    lcd.setCursor(0, 1);
    lcd.print("fps:");
    lcd.print(int(fps));
    
    
    lcd.setCursor(9, 0);
    lcd.print("joules:");
    lcd.setCursor(11, 1);
    lcd.print(joules);
  }

}

void reset_variables() {
  Trigger_1 = 0;
  Trigger_2 = 0;

}

/*
MENU ROUTINE
*/
void basicMenu(){

  byte topItemDisplayed = 0;  // stores menu item displayed at top of LCD screen
  byte cursorPosition = 0;  // where cursor is on screen, from 0 --> totalRows. 

  // redraw = 0  - don't redraw
  // redraw = 1 - redraw cursor
  // redraw = 2 - redraw list
  byte redraw = MOVELIST;  // triggers whether menu is redrawn after cursor move.
  byte i=0; // temp variable for loops.
  byte totalMenuItems = 0;  //a while loop below will set this to the # of menu items.

// Put the menu items here. Remember, the first item will have a 'position' of 0.
  const char* menuItems[]={
    "Set Pweight", 
    "Exit",
    "",
  };

  while (menuItems[totalMenuItems] != ""){
    totalMenuItems++;  // count how many items are in list.
  }
  totalMenuItems--;  //subtract 1 so we know total items in array.

  lcd.clear();  // clear the screen so we can paint the menu.

  boolean stillSelecting = true;  // set because user is still selecting.
  boolean set_pweight = true;
  boolean enter_menu = true;

  timeoutTime = millis() + menuTimeout; // set initial timeout limit. 

  do  {  // loop while waiting for user to select.
 

    /*
    IF YOU WANT OTHER CODE GOING ON IN THE BACKGROUND
    WHILE WAITING FOR THE USER TO DO SOMETHING, PUT IT HERE
    */
    
 
    switch(read_buttons()) {  // analyze button pressed response. Default is 0.
    


      case 1:  // EQUIVALENT OF 'UP' BUTTON PUSHED
  
        timeoutTime = millis()+menuTimeout;  // reset timeout timer
        //  if cursor is at top and menu is NOT at top
        //  move menu up one.
        if(cursorPosition == 0 && topItemDisplayed > 0)  //  Cursor is at top of LCD, and there are higher menu items still to be displayed.
        {
          topItemDisplayed--;  // move top menu item displayed up one. 
          redraw = MOVELIST;  // redraw the entire menu
        }
  
        // if cursor not at top, move it up one.
        if(cursorPosition>0)
        {
          cursorPosition--;  // move cursor up one.
          redraw = MOVECURSOR;  // redraw just cursor.
        }
        break;

      case 2:    // EQUIVALENT OF 'DOWN' BUTTON PUSHED
  
        timeoutTime = millis()+menuTimeout;  // reset timeout timer
        // this sees if there are menu items below the bottom of the LCD screen & sees if cursor is at bottom of LCD 
        if((topItemDisplayed + (totalRows-1)) < totalMenuItems && cursorPosition == (totalRows-1))
        {
          topItemDisplayed++;  // move menu down one
          redraw = MOVELIST;  // redraw entire menu
        }
        if(cursorPosition<(totalRows-1))  // cursor is not at bottom of LCD, so move it down one.
        {
          cursorPosition++;  // move cursor down one
          redraw = MOVECURSOR;  // redraw just cursor.
        }
        break;

      case 4:  // EQUIVALENT TO 'SELECT' OR 'OKAY' BEING PUSHED 

        timeoutTime = millis()+menuTimeout;  // reset timeout timer
      
        switch(topItemDisplayed + cursorPosition){ // adding these values together = where on menuItems cursor is.
            
            //  put code to be run when specific item is selected in place of the Serial.print filler.
            // the Serial.print code can be removed, but DO NOT change the case & break structure. 
            // (Obviously, you should have as many case instances as you do menu items.)
            
            case 0:  // menu item 1 selected
              Serial.print("Menu item ");
              Serial.print(topItemDisplayed + cursorPosition);
              Serial.print(" selected - ");
              Serial.println(menuItems[topItemDisplayed + cursorPosition]);
              if(enter_menu == false){
                lcd.clear();
                lcd.setCursor(0,0);
                lcd.print("Pellet weight:");
                lcd.setCursor(1,1);
                lcd.print(pWeight);
                Serial.println(pWeight);
                  do{
                      switch(read_buttons()){
                      case 1:
                        pWeight = pWeight + 0.01 ;
                        weightDec = (pWeight - int(pWeight))*100;
                        lcd.setCursor(1,1);
                        lcd.print(pWeight);
                      break;
                      
                      case 2:
                        pWeight = pWeight - 0.01 ;
                        weightDec = (pWeight - int(pWeight))*100;
                        lcd.setCursor(1,1);
                        lcd.print(pWeight);
                      break;
                      
                      case 4:
                      delay(200);
                      EEPROM.write(0,int(pWeight));
                      EEPROM.write(1,weightDec);
                      set_pweight = false;
                      break;
                      }
                  }while (set_pweight == true);
              }
              
                   
              //  to have nested menus,copy this function(i.e. all of basicMenu) into a new function

              enter_menu = false;
              break;
      
            case 1:  // menu item 2 selected
              Serial.print("Menu item ");
              Serial.print(topItemDisplayed + cursorPosition);
              Serial.print(" selected - ");
              Serial.println(menuItems[topItemDisplayed + cursorPosition]);        
              stillSelecting = false;
              Serial.println("exit");
              
             
              // add as many "case #:" as items you have. You could put 
              //  line separators in menuList and leave out the 
              //  corresponding case, which would mean that nothing
              // would be triggered when user selected the line separator.  
   
            break;
            
            }
      
      case 8:  //  button was pushed for long time. This corresponds to "Back" or "Cancel" being pushed.
//      stillSelecting = false;
//      Serial.println("Button held for a long time");
      break;

    }
    
    
    if(stillSelecting == true){
    switch(redraw){  //  checks if menu should be redrawn at all.
    case MOVECURSOR:  // Only the cursor needs to be moved.
      redraw = false;  // reset flag.
      if (cursorPosition > totalMenuItems) // keeps cursor from moving beyond menu items.
        cursorPosition = totalMenuItems;
      for(i = 0; i < (totalRows); i++){  // loop through all of the lines on the LCD
        lcd.setCursor(0,i);
        lcd.print(" ");                      // and erase the previously displayed cursor
        lcd.setCursor((totalCols-1), i);
        lcd.print(" ");
      }
      lcd.setCursor(0,cursorPosition);      // go to LCD line where new cursor should be & display it.
      lcd.print(">");
      lcd.setCursor((totalCols-1), cursorPosition);
      lcd.print("<");
      break;  // MOVECURSOR break.

    case MOVELIST:  // the entire menu needs to be redrawn
      redraw=MOVECURSOR;  // redraw cursor after clearing LCD and printing menu.
      lcd.clear(); // clear screen so it can be repainted.
      if(totalMenuItems>((totalRows-1))){  // if there are more menu items than LCD rows, then cycle through menu items.
        for (i = 0; i < (totalRows); i++){
          lcd.setCursor(1,i);
          lcd.print(menuItems[topItemDisplayed + i]);
        }
      }
      else{  // if menu has less items than LCD rows, display all available menu items.
        for (i = 0; i < totalMenuItems+1; i++){
          lcd.setCursor(1,i);
          lcd.print(menuItems[topItemDisplayed + i]);
        }
      }
      break;  // MOVELIST break
    }

    if (timeoutTime<millis()){  // user hasn't done anything in awhile
      stillSelecting = false;  // tell loop to bail out.
      defaultScreen();  
      
      /*
      in my main code, I had a function that
       displayed a default screen on the LCD, so
       I would put that function here, and it would
       bail out to the default screen.
       defaultScreen();
       */
      }
    } 
  }


  while (stillSelecting == true);  //
  Serial.println("EXIT");

}

void defaultScreen(){
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Ready");
lcd.setCursor(7,1);
lcd.print("OK > menu");
  
}


int read_buttons(){  // you may need to swap "void" with "int" or "byte"
  byte returndata = 0;
  int buttonState; 
  // *** REMEMBER to declare buttonUp, buttonDown, buttonSelect, & buttonCancel pins

  if ((lastButtonPressed + debounceTime) < millis()){  // see if it's time to check the buttons again
    
    // read Up button
    buttonState = digitalRead(UP);
    if (buttonState == LOW){
      returndata = returndata + 1;
      lastButtonPressed = millis();
    }

    // read Down button
    buttonState = digitalRead(DOWN);
    if (buttonState == LOW){
      returndata = returndata + 2;
      lastButtonPressed = millis();
    }

    // read Select button
    buttonState = digitalRead(OK_BTN);
    if (buttonState == LOW){
      returndata = returndata + 4;
      lastButtonPressed = millis();
    }

    // read Cancel button
//    buttonState = digitalRead(buttonCancel);
//    if (buttonState == LOW){
//      returndata = returndata + 8;
//      lastButtonPressed = millis();
//    }
  }
  return returndata; // this spits back to the function that calls it the variable returndata.
}

Credits

Pepisan

Pepisan

1 project • 3 followers

Comments