MyRaspberryAndMe

Tinkering with Raspberry (and other things)

Nerf Barrel Extension + AmmoCounter + Chrono

Leave a comment

OK, so now I am into Nerf-Guns. Seems my 8-year old alter ego has taken over. But, following the motto from the legendary eevblog, don’t turn it on, take it apart!

These Nerf blasters come with different technologies. There are purely mechanical ones, powered by springs and compressed air, and then there are some that use motors and flywheels to accelerate the funny foam darts. My first Nerf was a “Recon MK II”, mechanical. As soon as I held it in my hands I wanted to add an ammo counter (Remember: I am old and the Alien movies, especially the M41A pulse rifle, are part of my life…).
The question is how to add something electrical to a purely mechanical thingie. And I had some constraints: it has to be reversible and it needs to remain child-safe for the occasional battle with kids. So the voltage/motor/whatever modifications found on the internet were a no-go.
Well, the removable barrel extension seemed to offer enough space to integrate some circuitry…Here’s the plan: Use an Arduino Mini Pro, some pushbuttons and a phototransistor and put everything into the barrel extension. It took me some weeks to figure out how to pack everything into the barrel extension but finally it worked out great. Here’s an image of the internals:

Parts needed and the initial idea

The Alien ammo counter uses two 7-segment displays but these were too big to fit in so I opted for a tiny OLED display with a 128×32 pixel resolution. The display uses an SSD1306 controller and communicates with the Arduino via I2C. There are plenty of libraries for these kind of displays, of course there is one from Adafruit, too. I opted for a tinier version I found while researching, namely the u8g2lib. The displays can be acquired from ebay, just pay attention that you are ordering the i2c-enabled ones.
Then I needed two pushbuttons. One for selecting the magazine size, the second one to reset the counter, aka “reload”. Detecting a shot is done with a phototransistor mounted on the barrel. I am using the TCRT5000, it has an infrared LED and a phototransistor in a small housing and can be connected directly to an Arduino digital input. Here is an image of the sensor taken from the datasheet:

Everything is powered from a 9 Volt battery, thus an Arduini Mini Pro (5V, 16MHz) is used because the 9V battery can be connected directly to its V_in pin. The display needs 5V which is taken from the VCC output on the Arduino Mini Pro. As the display does not draw too much current, I measured values between 15 and 30 mA, everything is within the specification of the Arduino board.

The schematic is pretty straighforward. Connect the buttons to digital inputs, using the internal pull-ups of the Arduino, and connect the phototransistor to another digital pin. The display is connected to SDA, SCL and power. Very easy:

One step further: Measure dart speed (the “chrono”)

As soon as I had developed the initial idea and a working prototype I thought it should be possible to measure the speed of the dart as it passes the phototransistor. The darts move pretty quick so it was obvious that I had to use some interruot routines to make this happen. There is a very good series on Arduino interrupts on the Gammon Forum, especially the camera shutter speed example that I was able to use without any alterations. Credits go to Nick Gammon.

While I was planning and building prototypes I started reading in some Nerf forums. It’s incredibly what makers do with these toys. As it was to be expected, I have not been the first person with the idea of adding an ammo counter (a lot of people know Aliens…). And I came to the conclusion that (if someone from these forums were to read this blog entry) the accuracy of the speed measurement was to be questioned. So here’s some math:

Accuracy of speed measurement

Let’s assume all darts are exactly 72mm long (they should be) and they are moving at 100 km/h. If the measurement at this speed would be possible with an Arduino it can be assumed that it is accurate for much lower speeds and/or dart lengths that differ by some millimeters.
100 km/h translates to (rounded) 2780 m/s. So a dart with a length of 7.2 cm will pass a fixed position (the phototransistor) in 2.5 ms, that is 2500 microseconds.
The Arduino is running at 16 MHz, that means 16 000 000 clock cycles per second. So one cycle has a duration of 1/16000000, that is 62.5 ns (nano seconds). Or, as we are talking in the microsecond range, the Arduino runs 16 clock cycles per microsecond.
Our dart at 100 km/h (and I strongly believe only Adam Savage could build such a Nerf blaster) needs 2500 microseconds to pass the phototransistor. The Arduino is running 40000 cycles in this amount of time. As most instructions need 1-2 cycles (returns from functions need 4 cycles) the Arduino should habe enough time to process any overhead. Or, in other words: measuring the microseconds between entering and leaving the phototransistor even at 100 km/h are expected to be exact.
With lower dart speeds there is even more time, thus I am confident the speed measurement would be pretty accurate. And I am going to round it to full meters per second, no decimals.

Putting it all together

Cramming all the parts into the barrel extension needed some planning. I wanted to be able to dissasemble the barrel without needing to cut cables or other connections. And the Arduino Mini Pro should be detachable for updating the software. I made heavy use of shrinking tube and soldered the resistors R1 and R2 directly to the TCRT5000. The following images show the details:

This is the reassembled barrel extension after a shot has been fired:It shows a speed of 19 m/s which is about 68 km/h. This is pretty accurate, as reviews of the Recon MKII blaster mentions speeds of about 66 FPS (feet per second) and this translates to 20 m/s. So the more theoretical accuracy derivation from above proves correct.

The code and functionality

After much thinking and prototyping everything turned out to be pretty straightforward. Pressing the SELECT-button toggles through some predefined magazine sizes (6, 12, 15, 18, 25, 35 and 50). Holding the RELOAD-button and the pressing the SELECT-button increases the size by one, thus enabling the selection of individual magazine sizes.
Firing a dart decreases the number of darts, when zero darts are left, the display shows “RELOAD” and pressing the “RELOAD”-button resets the counter to the selected value so that the count-down can start again.
The small bar on the right bottom of teh display is a graphical representation of the darts left in the magazine.

Dart speed is measured with an interrupt routine attached to the signal pin and programmed to detect a signal change. When the dart enters the detector the interrupt triggers and the current microsecond timestamp is saved. On leaving (signal changes again) the interrupt is triggered again and the microsecond timestamp is saved again.
The dart speed in m/s is then simply calculated as v=72000/(end_time – start_time).

/**
 * AmmoCounter V2
 * internal pullups can be used
 *
 * (c)2017 mypiandme@gmail.com
 *
 * You may use this code for your own projects but you have to
 * credit my copyright.
 * You must not use this code or parts of it for any commercial
 * product!
 * 
 */


#include <avr/pgmspace.h>
#include <Arduino.h>
#include <U8g2lib.h>

#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif

#include "icon_gun.h"


U8G2_SSD1306_128X32_UNIVISION_2_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);  // Adafruit ESP8266/32u4/ARM Boards + FeatherWing OLED

#define INTERNAL_PULLUP

#ifdef INTERNAL_PULLUP
  #define BTN_DOWN 0    // when internal pull-up is used
  #define BTN_UP   1
#else 
  #define BTN_DOWN 1
  #define BTN_UP   0
#endif

char ammo[3];
char vDart[10];



int clipSizes[] = {6,12,15,18,25,35,50};
int clipSizeSelected = 0;   // default to 6 dart clip
int maxClip = clipSizes[clipSizeSelected];

int roundsLeft = maxClip;

int sigPin = 3;
volatile int lastSignal;
bool fired = false;
bool empty = false;

int reloadPin = 12;
int reloadRead = 0;
int selectPin = 11;
int selectRead = 0;

volatile boolean started;
volatile unsigned long startTime;
volatile unsigned long endTime;

unsigned long speedDisplayTime = 2500; // time in ms
volatile unsigned long startDisplay;
bool speedOn = false;

unsigned int selectDisplayTime = 2000;
volatile unsigned long selectStart;
bool selectOn = false;

#define LANDSCAPE 1
#define PORTRAIT  0



void setup(void) {
// interrupt setup
  u8g2.begin();
  u8g2.setFont(u8g2_font_pxplusibmvga8_tr);
  #ifdef INTERNAL_PULLUP
    pinMode(reloadPin, INPUT_PULLUP);
    pinMode(selectPin, INPUT_PULLUP);
  #else
    pinMode(reloadPin, INPUT);
    pinMode(selectPin, INPUT);
  #endif
  
  attachInterrupt( digitalPinToInterrupt(sigPin), detect, CHANGE);
  drawScreen();
}

void loop(void) {

// interrupt loop
  if( !selectOn && !empty && endTime ) {
    // shot has been fired
    roundsLeft--;
    drawScreen();
    endTime = 0;
  }
  if( roundsLeft == 0 ) {
    drawReloadIcon();
    empty = true;
  }

  reloadRead = digitalRead(reloadPin);
  selectRead = digitalRead(selectPin);
  
  if( selectRead==BTN_DOWN ) {
    selectClip();
    delay(170); // poor man's debounce
  }
  
  if( !selectOn && reloadRead==BTN_DOWN ) {
    empty = false;
    roundsLeft = maxClip;
    reloadRead = BTN_UP;
    drawScreen();
    endTime = 0;
  }

  // check if select display should be cleared
  // which means that selection is confirmed.
  // assume that a reload occures when selecting rounds
  if( selectOn && (millis()-startDisplay) > selectDisplayTime ) {
      roundsLeft = maxClip;
      selectOn = false;
    drawScreen();
  }
  
//   check if speed display should be cleared. If commented out, speed display stays on.
//  if( !empty && speedOn && (millis()-startDisplay) > speedDisplayTime ) {
//    drawScreen();
//  }
}

void detect() {
  if( started ) {
    endTime = micros();
  }
  else {
    startTime = micros();
  }
  started = ! started;
}

void selectClip() {
  if( selectOn && reloadRead==BTN_UP ) {
    clipSizeSelected++;
    if( clipSizeSelected==(sizeof(clipSizes)/sizeof(int) )) {
      clipSizeSelected=0;
    }
    maxClip = clipSizes[clipSizeSelected];
  }
  else if ( selectOn && reloadRead==BTN_DOWN ) {
    maxClip++;
    if ( maxClip>99 ) maxClip=1;
  }
  drawSelectedClip();
  

  selectOn = true;
  startDisplay = millis();
}


/**
  * most drawing routines exist as a loop and frame variant.
  * This is because th u8g2 lib offers different drawing modes that require
  * different amounts of memory. This lets me choose how much memory I am reserving
  * for the library based on the Arduino/Atmel I am using
**/

void drawSelectedClip() {
  drawSelectedClipLoop();
}

void drawSelectedClipLoop() {
  int cs = maxClip; //clipSizes[clipSizeSelected];
  int newSizeH = cs/100;
  int newSizeT = (cs-100*newSizeH)/10;
  int newSizeO = (cs-100*newSizeH-10*newSizeT);
  u8g2.firstPage();
  do {
    u8g2.drawXBMP( 0,0, sel_width, sel_height, sel_bits);  //display "S" for select
    u8g2.drawXBMP( 75,0, seg_width, seg_height, segments[newSizeT]);
    u8g2.drawXBMP( 95,0, seg_width, seg_height, segments[newSizeO]);
  } while(u8g2.nextPage() );
}

void drawScreen() {
  drawScreenLoop();
}

void drawReloadIcon() {
  drawReloadLoop();
}

void drawScreenFullBuffer() {
  u8g2.clearBuffer();
  draw();
  u8g2.sendBuffer();
}

void drawScreenLoop() {
  u8g2.firstPage();
  do {
      draw();
  } while (u8g2.nextPage() );
}

void drawReloadLoop() {
  u8g2.firstPage();
  do {
       drawReload();
  } while (u8g2.nextPage() );
}
void drawReloadFull() {
  u8g2.clearBuffer();
  drawReload();
  u8g2.sendBuffer();
}

void draw() {
  drawRounds();
  drawAmmoBox(50, 25, 75, 5, LANDSCAPE);
  speedOn = false;
  if(endTime) {
    drawSpeed(80, 15);
    startDisplay = millis();
    speedOn = true;
  }
}

void drawSpeed(int x, int y) {
  unsigned long interval = (endTime-startTime); // micro seconds 1/1,000,000 s
  unsigned long v = 72000/interval; // dart size = 72mm = 0.072m; 
  sprintf(vDart,"%02i m/s", v);
  u8g2.drawStr(x,y, vDart);
}

void drawRounds()
{
  int right = roundsLeft % 10;
  int left = roundsLeft/10;
  u8g2.drawXBMP( 0,0, seg_width, seg_height, segments[left]);
  u8g2.drawXBMP( 22,0, seg_width, seg_height, segments[right]);
}

void drawReload() {
  u8g2.drawXBMP(0,0, reload_width, reload_height, reload_bits);
}

void drawAmmoBox() {
  int height =32*roundsLeft/maxClip;
  u8g2.drawBox(80,31-height,20,height);
  u8g2.drawFrame(80,0,20,31);
}

void drawAmmoBox(int x, int y, int dx, int dy, int orientation) {
  u8g2.drawFrame(x,y,dx,dy);
  if( orientation == PORTRAIT ) {
    int height = dy*roundsLeft/maxClip;
    u8g2.drawBox(x, dy-height, dx, height);
  }
  if( orientation == LANDSCAPE ) {
    int height = dx*roundsLeft/maxClip;
    u8g2.drawBox(x, y, height, dy);
  }
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s