Arduino Talking Defect Detector
d. bodnar   revised 1-27-2015

Questions?  email info@trainelectronics.com

Video One - human voice

Direct link to video: http://youtu.be/jQUcPKKRVdA

 

Video Two - machine voice & menus & radio transmitter

 

Direct link to video:http://youtu.be/2arDkxARiP4

 

 

Introduction
I have been experimenting with a small MP3 player (see: Arduino MP3 Talking Temperature ) and thought it would be an ideal device to incorporate into a model railroad defect detector simulator.  As you may know, defect detectors are installed along the side of railroad tracks and check for hot bearings on the trucks of each locomotive and car.  For more information see:  http://en.wikipedia.org/wiki/Defect_detector 

The primary objectives of this project are to:

  • Design and install detectors that can be placed along a model railroad track

  • Design and build a microcontroller based circuit that can manage these sensors and use the data from them to report:

    1. ambient temperature

    2. axle count

    3. train speed

    4. train length (first axle to last axle)

    5. randomly report a defect

  • Write and test code to do the above

  • Incorporate a speaking report capability

  • Incorporate a 2 line x 16 character LCD display

  • Allow for end-user modifications of scale, sensor spacing, random report ratio, etc 

  • Report accurately on trains running in either direction

 

Hardware
The microcontroller is an Arduino Pro Mini. 
These small devices are available on eBay and from other sources for $5.00 or less The MP3 player is also available from eBay for $5.00 or less.  Be sure to find the module that is almost square in shape and that supports the Arduino and serial communication. 

Please note: If you have intermittent operation or excessive noise make sure that you are supplying 5 volts at 0.5 amps and have installed the 1K resistors on the TX (Arduino) to RX (DFPlayer) connection.

Trackside Laser Sensors
The trackside sensors are composed of a small
red laser and a phototransistor.  Two of these sensors are needed to determine the train's speed.  It is important that the lasers beam is adjusted so that it just clears the top of the rail.  If it is any higher it may not properly detect just the passing wheels and incorrectly add to the count when it encounters hanging coupler parts.  The holder shown here is 3D printed (files are here is SketchUp and STL format: Arduino/DefectDetector/laser_holderX4-D.skp Arduino/DefectDetector/laser_holderX4-D.stl) and holds the two lasers and sensors 1" apart.

This overhead view shows the lasers (bottom) and the phototransistors (top).  If you look carefully you can see the laser beams hitting the bottom rail.

This view is from the lasers and clearly shows the alignment of the beams as they hit the tops of the rails and the phototransistors.  The plastic track has been notched to help with alignment.

 

The phototransistors are mounted on a small piece of circuit board.  The emitters (by the tab) are at the top and the collectors are at the bottom.

The back of the board shows the connections to the 3 wire cable that goes to the main circuit board.  I used two servo cables to connect the photo transistors and the lasers to the circuit.

Here you can see the laser beam hitting each phototransistor.

The notched plastic track allows the sensor unit to fit under the track.  Other accommodations for sensor placement would be needed if other track installation are used.

 

The Circuit
The schematic is shown here.  Even though the Pro Mini version of the Arduino is shown you could easily use an Uno or other Arduino.

The library that I used for the 2 line x 16 character LCD calls for the SDA and SLC  lines to be connected to A5 and A4, respectively.  These connections are not on the 24 pins on the edges of the Pro Mini.  The circuit board that is shown below has holes for two different placements of A4 and A5.

 

Circuit Board
A custom circuit board has been designed.

 



There is one modification that needs to be made on the board.  While a direct connection between the TX/RX pins on the Arduino and the DFPlayer will work there is a good chance that noise may accompany the sounds coming from the speaker.  If a 1K resistor is inserted between the TX pin on the Arduino and the RX pin on the MP3 board this noise is squelched.  The easiest way to do this is shown below.

The trace between the pins needs to be cut as shown by the yellow circle.  The pins that are connected by the 1K resistor are circled in red. 

Also note that the back of the board improperly identifies the TX and RX pins - TX is labeled Pin1 and RX is labeled TX.  These pins are properly identified on the silkscreen on top of the board.

Laser Alignment
The trickiest part of setting up this unit is to properly align the laser beams and the phototransistors so that the laser just clears the top of the rail.  This allows the unit to detect just wheels while ignoring other parts of the cars and engines.  To facilitate alignment the detector checks for the presence of a laser beam hitting each phototransistor.  If either beam is missing the program enters an alignment routine that shows a 1 or a 0 for each beam.   This screen shows that the laser on the left is OK (shows a "1") and the right laser is blocked or misaligned.

Adjust the lasers so that two 1's are shown. Once that is done the routine counts up to 100 before exiting to the normal program.

Here the two lasers are properly adjusted and the blurry number in the upper right (shows 59) is rapidly counting to 100 at which point it will exit.  If either beam is broken before this hits 100 the count will reset to zero.

During testing I had to bend a few of the bent wires extending from the bottom of couplers as they extended below the rail.  I also had some erroneous reading caused by a car pulling along a bit of my dog's hair - yes, the laser detectors are that sensitive!  Anything that extends below the rail will be counted as an axle.

Planned Enhancements
Since real defect detectors report their findings by radio I plan on connecting the unit to a small audio transmitter and broadcasting the report to a radio receiver. 
Setup and Use
There are four buttons that I connected to the defector detector.  They are shown here on one of my prototypes.  The button on top (circled in blue) is the MENU button.  The one to the right is UP or ON and the one to its left is DOWN or OFF.  The button in the upper left (circled in yellow) is a reset button that is optional as Arduinos typically have one on the circuit board.  I found it convenient to have it on the front of the unit during development.

 

When the unit is powered on it displays a few identification screens then flashes MENU = SETUP! a number of times.  If you press the MENU button while this is happening you will enter the setup menus.

A number of options are displayed one after the other.  All are controlled by the three buttons.  The button to the left is labeled on the screen as either OFF or Down depending on the menu item.  The button on the right is labeled ON or UP and the center button is the MENU button that moves you from one item to the next.

Mile Marker - the mile marker that is spoken has a value that can range from 000.1 miles to 999.9 miles.  Each of these four digits is entered individually by pressing the UP or DOWN buttons.  Press MENU when done with a digit.  For example, to change the hundreds digit (the 8) to a 3 press DOWN five times.  Then press MENU to move to the 10's digit.

Similarly you can change the 3 in the 10's place to anything from zero to nine.

Next is the 1's digit....

then the 1/10ths digit.

 

The Defects option determines the likelihood of hearing a defect reported.  In this screen it is set to 50% meaning that about 1/2 of the time a defect will be reported.  Change this to a smaller number to have defects reported less frequently or increase it to have them pop up more often.  Set this value to zero to have no defects reported.

The scale is shown as 1:48 (O-scale).  Press the UP or DOWN buttons to change the scale.

 

If you want to have the detector silent set this option to OFF.  LCD display items such as axle count will still be shown.

The next few screens allow you to turn a specific part of the detector report on or off.  Press ON or OFF to activate or deactivate each.  Note that you do not need to press MENU after selecting ON or OFF but will automatically be moved to the next item.

 

 

 

These continue to include RR Name, Defect Det, Mile Marker and Axles.  When done "Ready" is displayed and the detector is set to operate.

Note that all of the setting that were selected are written to memory and saved.

 

 

Software
The software is a work in progress.  This version reports axle count, speed, length and temperature.   Reports go to the LCD display, the serial monitor and the MP3 board.

The program is fairly well commented and contains a good bit of debugging code.

DFPlayer_MP3_2lineLCD_18b20_Lasers_Ver_8_8_defects

 
/*
 d. bodnar
 1-26-15 
 TODO:  
 1. OK add menu to select scale
 2.  add random defect reports (select %age) 
 3.  add 4th button to activate defect reports
 4.  record defect announcements
 U P Detector Milepost 2 8 Point 4 First Hotbox East Rail Axle 2 From End Of Train Temperature 7 3 Degrees Detector Out
 Hot Box on Axle 47, 
 first hot box east side axle 23   second hot box west side axle 12
 */
#include<EEPROM.h>
#include <SoftwareSerial.h>
#include <DFPlayer_Mini_Mp3.h>
#include <Wire.h>
#include <LCD.h>
#include <LiquidCrystal_I2C.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#define I2C_ADDR    0x27 // <<----- Add your address here.  Find it from I2C Scanner
#define BACKLIGHT_PIN     3
#define En_pin  2
#define Rw_pin  1
#define Rs_pin  0
#define D4_pin  4
#define D5_pin  5
#define D6_pin  6
#define D7_pin  7
#define ONE_WIRE_BUS 2
#define TEMPERATURE_PRECISION 9
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
int numberOfDevices; // Number of temperature devices found
DeviceAddress tempDeviceAddress; // We'll use this variable to store a found device address
int value = 0; // 
float xtemp=0;
byte buffer[10];
LiquidCrystal_I2C	lcd(I2C_ADDR,En_pin,Rw_pin,Rs_pin,D4_pin,D5_pin,D6_pin,D7_pin);
char buf[12]; 
const int buttonPin = 3;     // the number of the pushbutton pin
const int buttonPin2 = 4;    // second button pin 
const int menuPin = A3;     // analog pin to Pot 1
const int ledPin =  13;      // the number of the LED pin
int buttonState = 0;         // variable for reading the pushbutton status
int buttonState2 = 0;      // same for 2nd button
int buttonStateTemp = 0;         // variable to temporarily remember which button was hit first
int buttonState2Temp = 0;      // same for 2nd button
int buttonStateMenuTemp=0;
int buttonStateMenu = 0;   // same for menu button on A3
int buusyPin = 10;// buusyPin = 10; // sound player busy
int bsy = 0;
int z=0;
int PTState;
int Laser = 11; // laser control 
int PhotoTransistor = 9;
int PTState2;
int Laser2 = 12; // laser control 
int PhotoTransistor2 = 8;
int PTFlag = 0;
int PTFlag2 = 0;
int axleCount = 0;
int axleFlag = 0;
long tempF=0;
char tempFString(12);
String thisString(12);
unsigned long time=0;
unsigned long axleTime=0; // time between axle hits -  also used to compute train legth
int reportedTime=0;
unsigned long saveSpeed=0;
int mute=0; //  =0 if talking, =1 if muted  
int sayTemperature=0; //  =0 if speaking temperature, =1 if muted  
int sayMileMarker=0; //  =0 if speaking mile marker, =1 if muted  
int sayLength=0; //  =0 if speaking lentgh, =1 if muted  
int saySpeed=0; //  =0 if speaking speed, =1 if muted  
int sayAxles=0; //  =0 if speaking axles, =1 if muted 
int sayRR=0; //  =0 if speaking RR, =1 if muted  
int sayDD=0; //  =0 if speaking Defect Detector, =1 if muted  
int mileMarkerW = 743;  //NOTE - this is the whole # portion
int  mileMarkerWh=0; // high byte of mile marker whole number
int  mileMarkerWl=0; // low byte of mile marker whole number
int mileMarkerD = 9;  // NOTE - this is the decimal portion - one digit
int hDigit = 0; // hundreds digit
int tDigit = 0; // tens digit
int oDigit = 0; // ones digit
unsigned long mph = 0; // speed in MPH
int scale = 96;
int defectOnOff=0;  // report defects- 0=never otherwise a percentage to 100
int defectFlag=0; // do defect or not based on comparison with defect ratio
unsigned long mphScale = 0;
unsigned long speedConstant = 361054347; // adjust to fine tune speed
unsigned long lengthTime = 0;
unsigned long trainLength = 0;
unsigned long lengthStartTime;

//***************************************SETUP*************************************
void setup () {
  pinMode(Laser, OUTPUT);
  pinMode(PhotoTransistor, INPUT);
  pinMode(Laser2, OUTPUT);
  pinMode(PhotoTransistor2, INPUT);
  pinMode(buusyPin, INPUT);
  pinMode(ledPin, OUTPUT);      
  pinMode(buttonPin, INPUT);   
  pinMode(buttonPin2, INPUT);   
  pinMode(menuPin, INPUT);   
  Serial.begin (9600);
  lcd.begin (16,2); //  LCD is 16x2
  lcd.setBacklightPin(BACKLIGHT_PIN,POSITIVE);// Switch on the backlight
  lcd.setBacklight(HIGH);
  lcd.home (); // go home
  lcd.print("  MP3 Player");    
  delay(200);
  lcd.setCursor(0,1); // start of line 2
  lcd.print("  Version 8.8  ");
  delay ( 500);
  mp3_set_serial (Serial);	//set Serial for DFPlayer-mini mp3 module 
  mp3_reset();
  mp3_set_volume (30);          // does not appear to make any difference
  mp3_reset();
  delay (400); 

  readEEPROM();
  lcd.home (); // go home
  lcd.clear();
  lcd.print("trainelectronics");    
  lcd.setCursor(0,1);
  lcd.print("      .com");    
  delay(1400);
  lcd.clear();
  lcd.setCursor(0,1);
  lcd.print(" 2015 d. bodnar");
  delay ( 1500);
  //Menu on power up if button held down
  for (int xxx=0; xxx < 10;xxx++){
    buttonStateMenu = digitalRead(menuPin);
    delay(200);
    lcd.clear();
    lcd.setCursor(0,(xxx % 2)) ; // switch cursor from line 0 to 1 based on xx being odd/even
    lcd.print("  MENU = Setup!");
    if(buttonStateMenu==1){   // 1 = pressed, 0= not pressed
      Serial.println("MENU!!!");
      readEEPROM();
      //   scale=96; // set once - REMOVE!
      lcd.clear();
      lcd.print("MENU!");
      do
      {
        buttonStateMenuTemp = (digitalRead(menuPin));// stay here till button released
      }
      while (buttonStateMenuTemp==1);  
      mainMenu();
    }
  }
  sensors.begin();
  numberOfDevices = sensors.getDeviceCount();
  if (sensors.isParasitePowerMode()) Serial.println("ON");
  else Serial.println("OFF");
  for(int i=0;i<numberOfDevices; i++)
  {
    if(sensors.getAddress(tempDeviceAddress, i))
    {
      printAddress(tempDeviceAddress);
      sensors.setResolution(tempDeviceAddress, TEMPERATURE_PRECISION);
    }
    else{
    }
  }  
  digitalWrite(Laser, HIGH);
  digitalWrite(Laser2, HIGH);
  sensors.requestTemperatures(); // Send the command to get temperatures
  printTemperature(tempDeviceAddress); // Use a simple function to print out the data
  alignLasers();  // go to routine to align lasers if either is blocked at startup
  lcd.clear();
  lcd.print("Ready");
}

//.......................................LOOP................................................
void loop () {
  // this section computes time between first sensor hit and first hit on other sensor - to compute speed
  PTState = digitalRead(PhotoTransistor);   
  PTState2 = digitalRead(PhotoTransistor2);
  // which is hit first?
  if (PTState == 0 && PTFlag == 0 && PTFlag2 == 0){  //1st sensor hit for the first time
    PTFlag =1;
    time =  micros();
    lengthStartTime= micros();
    digitalWrite(ledPin, HIGH); // connect to beeper
    lcd.clear();
    lcd.print("Axle Count = ");
    axleCount=1;
    lcd.setCursor(12,0);
    lcd.print(axleCount);
    axleFlag=1; // use this sensor to count axles
    do
    {
      delay(1);          // wait for sensors to stabilize
      PTState = digitalRead(PhotoTransistor); 
    } 
    while (PTState==0);  //stay here till axle clears
    digitalWrite(ledPin, LOW); // turn off beeper
  }
  else if(PTState2==0 && PTFlag2 == 0 && PTFlag == 0){  //2nd sensor hit for the first time
    PTFlag2=1;
    time =  micros();
    lengthStartTime= micros();
    digitalWrite(ledPin, HIGH);// connect to beeper
    lcd.clear();
    lcd.print("Axle Count=");
    axleCount=1;
    lcd.setCursor(12,0);
    lcd.print(axleCount);
    axleFlag=2; // use this sensor to count axles
    do
    {
      delay(1);          // wait for sensors to stabilize
      PTState2 = digitalRead(PhotoTransistor2); 
    } 
    while (PTState2==0);  //stay here till axle clears
    digitalWrite(ledPin, LOW); // turn off beeper
  }
  // use first hit on 2nd sensor to compute speed
  if(PTState ==0 && PTFlag2==1 && reportedTime==0){
    saveSpeed =  micros() - time;
    digitalWrite(Laser, LOW);
    //digitalWrite(Laser2, LOW);
    reportedTime=1;// indicates that both sensors have been hit & its OK to finalize an axle count
  }
  else if(PTState2 == 0 && PTFlag ==1 && reportedTime==0){
    saveSpeed =  micros() - time;
    //     digitalWrite(Laser, LOW);
    digitalWrite(Laser2, LOW);
    reportedTime=1;// indicates that both sensors have been hit & its OK to finalize an axle count
  }
  // Routines to count axles
  if (axleFlag==1 && PTState ==0 ){
    digitalWrite(ledPin, HIGH);// connect to beeper
    axleCount = axleCount++; 
    axleTime =  micros();// reset time after a hit on an axle
    lcd.setCursor(12,0);
    lcd.print(axleCount);
    do
    {
      delay(5);          // wait for sensors to stabilize
      PTState = digitalRead(PhotoTransistor); 
    } 
    while (PTState==0);  //stay here till axle clears
    digitalWrite(ledPin, LOW); // turn off beeper
  }
  if (axleFlag==2 && PTState2 ==0 ){
    digitalWrite(ledPin, HIGH);// connect to beeper
    axleCount = axleCount++; 
    axleTime =  micros();// reset time after a hit on an axle
    lcd.setCursor(12,0);
    lcd.print(axleCount);
    do
    {
      delay(5);          // wait for sensors to stabilize
      PTState2 = digitalRead(PhotoTransistor2); 
    } 
    while (PTState2==0);  //stay here till axle clears 
    digitalWrite(ledPin, LOW); // turn off beeper
  }
  // Finalize axle count after sufficient time has passed
  if ( reportedTime==1 && axleTime >= 1  && ( micros()-axleTime) >= 4000000){//4 sec & no axle hit =DONE!
    computeSpeed();  // in scale MPH
    speakRR(); // railroad name
    speakMM(); // Mile Marker
    speakDefects(); // Defect report
    speakAxles(); // # of axles
    speakLength(); // length of train
    speakSpeed(); // speed of train
    speakTemperature(); // ambient temperature 
    speakOut(); // say "out"
    axleTime=0; //reset variables after train passes
    PTFlag=0; //reset variables after train passes
    PTFlag2=0; //reset variables after train passes
    axleCount=0; //reset variables after train passes
    reportedTime=0; //reset variables after train passes
    digitalWrite(Laser, HIGH);
    digitalWrite(Laser2, HIGH);
  }
}
//...................................... END LOOP ........................................

// DO RAILROAD NAME & DEFECT DETECTOR
void speakRR(){
  // delay(1000);
  lcd.clear();
  lcd.print("Axles ");
  lcd.print(axleCount);
  if (mute ==0 && sayRR==0){
    mp3_play(16); // rr name..........................1
    dlayPrint();
    delay(200);
  }
  if (mute ==0 && sayDD==0){
    mp3_play(17); // defect detector..........................2
    dlayPrint();
    delay(200);
  }
}

// DO MILE MARKER  25..........................3
void speakMM(){
  if (mute ==0 && sayMileMarker==0){
    mp3_play(25); // mile marker
    dlayPrint();
    delay(200);
  }
  // DO MILE MARKER  number..........................4
  thisString =itoa(mileMarkerW, buf,10);
  if (mute ==0 && sayMileMarker==0){
    sayText();
    mp3_play(12);  // point
    delay(200);
    dlayPrint();
    delay(200);
    thisString = itoa(mileMarkerD,buf,10);
    sayText();
  } 
}

// DO DEFECTS 18..........................5
void speakDefects(){
  if (mute ==0){
    int rndDefectReport= random(1,100); // compare this to ratio to decide on reporting defect
    if (rndDefectReport <= defectOnOff){
      defectFlag =1; 
    }
    else{
      defectFlag = 0; 
    }
    if(defectOnOff==0 || defectFlag == 0){
      mp3_play(18); // no defects
      delay(1200);
      dlayPrint();
      delay(200);
    }
    else{   // 28=North, 29=South, 30=East, 31=West
      randomSeed(analogRead(0));
      int whichDirection = random(1,4);
      whichDirection = 27 + whichDirection; // choose n, s, e or west
      mp3_play(whichDirection); 
      delay(1200);
      dlayPrint();
      delay(200);
      long rndAxle= random(1,axleCount);
      mp3_play(rndAxle);
      delay(200);
      dlayPrint();
      delay(200);
    }
  } 
}

// do axle report..........................6
void speakAxles(){
  if (mute ==0&& sayAxles==0){
    thisString = itoa((axleCount), buf, 10);  
    delay(200);
    mp3_play(22);  //    22 axles
    dlayPrint();
    delay(200); 
    sayText();
  } 
}

//say length ..........................7
void speakLength(){
  computeLength();
  if (mute ==0&& sayLength==0){
    delay(200);
    mp3_play(23);  //    23 length
    dlayPrint();
    delay(200);  
  }
  if (mute ==0&& sayLength==0){
    thisString = itoa((trainLength/10000), buf, 10); 
    sayText();
    delay(400);
  }
}

// do speed report..........................8
void speakSpeed(){
  if (mute ==0&& saySpeed==0){
    thisString = itoa(mphScale/10, buf, 10);//saveSpeed/10), buf, 10);  
    delay(200); 
    mp3_play(24); // speed
    dlayPrint();
    delay(200);
    sayText();
  } 
}

// do temperature report ..........................9
void speakTemperature(){
  if (mute ==0&& sayTemperature==0){
    Serial.print("Requesting temperatures...");
    sensors.requestTemperatures(); // Send the command to get temperatures
    Serial.println("DONE");
    for(int i=0;i<numberOfDevices; i++)
    {
      if(sensors.getAddress(tempDeviceAddress, i))
      {
        Serial.print("Temperature for device: ");
        Serial.println(i,DEC);
        printTemperature(tempDeviceAddress); // Use a simple function to print out the data
      } 
    }
    int xtemp2 = xtemp  * 100;
    int xtempWhole = xtemp2 / 100;
    int xtempDecimal = xtemp2 - (xtempWhole * 100);
    Serial.print(xtempWhole); // Converts tempC to Fahrenheit  
    Serial.print(".");
    Serial.println(xtempDecimal);
    String tempFStringW = itoa((xtempWhole), buf, 10);  // whole number portion
    String tempFStringD = itoa((xtempDecimal), buf, 10); // decimal portion
    //    thisString = tempFStringW + "." + tempFStringD;  //use this line for 1/100 deg resolution
    thisString = tempFStringW;// + "." + tempFStringD;  //use this line for whole # degrees only(no decimal)
    delay(1200);
    Serial.println(thisString); // Converts tempC to Fahrenheit  
    Serial.println(thisString); 
    mp3_play(19);  // 19=temperature
    dlayPrint();
    delay(200); 
    sayText();  // speak temperature with or without decmial degrees (see above comments)
    mp3_play(13);  //degrees..........................10
    dlayPrint();
    //    mp3_play(14);  //fahrenheit- add these 3 lines to say "Fahrenheit"
    //    dlayPrint(); 
    //    delay(800);
  } 
}

void speakOut(){
  mp3_play(20);  //out..........................12
  dlayPrint(); 
}

void computeSpeed(){
  mph = speedConstant / saveSpeed;  //speed in real MPH
  mph = mph * 10;
  mph=mph/63;
  mphScale = (mph*scale);
  mphScale = mphScale +50;
  mphScale = mphScale /100; // round up
}

void computeLength(){
  lengthTime = axleTime -lengthStartTime;
  trainLength = mphScale *88; // convert mph to fps (mph x 1.4666666 = fps)
  trainLength = trainLength / 60;
  trainLength = trainLength * (lengthTime / 1000);
  lcd.setCursor(8,0);
  lcd.print(" L= ");
  lcd.print(trainLength/10000);
  lcd.setCursor(0,1);
  lcd.print("Scale Spd= ");
  lcd.print(mphScale/10);
}

// routine to stay here till busy pin goes low once then goes high after speech item completes
void dlayPrint()
{
  int bsyflag=0;
  Serial.println(" ");
  Serial.print("buusypin ");
  for( z=0; z<=300 ; z++){   // was 300
    bsy = digitalRead(buusyPin);
    Serial.print(bsy);
    delay(20); 
    if (bsyflag==1 && bsy==1){
      break;
    }
    if(bsy==0){
      bsyflag=1;
    }
  }
  Serial.println(" ");
  Serial.println("done");
}

// function to print a device address
void printAddress(DeviceAddress deviceAddress)  // NEEDED?????????????????????????????????????????????
{
  for (uint8_t i = 0; i < 8; i++)
  {
    if (deviceAddress[i] < 16) Serial.print("0");
    Serial.print(deviceAddress[i], HEX);
  }
}

// function to print the temperature for a device
void printTemperature(DeviceAddress deviceAddress)
{
  float tempC = sensors.getTempC(deviceAddress);
  xtemp = DallasTemperature::toFahrenheit(tempC);
  lcd.setCursor (9,0);
  lcd.print("t=");
  lcd.print(xtemp);
}

// routine to go into alignment if either is blocked on boot
void alignLasers(){
  PTState = digitalRead(PhotoTransistor);   
  PTState2 = digitalRead(PhotoTransistor2);
  if (PTState == 0 || PTState2 ==0){
    lcd.clear();
    for (int xx=0; xx <= 100; xx++){   // 100 is delay to stay on test till good for 100 loops
      lcd.setCursor(0,0); // start of line 1f
      lcd.print("Laser Align ");
      lcd.setCursor(12,0);
      lcd.print(xx);
      delay(50);
      do{
        PTState = digitalRead(PhotoTransistor);   
        PTState2 = digitalRead(PhotoTransistor2);
        lcd.setCursor(0,1);
        lcd.print(PTState);
        lcd.print(" ");
        lcd.print(PTState2);
        if (PTState==0 || PTState2==0){
          xx=0;
          lcd.setCursor(12,0);
          lcd.print("   ");
        }
      }
      while (PTState==0 || PTState2==0);  //stay here till both axles clear
    }
  }
}

void sayText()  // Say number with decimal
{
  if (mute ==1){
    Serial.println("SKIPPING!!!");
  }
  else { 
    int lgth = thisString.length();
    for (int i=0; i <= lgth-1; i++){
      int say = (thisString.substring(i,i+1)).toInt();
      Serial.println(thisString.substring(i,i+1)); 
      String isDecPoint=(thisString.substring(i,i+1));
      Serial.print("saying ");
      Serial.print(isDecPoint );
      delay(100);  // needed to prevent audio "pops" at start
      if(isDecPoint=="."){
        delay(200);
        mp3_play(12);
        delay(200);
      }
      else if (say == 0 ){
        mp3_play(10); //zero
      }
      else {  
        mp3_play(say);  
      }
      dlayPrint();// delay(1200);
      lcd.setCursor(0,1);
    }
  }
}

//Main setup menu routine....................................
// left button is Down and No, right is Up and Yes
void mainMenu(){  
  getMileMarker();
  lcd.clear();
  lcd.print("Setup Menu");
  lcd.setCursor(0,1);
  lcd.print("UP & DOWN to Set");
  delay(2000); 
  getDefectOnOff();
  getScale();
  // Do Sound on/off =0 if talking, =1 if muted  
  showOFFON();
  lcd.print("Sound = ");
  lcd.setCursor(13,1);
  if(mute==0){
    lcd.print("ON ");
  }
  else
  {
    lcd.print("OFF"); 
  }
  getButtonPress();
  lcd.setCursor(13,1);
  if (buttonStateTemp == 0){  //0=pressed button
    mute = 0; // 0= talking 1=mute
    lcd.print("ON ");
  }
  else
  {
    mute = 1; 
    lcd.print("OFF");
  }
  delay(500);
  // Do Temperature on/off  =0 if speaking temperature, =1 if muted  
  showOFFON();
  lcd.print("Temperature = ");
  lcd.setCursor(13,1);
  if(sayTemperature==0){
    lcd.print("ON ");
  }
  else
  {
    lcd.print("OFF"); 
  }
  getButtonPress();
  lcd.setCursor(13,1);
  if (buttonStateTemp == 0){  //0=pressed button
    sayTemperature = 0; // 0= talking 1=mute
    lcd.print("ON ");
  }
  else
  {
    sayTemperature = 1; 
    lcd.print("OFF");
  }
  delay(500);
  // Do Length on/off =0 if speaking length, =1 if muted  
  showOFFON();
  lcd.print("Length = ");
  lcd.setCursor(13,1);
  if(sayLength==0){
    lcd.print("ON ");
  }
  else
  {
    lcd.print("OFF"); 
  }
  getButtonPress();
  lcd.setCursor(13,1);
  if (buttonStateTemp == 0){  //0=pressed button
    sayLength = 0; // 0= talking 1=mute
    lcd.print("ON ");
  }
  else
  {
    sayLength = 1; 
    lcd.print("OFF");
  }
  delay(500);
  // Do Speed on/off =0 if speaking speed, =1 if muted  
  showOFFON();
  lcd.print("Speed = ");
  lcd.setCursor(13,1);
  if(saySpeed==0){
    lcd.print("ON ");
  }
  else
  {
    lcd.print("OFF"); 
  }
  getButtonPress();
  lcd.setCursor(13,1);
  ;
  if (buttonStateTemp == 0){  //0=pressed button
    saySpeed = 0; // 0= talking 1=mute
    lcd.print("ON ");
  }
  else
  {
    saySpeed = 1; 
    lcd.print("OFF");
  }
  delay(500);

  // Do RR name on/off =0 if speaking RR, =1 if muted  
  showOFFON();
  lcd.print("RR Name = ");
  lcd.setCursor(13,1);
  if(sayRR==0){
    lcd.print("ON ");
  }
  else
  {
    lcd.print("OFF"); 
  }
  getButtonPress();
  lcd.setCursor(13,1);
  ;
  if (buttonStateTemp == 0){  //0=pressed button
    sayRR = 0; // 0= talking 1=mute
    lcd.print("ON ");
  }
  else
  {
    sayRR = 1; 
    lcd.print("OFF");
  }
  delay(500);

  // Do Defect Detector on/off =0 if speaking RR, =1 if muted  
  showOFFON();
  lcd.print("Defect Det = ");
  lcd.setCursor(13,1);
  if(sayDD==0){
    lcd.print("ON ");
  }
  else
  {
    lcd.print("OFF"); 
  }
  getButtonPress();
  lcd.setCursor(13,1);
  ;
  if (buttonStateTemp == 0){  //0=pressed button
    sayDD = 0; // 0= talking 1=mute
    lcd.print("ON ");
  }
  else
  {
    sayDD = 1; 
    lcd.print("OFF");
  }
  delay(500);

  // Do mile marker on/off =0 if speaking milemarker, =1 if muted  
  showOFFON();
  lcd.print("Mile Marker = ");
  lcd.setCursor(13,1);
  if(sayMileMarker==0){
    lcd.print("ON ");
  }
  else
  {
    lcd.print("OFF"); 
  }
  getButtonPress();
  lcd.setCursor(13,1);
  ;
  if (buttonStateTemp == 0){  //0=pressed button
    sayMileMarker = 0; // 0= talking 1=mute
    lcd.print("ON ");
  }
  else
  {
    sayMileMarker = 1; 
    lcd.print("OFF");
  }
  delay(500);

  // Do axles on/off =0 if speaking axles, =1 if muted  
  showOFFON();
  lcd.print("Axles = ");
  lcd.setCursor(13,1);
  if(sayAxles==0){
    lcd.print("ON ");
  }
  else
  {
    lcd.print("OFF"); 
  }
  getButtonPress();
  lcd.setCursor(13,1);
  ;
  if (buttonStateTemp == 0){  //0=pressed button
    sayAxles = 0; // 0= talking 1=mute
    lcd.print("ON ");
  }
  else
  {
    sayAxles = 1; 
    lcd.print("OFF");
  }
  delay(500);
  writeEEPROM();
}

void getDigitValues(){
  Serial.print("mileMarkerW ");
  Serial.println(mileMarkerW);
  hDigit = mileMarkerW / 100; // hundreds digit
  tDigit = mileMarkerW - (100*hDigit);
  tDigit = tDigit / 10; // tens digit
  oDigit = mileMarkerW -hDigit*100 - tDigit*10; // ones digit
  Serial.print("parsed = ");
  Serial.print(hDigit);
  Serial.print(tDigit);
  Serial.println(oDigit);
}

// set defect on/off and %age   ................................................................................
void getDefectOnOff(){

  do
  {
    lcd.clear();
    lcd.setCursor(0,1);
    lcd.print("Defects = ");
    lcd.print(defectOnOff );
    lcd.setCursor(0,0);
    lcd.print("DOWN          UP");
    getButtonPress();
    if (buttonStateTemp==1)
    {
      if(defectOnOff != 0){
        defectOnOff =( defectOnOff-1);
      }
    }
    else if (buttonState2Temp==1){
      if(defectOnOff <=99){
        defectOnOff =( defectOnOff+1);
      }
    }
  }
  while (digitalRead(menuPin)==0);
  do
  {
    buttonStateMenuTemp = (digitalRead(menuPin));// stay here till button released
  }
  while (buttonStateMenuTemp==1);
  writeEEPROM();
}

void getScale(){  // use UP DOWN to increment / decrement 0-250 range
  do
  {
    lcd.clear();
    lcd.setCursor(0,1);
    lcd.print("Scale = ");
    lcd.print(scale );
    lcd.setCursor(0,0);
    lcd.print("DOWN          UP");
    getButtonPress();
    if (buttonStateTemp==1)
    {
      if(scale != 0){
        scale =( scale-1);
      }
    }
    else if (buttonState2Temp==1){
      if(scale <=249){
        scale =( scale+1);
      }
    }
  }
  while (digitalRead(menuPin)==0);
  do
  {
    buttonStateMenuTemp = (digitalRead(menuPin));// stay here till button released
  }
  while (buttonStateMenuTemp==1);
  writeEEPROM();
}

void getMileMarker(){ 
  // 100's .....................................
  do
  {
    lcd.clear();
    lcd.setCursor(0,1);
    lcd.print("MileMarker=");
    thisString=itoa(mileMarkerW, buf,10);// + "." );// + itoa(mileMarkerD,buf,10));
    thisString = thisString + ".";
    thisString = thisString + itoa(mileMarkerD,buf, 10);
    lcd.print(thisString );//+ "."+mileMarkerD);
    lcd.setCursor(0,0);
    lcd.print("DOWN  100's   UP");
    getButtonPress();
    getDigitValues();
    if (buttonStateTemp==1)
    {
      if(hDigit != 0){
        mileMarkerW =( mileMarkerW-100);
      }
    }
    else if (buttonState2Temp==1){
      if(hDigit !=9){
        mileMarkerW =( mileMarkerW+100);
      }
    }
  }
  while (digitalRead(menuPin)==0);
  do
  {
    buttonStateMenuTemp = (digitalRead(menuPin));// stay here till button released
  }
  while (buttonStateMenuTemp==1);

  // 10's ..................................... 
  do
  {
    lcd.clear();
    lcd.setCursor(0,1);
    lcd.print("MileMarker=");
    thisString=itoa(mileMarkerW, buf,10);// + "." );// + itoa(mileMarkerD,buf,10));
    thisString = thisString + ".";
    thisString = thisString + itoa(mileMarkerD,buf, 10);
    lcd.print(thisString );//+ "."+mileMarkerD);
    lcd.setCursor(0,0);
    lcd.print("DOWN   10's   UP");
    getButtonPress();
    getDigitValues();
    if (buttonStateTemp==1)
    {
      if(tDigit != 0){
        mileMarkerW =( mileMarkerW-10 );
      }
    }
    else if (buttonState2Temp==1){
      if(tDigit !=9){
        mileMarkerW =( mileMarkerW+10 );
      }
    } 
  }
  while (digitalRead(menuPin)==0);
  do
  {
    buttonStateMenuTemp = (digitalRead(menuPin));// stay here till button released
  }
  while (buttonStateMenuTemp==1);  

  // 1's ..................................... 
  do
  {
    lcd.clear();
    lcd.setCursor(0,1);
    lcd.print("MileMarker=");
    thisString=itoa(mileMarkerW, buf,10);// + "." );// + itoa(mileMarkerD,buf,10));
    thisString = thisString + ".";
    thisString = thisString + itoa(mileMarkerD,buf, 10);
    lcd.print(thisString );//+ "."+mileMarkerD);
    lcd.setCursor(0,0);
    lcd.print("DOWN    1's   UP");
    getButtonPress();
    getDigitValues();
    if (buttonStateTemp==1)
    {
      if(oDigit != 0){
        mileMarkerW =( mileMarkerW-1  );
      }
    }
    else if (buttonState2Temp==1){
      if(oDigit !=9){
        mileMarkerW =( mileMarkerW+1  );
      }
    } 
  }
  while (digitalRead(menuPin)==0);
  do
  {
    buttonStateMenuTemp = (digitalRead(menuPin));// stay here till button released
  }
  while (buttonStateMenuTemp==1);  

  // 1/10's ..................................... 
  do
  {
    lcd.clear();
    lcd.setCursor(0,1);
    lcd.print("MileMarker=");
    thisString=itoa(mileMarkerW, buf,10);// + "." );// + itoa(mileMarkerD,buf,10));
    thisString = thisString + ".";
    thisString = thisString + itoa(mileMarkerD,buf, 10);
    lcd.print(thisString );//+ "."+mileMarkerD);
    lcd.setCursor(0,0);
    lcd.print("DOWN 1/10ths  UP");
    getButtonPress();
    if (buttonStateTemp==1)
    {
      if(mileMarkerD != 0){
        mileMarkerD =( mileMarkerD-1  );
      }
    }
    else if (buttonState2Temp==1){
      if(mileMarkerD != 9){
        mileMarkerD =(mileMarkerD+1  );
      }
    } 
  }
  while (digitalRead(menuPin)==0);
  do
  {
    buttonStateMenuTemp = (digitalRead(menuPin));// stay here till button released
  }
  while (buttonStateMenuTemp==1);  
  Serial.print("EEPROM-W2 ");
  Serial.println(mileMarkerW);
  Serial.print("EEPROM-D2 ");
  Serial.println(mileMarkerD);
  writeEEPROM();
}

// routine to show OFF or ON  or UP or DOWN on LCD
void showOFFON(){
  lcd.clear();
  lcd.print("OFF           ON");
  lcd.setCursor(0,1); 
}

// Routine to get button press events from left and right buttons and Menu button
void getButtonPress(){  //stay here till button or button 1 pressed - returns buttonState or buttonState2 = 0
  do{
    buttonState = digitalRead(buttonPin);   
    buttonState2 = digitalRead(buttonPin2);
    buttonStateMenu = digitalRead(menuPin);
  }
  while (buttonState==0 && buttonState2==0 && buttonStateMenu==0);  //stay here till one is pressed
  buttonStateTemp=buttonState;
  buttonState2Temp=buttonState2;
  buttonStateMenuTemp=buttonStateMenu;
  do{
    buttonState = digitalRead(buttonPin);   
    buttonState2 = digitalRead(buttonPin2);
    buttonStateMenu=digitalRead(menuPin);
  }
  while (buttonState==1 || buttonState2==1 || buttonStateMenu==1);  //stay here till all released
  lcd.setCursor(0,0);
  lcd.print("                ");
}

void writeEEPROM(){
  EEPROM.write(0,sayTemperature );
  EEPROM.write(1,sayMileMarker);
  EEPROM.write(2,sayLength);
  EEPROM.write(3,saySpeed);
  EEPROM.write(4,sayAxles);
  EEPROM.write(5,sayRR);
  EEPROM.write(6,sayDD);
  mileMarkerWh = mileMarkerW/256;
  mileMarkerWl = mileMarkerW -mileMarkerWh*256;
  EEPROM.write(7,mileMarkerWh); // whole # part of mile marker
  EEPROM.write(8,mileMarkerWl); // 1 digit decimal part of mile marker
  EEPROM.write(9,mileMarkerD); // 1 digit decimal part of mile marker
  EEPROM.write(10,scale); // scale from 0 to 250
  EEPROM.write(11,defectOnOff); // =0 if never says defects otherwise a percentage to 100
}

void readEEPROM(){
  sayTemperature = EEPROM.read(0);
  sayMileMarker = EEPROM.read(1);
  sayLength = EEPROM.read(2);
  saySpeed = EEPROM.read(3);
  sayAxles = EEPROM.read(4);
  sayRR=EEPROM.read(5);
  sayDD=EEPROM.read(6);
  mileMarkerWh=EEPROM.read(7);// whole # part of mile marker - upper
  mileMarkerWl=EEPROM.read(8); // whole # part of mile marker - lower
  mileMarkerD=EEPROM.read(9); // 1 digit decimal part of mile marker
  mileMarkerW = mileMarkerWh *256 + mileMarkerWl;
  scale=EEPROM.read(10);
  defectOnOff=EEPROM.read(11);
  Serial.print("EEPROM-temp ");
  Serial.println(sayTemperature);
  Serial.print("EEPROM-mm ");
  Serial.println(sayMileMarker);
  Serial.print("EEPROM-len ");
  Serial.println(sayLength);
  Serial.print("EEPROM-sp ");
  Serial.println(saySpeed);
  Serial.print("EEPROM-ax ");
  Serial.println(sayAxles);
  Serial.print("EEPROM-rr ");
  Serial.println(sayRR);
  Serial.print("EEPROM-dd ");
  Serial.println(sayDD);
  Serial.print("EEPROM-w ");
  Serial.println(mileMarkerW);
  Serial.print("EEPROM-d ");
  Serial.println(mileMarkerD); 
}

Software Notes:
I needed to convert floating point values (such as mile marker) to strings to "say" them - this code helped with that conversion:
Converting a floating point value to a string
Audio Files
The MP3 files were created in Audacity (see: http://audacity.sourceforge.net/ ) - some information on using Audacity is here: http://www.trainelectronics.com/MP3_project/#Audacity_-_Sound_Editor ).  While it is out of the scope of this discussion to go into great detail about using Audacity some comments are in order.

I recorded all of the sounds that I needed by making one large file (shown here).  Then I individually saved each one with an appropriate name.

There the number 9 segment of the file is highlighted and I used Export Selection to save it with the name "nine.mp3"

 

No properties need be changed.  Just hit ENTER on this screen.

 

The audio files must be in a directory called /mp3 and named with 4 digits from 0001.mp3 through 0025.mp3

See:  http://www.trainelectronics.com/Arduino/MP3Sound/TalkingTemperature/#Program_to_test_audio_files for a testing routine to verify that your sound unit and files work properly.

 

 
The 25 audio files on my micro SD card are::
 1 - one
 2 - two
 3 - three
 4 - four
 5 - five
 6 - six
 7 - seven
 8 - eight
 9 - nine
 10 - zero
 11 - decimal
 12 - point
 13 - degrees
 14 - fahrenheit
 15 - celsius
 16 - CSX  (railroad name)
 17 - Defect Detector
 18 - no defects
 19 - temperature
 20 - out
 21 - defect axle # ??
 22 - axles...
 23 - train length...
 24 - speed...
 25 - milepost
"RadioActive" Defect Detector
I have experimented with several inexpensive FM transmitters like the one shown below.  Each of these units is designed to connect an MP3 player or cell phone to a car's FM radio so that you can listen through the car's speakers.  This device was purchased from eBay for less than $5.00, delivered.  See:
eBay Search

The transmitter runs on either two AAA cells or a 12 volt cigarette lighter adapter that is included.   A slide switch allows you to select from four different transmitting frequencies so that a locally used station can be avoided.

I found that that the frequencies were a bit off.  This can be dealt with by tuning the scanner / radio receiver up and down the band or by adjusting the small coil inside of the transmitter.  It is circled below.  Make sure you adjust it with a non-metallic wand.

There is even a hole behind the label that can be used to make this adjustment with the case closed.

The range is not terrific but should be more than enough for many model railroad layouts.  When properly adjusted I was picking up 30-50 feet away. 


 

Connecting to the Defect Detector
The transmitter normally connects to an MP3 player via a 1/8" stereo plug.  To pick up the audio from the Defect Detector this connection needs to go to the MP3 player's output.  While you could go into the transmitter and solder wires to the input connections I find it just as easy to use a female stereo connector like the one shown here.  It was cut from a splitter that can be found on eBay.

Stripping the insulation reveals a braided lead and two insulated leads, one white and one red.

These leads were connected to a two pin plug.  The red and white wires are connected together and terminate in a 1K resistor.  The braid goes to the other lead.

This connector plug into the two terminals on the MP3 player that are line output (pins 4 or 5- labeled DAC_R and DAC_L) and ground.  The 1K resistor can be increased to 10 or 100K should the audio be too strong and distorted.

Here is my test setup.  The speaker is plugged into the speaker terminals (yellow wires) and the transmitter goes to the DAC_L/DAC_R terminals and ground.