I'm working on an Arduino to control sound effects on my model railroad. It has 3 LDR sensors on pins A0-A2, which will be read and trigger off various sound effects. The idea is that it will loop ambient tracks depending on which sensor was last triggered.
At the moment, it will work with the 'liftout' sensor, which turns the sound off as another Arduino handles the sound effects for the other part of the layout. This works. However, when I call the code to read the second sensor, the Arduino becomes unresponsive. Below is the code in question, 'CampB_Controller_0.4':
d#include <NmraDcc.h>
#include "DFRobotDFPlayerMini.h"
#include <ServoTimer2.h>
/* This is the animation controller for Camp B on my model railroad. Version notes are below.
Version 0.1:
- Test version, designed to test the use of the DFPlayerMini in conjunction with the DCC sniffer.
Version 0.2:
- Test version, adds the StepperTimer2 library into the mix and tests this alongside the DFPlayerMini and
NmraDcc libraries.
Version 0.3:
- Removes test code from 0.2. Implements existing functionality from Camp A sound driver micro:
- Playing of ambient town sounds
- Playing of non-workshop sounds when train taken out of workshop
- Stopping ambient sounds when train passes over liftout
Note: The coil sensor for the block occupancy detector has been replaced with a pushbutton with INPUT_PULLUP for testing.
This will need to be changed to a regular input before being installed under the layout.
This version also keeps the NMRA sniffer code in place - to ensure it all works nicely together.
Version 0.4:
- Adds forest ambient sounds triggered by approach to upper deck. Is still a 'bench test' version
with INPUT_PULLUP in place on MOW pin. Two main changes were made:
- Program loop checks the upper deck approach LDR and flips townMode.
- PlayAmbient() will play forest or town ambient sound, depending on state of townMode.
- Also added defines for all music tracks - and added these to the DFPLayer Mini SD card.
*/
// Define the Arduino input Pin number for the DCC Signal
#define DCC_PIN 2
//Define sound tracks for town
#define TRACK_TEST 1 //Number of test track
#define TRACK_AMBI1 2 //Number of first non-workshop town ambient track
#define TRACK_AMBI2 3 //Number of second non-workshop town ambient track
#define TRACK_WOAMBI1 4 //Number of first workshop town ambient track
#define TRACK_WOAMBI2 5 //Number of second workshop town ambient track
#define TRACK_FORAMBI1 6 //Number for first forest (Camp B) ambient track
#define TRACK_FORAMBI2 7 //Number for second forest (Camp B) ambient track
#define TRACK_LOAD1 8 //Number for first log loading track
#define TRACK_LOAD2 9 //Number for second log loading track
#define TRACK_LOAD3 10 //Number for third log loading track
//Set the status pin for the MP3 player
#define MP_STATUS 3
//Define the servo constants
#define SERVO_PIN 6 //Output pin
#define TOWER_START 1200 //Starting angle for servo motor
#define TOWER_END 750 //Ending angle for servo motor - spout lowered
#define TOWER_DELAY 7 //MS for delay between tower steps.
//Define delay after audio command
#define AUDCMDDELAY 42
//Defines for LDR Inputs + Sound occupancy sensors
#define LIFTOUT_PIN A0
#define UDECK_APPROACH A1
#define UDECK_TOWER A2
#define LDR_THRESH 650
#define WAIT_DELAY 60000 //Amount of time to wait after sensor is triggered, for entry/
// exit to the Camp A module.
#define MOW_DELAY 100//Milliseconds to wait before checking for train in workshop
//siding.
#define MOW_PIN 4
//Variables for ambient sounds:
unsigned long entryTime = 0; //Timer used to check LDR again.
unsigned long sidingTime = 0; //Timer used to check siding occupancy
boolean ambientPlay = true; //Goes false when a train leaves the Camp A side of the
//layout, as detected by the LDR.
boolean mowInSiding = true; //MOW train will start in siding - all trains start and
//finish at Camp A, aside from the PRM train.
boolean lastCheck = false; //Used to check for departure of locomotive from MOW siding.
boolean townMode = true; //Tracks if we're playing town or forest ambient. Town is starting state.
//Create DFPlayerMini object
DFRobotDFPlayerMini mp3Player;
//Create DCC reader object
NmraDcc Dcc ;
//******** Below functions are readers for DCC sniffer
// Below line is commented out - only interested in functions, not speed.
//#define NOTIFY_DCC_SPEED
#ifdef NOTIFY_DCC_SPEED
void notifyDccSpeed( uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t Speed, DCC_DIRECTION Dir, DCC_SPEED_STEPS SpeedSteps )
{
//Will pull out the speed of the locomotive. Speed is the current throttle setting. Steps is the mode (14/28/128).
};
#endif
// Uncomment the #define below to print all Function Packets
#define NOTIFY_DCC_FUNC
#ifdef NOTIFY_DCC_FUNC
void notifyDccFunc(uint16_t Addr, DCC_ADDR_TYPE AddrType, FN_GROUP FuncGrp, uint8_t FuncState)
{
//Will pull out the function bits. Need to bitwise AND ('&') with FuncState.
switch ( FuncGrp )
{
#ifdef NMRA_DCC_ENABLE_14_SPEED_STEP_MODE
case FN_0:
//Output of function 0 for 14-step speed mode here.
break;
#endif
case FN_0_4:
if (Dcc.getCV(CV_29_CONFIG) & CV29_F0_LOCATION) // Only process Function 0 in this packet if we're not in Speed Step 14 Mode
{
//Output of function 0 for 28/128 speed step mode here.
}
//Output for first four functions here. Can bitwise AND with defined masks - FN_BIT_01, FN_BIT_02, etc.
break;
case FN_5_8:
//Output for functions 5-8 here. Can bitwise AND with defined masks - FN_BIT_05, FN_BIT_06, etc.
break;
case FN_9_12:
//Output for functions 9-12 here. Can bitwise AND with defined masks - FN_BIT_09, FN_BIT_10, etc.
break;
case FN_13_20:
//Output for functions 13-20 here. Can bitwise AND with defined masks - FN_BIT_13, FN_BIT_14, etc.
break;
case FN_21_28:
//Output for functions 21-28 here. Can bitwise AND with defined masks - FN_BIT_21, FN_BIT_22, etc.
break;
}
}
#endif
//******** Below are helper functions for sound effects
boolean mp3Playing() {
/* Checks the DFPlayer busy pin. Returns true if the player is
not busy, and false if the player is busy.
*/
int busyPin = digitalRead(MP_STATUS);
if (busyPin) {
//busyPin value of high means it's not playing
return false;
} else {
//busyPin is low, so it's playing
return true;
}
}
void playSound(int trackNo) {
//Will play a single MP3 track once.
//Used for triggering sound effects and
//test track.
if (mp3Playing()) {
//Need to stop whatever's playing
mp3Player.stop();
}
mp3Player.volume(25);
mp3Player.playMp3Folder(trackNo);
delay(AUDCMDDELAY); //Delay to allow track to start playing
}
boolean mowTrainDetect() {
if (digitalRead(MOW_PIN)) {
return false; //Pin is high when no train
} else {
return true; //Pin will be low - train detected
}
}
boolean locoDeparted() {
//Returns true if a loco has just departed the
//MOW siding, false otherwise.
if (lastCheck && (!mowTrainDetect())) {
//Train was in siding, now it isn't.
lastCheck = false; //As train has left siding
return true;
} else if ((!lastCheck) && mowTrainDetect()) {
//Train wasn't in siding, now it is.
lastCheck = true;
return false;
} else {
//Above cases handle change in state.
//Return false if no state change.
return false;
}
}
//******** Below helpers are for the ambient sounds
void checkLiftout() {
//Checks for entry/exit on liftout section
if (readLDR(LIFTOUT_PIN) && ((millis() - entryTime > WAIT_DELAY) || (entryTime == 0))) {
//So, LDR is tripped and there's either been enough time, or there hasn't been a previous
//time. Thus, a train is entering.
ambientPlay = !ambientPlay; //Flip the ambientPlay value
entryTime = millis(); //Set timer for next time.
if (!ambientPlay) {
//Then it's not playing, and we need to stop the sound effects
mp3Player.stop();
}
}
}
void checkDeck() {
//Checks for entry/exit on upper deck
if (readLDR(UDECK_APPROACH) && ((millis() - entryTime > WAIT_DELAY) || (entryTime == 0))) {
//So, LDR is tripped and there's either been enough time, or there hasn't been a previous
//time. Thus, a train is entering.
townMode = !townMode; //Flip the townMode value
entryTime = millis(); //Set timer for next time.
//Next, we need to stop and restart the ambient sound - playAmbient() will handle the change
//of track.
mp3Player.stop();
playAmbient();
}
}
boolean readLDR(int ldrPin) {
int ldrVal = analogRead(ldrPin);
if (ldrVal < LDR_THRESH) {
//LDR has been tripped - return true
return true;
} else {
//LDR has not been tripped - return false
return false;
}
}
void playAmbient() {
//Will play one of two Ambient tracks at random, and will also pick
//between the workshop and non-workshop ambients.
int choiVal = random(0, 100); //Pick a number between 0 and 100
digitalWrite(13, townMode); //For debugging
mp3Player.volume(18);
if (choiVal < 50) {
//Play first ambient tracks.
if (townMode) {
//Then we need to play town ambient sounds
if (mowInSiding) {
//Play workshop ambient
mp3Player.playMp3Folder(TRACK_WOAMBI1);
} else {
//Play non-workshop ambient
mp3Player.playMp3Folder(TRACK_AMBI1);
}
} else {
//We play forest ambient sounds
mp3Player.playMp3Folder(TRACK_FORAMBI1);
}
} else {
//Play second ambient tracks.
if (townMode) {
if (mowInSiding) {
//Play workshop ambient
mp3Player.playMp3Folder(TRACK_WOAMBI2);
} else {
//Play non-workshop ambient
mp3Player.playMp3Folder(TRACK_AMBI2);
}
} else {
//Play forest ambient sounds
mp3Player.playMp3Folder(TRACK_FORAMBI2);
}
}
}
void sysPause() {
delay(WAIT_DELAY); //Wait for train to leave (1 minute)
}
//******** Below are main body of program.
void setup()
{
// Setup which External Interrupt, the Pin it's associated with that we're using and enable the Pull-Up
// Many Arduino Cores now support the digitalPinToInterrupt() function that makes it easier to figure out the
// Interrupt Number for the Arduino Pin number, which reduces confusion.
#ifdef digitalPinToInterrupt
Dcc.pin(DCC_PIN, 0);
#else
Dcc.pin(0, DCC_PIN, 1);
#endif
// Call the main DCC Init function to enable the DCC Receiver
//Third along is 0 - no flags. Makes it respond to any address.
Dcc.init( MAN_ID_DIY, 10, 0, 0 );
//Set up the MP3 player and pins
pinMode(MP_STATUS, INPUT);
Serial.begin(9600);
mp3Player.begin(Serial);
mp3Player.volume(20);
mp3Player.stop();
//Set up MOW track input. Will need to be changed to INPUT instead of INPUT_PULLUP for actual install.
pinMode(MOW_PIN, INPUT_PULLUP);
//Set up ambient sounds
randomSeed(analogRead(A4));
//Set up LED output for debugging
pinMode(13, OUTPUT);
digitalWrite(13, LOW);
}
void loop()
{
// You MUST call the NmraDcc.process() method frequently from the Arduino loop() function for correct library operation
Dcc.process();
if ((millis() - sidingTime) > MOW_DELAY) {
//Time to check for departure
if (!mowInSiding) {
//MOW train is out of siding. Need to swap to workshop ambient
if (locoDeparted()) {
mowInSiding = true; //Set MOW train back in siding
mp3Player.stop();
playAmbient(); //Switch track
sysPause(); //Wait for train to leave
}
} else {
//MOW train is in siding. Need to swap to non-workshop ambient
//when the locomotive arrives.
if (mowTrainDetect()) {
//Train is in the siding
mowInSiding = false; //Set it as though train's out - for ambient
mp3Player.stop(); //Stop current ambient track
playAmbient(); //Start new ambient track - should be non-ambient
sysPause(); //Wait for train to leave
}
}
sidingTime = millis();
} else {
//Check if ambient track should be playing
if ((!mp3Playing()) && ambientPlay) {
playAmbient();
}
}
checkLiftout(); //Check to see if a train has exited
checkDeck(); //Check to see if a train is entering upper deck
}
If I comment out the call to checkDeck(), the Arduino will run. If I call that function, then it will not respond to anything, instead it'll just crash. It doesn't even get as far as turning on the LED on pin 13 for debugging.
I can confirm this is not a hardware problem. I have tried multiple DFPLayer Mini modules and multiple Arduino Unos. That one function call is the difference between failure and success.
I can also confirm this is not a library compatibility issue. All the libraries used in this sketch have been tested together and confirmed no compatibility problems.
(Also, the DCC stuff will be utilised later - I'm trying to get each stage working before moving onto the next.)
All I did for the checkDeck() function was to copy checkLiftout() and change it to point to the upper deck sensor (UDECK_APPROACH), then flip the townMode. The rest of the function is the same as for the one that works. I honestly don't have a clue what's going on with this one. There's no loops or recursion that could cause an infinite loop. Can anyone shed any light on this?