Contents
- Preamble
- Introduction
- The wiring
- The sound files
- The RoboRemo app
- The code
- How the code works
- Conclusion
Preamble
In the previous stages of my investigations into controlling a loco using a Bluetooth phone app you will see that -- ..... I started off by using a freebie app and its associated code provided by Steve Massiker at arduinorailwaycontrol.com (see Part 1)
- .... I then tinkered with the default speed settings to try and improve the way the loco responded at slow speeds (see Part 2)
- ...... I moved on to explore the potential of a different phone app (RoboRemoFree) as this enabled me to have 255 speed steps rather than the nine speed steps offered by Steve's set-up (see Part 3)
Introduction
Arduino is an Open Source development, which means the resources are made freely available for anyone to explore and further enhance. As a consequence, over the years, numerous add-ons have been developed which enable the basic micro-controller boards to be extended. In the first parts of my explorations, a motor driver and a Bluetooth transceiver were added. In this section, I have added another module - a small SD card player which can be controlled by the Arduino board.The DFPlayer Mini not only interfaces with the Arduino, it also includes an audio amplifier enabling it to be connected directly to a speaker to provide up to 2 watts of output. Its small size (20 x 20 x 10mm) makes it ideal for squeezing into the insides of a battery powere loco. I bought mine from a UK supplier for £3.50, but they can be bought directly from China or Hong Kong for less the £1.00.
To control the DFPlayer, I downloaded a library of instructions from the GitHub website and installed them in the Arduino IDE program.
The procedure for installing the library I followed was:
- Visit the GitHub website and click the Download button
- Remember where I saved the downloaded ZIP file
- Open the Arduino IDE program
- Click the 'Sketch' menu item and select Include Library and then Add ZIP. library
- Navigate to where I saved the ZIP file
- Open it
- The library is now installed. When selecting the Include Library option once more, the new library should appear the the bottom of the list of installed libraries.
Before editing the code, I needed to wire up the Player, motor driver and Bluetooth transceiver to the Nano, so I would know to which pins they were connected.
The wiring
I needed to change the existing wiring (see Part 3) to add the Player to the Nano. The Player and the Bluetooth transceiver are both serial communication devices and only one serial device can normally be added to the Nano. However, it is possible to add another serial device by using the port used by the Nano to communicate with the computer. In addition, the library which I had just installed required the Player to be connected to pins D8 and D9 and so, the motor driver needed to be connected to another pin which could provide PWM output.- I removed the voltage regulator. The L298N motor driver can supply 5 volts (provided its input does not exceed 12 volts) and so this was tapped to power the Player, the Nano and the HC-06 transceiver.
- I kept the control pins for the motor driver the same (D5 and D6) but moved the PWM input to pin D3.
- The serial connections for the DFPlayer were attached to pins D8 and D9.
- The TX and RX connections for the HC-06 were attached to Pins D1 and D0 via a plug and socket so the transceiver could be disconnected when the Nano was connected to the computer.
- A 3mm white LED was connected to Pin D12 via a 220ohm resistor for a front light.
The Sound files
I prepared twelve sound files:- 001 - Silence (1 min)
- 002 - Silence plus horn sound (2.5 seconds)
- 003 - engine start-up to idle (7 seconds)
- 004 - idle (15 secs)
- 005 - idle plus horn (2.5 seconds)
- 006 - slow run (15 secs)
- 007 - slow run plus horn (2.5 secs)
- 008 - mid run (15 secs)
- 009 - mid run plus horn (4 secs)
- 010 - fast run (15 secs)
- 011 - fast run plus horn (2.5 secs)
- 012 - shut down (7 secs)
I downloaded several sound clips from the internet until eventually homing in on some clips of an old tractor on the SoundSnap website. Although it cost me $15 to buy five sound clips (only three of which I used), I had been unable to track down anything which gave me the range of sounds I wanted. I figured that there was not a lot of difference between the sounds made by a tractor and a narrow gauge diesel locomotive - though no doubt some purists will disagree!
The clips were edited using Audacity - wonderful free program which enabled me to select and save sections from the downloaded sound files and also superimpose the sound of the horn over the sound of the engine noise, and adjust the volume of the two so one wasn't drowned out by the other.
The edited files were exported as 16-bit WAVs, which means they play immediately they are called up by the Arduino code. On other players with MP3 files, I have found there can be a silent gap of up to a second between tracks or when one track loops around.
For more information on using Audacity to edit sound effect files see - How I edited sound files for my railbus
Unfortunately, as the files I used were edited from paid-for originals, I cannot provide them here so I am afraid you will have to do your own tracking-down of files (or also pay for the ones I used)
The RoboRemo App
As I wanted more than five controls on the screen of the app, I had to abandon the free version of RoboRemo and invest in the paid-for Bluetooth version - at a cost of £2.89.I set up five buttons and one slider:
The actions for the buttons were (without the quote marks):
- Reverse - 'r'
- Forward - 'f'
- STOP - 'x'
- Horn - 'h'
- Engine start/stop - 'e'
The code
As previously, the entire code is shown here first followed by a break-down of each section explaining its function. This should enable you to copy and paste the code into Arduino IDE if you want to follow in my footsteps //Train control using RoboRemo
//Slider on app has id of 's' and range of 40 to 255
//Forward button on app has action 'f'
//Reverse button on app has action 'b'
//STOP button on app has action 'x'
#include <softwareserial .h="">
#include <DFPlayerMini_Fast.h>
// SOFTWARE SERIAL
SoftwareSerial mySerial(10, 11); // RX, TX for SD player
DFPlayerMini_Fast myMP3; // Initialising SD Player
//Pin connections
int motorSpeedPin = 3;
#define IN1 6 //motor Fwd
#define IN2 5 //motor Reverse
#define fwdLED 12 //Front facing LED pin
#define revLED 13 //Rear facing LED pin
// VARIABLES //
int val = 0; //Speed
int oldval = 0; //Previous speed
int Track = 1; // which track is playing
char cmd[100]; //Command from app
int cmdIndex; //Used to index character in the command
int loSound = 80; // Speed setting between idle and slow run sounds
int midSound = 120; // Speed setting for slow to mid run sounds
int hiSound = 170; // Speed setting for mid to high speed run sounds
void setup() {
// Initializing Serial (Hardware and Software Serial)
Serial.begin(9600);
mySerial.begin(9600);
myMP3.begin(mySerial);
myMP3.volume(30); //Volume setting
myMP3.loop(1); //Loops first (silent) sound track
// Initialising Motor-Driver
pinMode(motorSpeedPin, OUTPUT);
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
analogWrite(motorSpeedPin, 0);
// Initialising LED pins
pinMode(fwdLED, OUTPUT);
pinMode(revLED, OUTPUT);
digitalWrite(fwdLED, HIGH); //Turns on front LED as indicator that loco is switched on
}
void loop() {
// ---- Intrepreting app commands
if(Serial.available()) {
char c = (char)Serial.read();
if(c=='\n') {
cmd[cmdIndex] = 0;
exeCmd(); // execute the command
cmdIndex = 0; // reset the cmdIndex
} else {
cmd[cmdIndex] = c;
if(cmdIndex<99) cmdIndex++;
}
}
}
//// FUNCTIONS ////
void exeCmd() {
if(cmd[0]=='s') {
oldval = val;
val = 0;
for(int i=2; cmd[i]!=0; i++) { // number begins at 2
val = val*10 + (cmd[i]-'0'); // if cmd is "speed 100", val will be 100
}
}
if(val<loSound-10) val=0; //Sets speed to zero to stop motor buzz on default PWM setting
// Direction and Stop
if (cmd[0] =='f') { // (f) Forward
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
digitalWrite(fwdLED, HIGH);
digitalWrite(revLED, LOW);
}
if (cmd[0] =='b') { // (b) Backward
digitalWrite(IN1, LOW);
digitalWrite(IN2, HIGH);
digitalWrite(fwdLED, LOW);
digitalWrite(revLED, HIGH);
}
if (cmd[0] =='x') { // (x) Stop button
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
digitalWrite(fwdLED, LOW);
digitalWrite(revLED, LOW);
myMP3.loop(1);
Track = 1;
val = 0;
oldval = 0;
}
analogWrite(motorSpeedPin, val); // Throttle
//Playing the SD card tracks
//There are twelve tracks on the SD card
//001 - Silence (1 min)
//002 - Silence plus horn sound (2 seconds)
//003 - engine start-up to idle (15 seconds)
//004 - idle (15 secs)
//005 - idle plus horn (2.5 seconds)
//006 - slow run (15 secs)
//007 - slow run plus horn (2.5 secs)
//008 - mid run (15 secs)
//009 - mid run plus horn (4 secs)
//010 - fast run (15 secs)
//011 - fast run plus horn (2.5 secs)
//010 - shut down (3 secs)
if(cmd[0]=='e' && Track==1){
myMP3.play(3);
delay(10000);
myMP3.loop(4);//Play 'engine start-up' and then on to 'idle' (Track 4)
Track = 4;
cmd[0]='n';
}
if(cmd[0]=='e' && Track==4){
myMP3.play(12);
delay(7000);
myMP3.loop(1);//Play 'engine shut down' then on to silence (Track 1)
Track = 1;
cmd[0]='n';
}
if(cmd[0]=='h' && (Track==1 || Track==4 || Track==6 || Track==8 || Track==10)) {
myMP3.play(Track + 1); //Play relevant horn track
delay(3000);
myMP3.loop(Track);
}
if(Track==4 && val>loSound){
myMP3.loop(6);
Track = 6;
}
if(Track==6 && val<loSound){
myMP3.loop(4);
Track = 4;
}
if(Track==6 && val>midSound){
myMP3.loop(8);
Track=8;
}
if(Track==8 && val<midSound){
myMP3.loop(6);
Track=6;
}
if(Track==8 && val>hiSound){
myMP3.loop(10);
Track=10;
}
if(Track==10 && val<hiSound){
myMP3.loop(8);
Track=8;
}
}
NOTE:
Check that the symbols are showing up correctly in the code - some browsers change them.
For example the last part of the code should look like this
How it works
The first section sets up the serial connection and opens the library for the DFPlayer
#include <SoftwareSerial.h>
#include <DFPlayerMini_Fast.h>
// SOFTWARE SERIAL
SoftwareSerial mySerial(10, 11); // RX, TX for SD player
DFPlayerMini_Fast myMP3; // Initialising SD Player
The next section sets up the pins for the various devices. Note: Because the Bluetooth transceiver is using the Hardware Serial port, it does not need to be initialised.
//Pin connections
int motorSpeedPin = 3;
#define IN1 6 //motor Fwd
#define IN2 5 //motor Reverse
#define fwdLED 12 //Front facing LED pin
#define revLED 13 //Rear facing LED pin
The next chunk of code sets-up the variables used in the program:
// VARIABLES //
int val = 0; //Speed
int oldval = 0; //Previous speed
int Track = 1; // which track is playing
char cmd[100]; //Command from app
int cmdIndex; //Used to index character in the command
int loSound = 80; // Speed setting between idle and slow run sounds
int midSound = 120; // Speed setting for slow to mid run sounds
int hiSound = 170; // Speed setting for mid to high speed run sounds
Hopefully the comments (signified with a prefix of //) make them self explanatory.
The setup section of the program is very similar to that used in Part 3, with a couple of slight amendments:
void setup() {
// Initializing Serial (Hardware and Software Serial)
Serial.begin(9600);
mySerial.begin(9600);
myMP3.begin(mySerial);
myMP3.volume(30); //Volume setting
myMP3.loop(1); //Loops first (silent) sound track
// Initialising Motor-Driver
pinMode(motorSpeedPin, OUTPUT);
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
analogWrite(motorSpeedPin, 0);
// Initialising LED pins
pinMode(fwdLED, OUTPUT);
pinMode(revLED, OUTPUT);
digitalWrite(fwdLED, HIGH); //Turns on front LED as indicator that loco is switched on
}
The initialisation of the serial connections is slightly different as the Bluetooth module is connected to the Hardware Serial port and so is initialised with Serial.begin(9600) which sets up the baud rate for the communication. Similarly, the baud rate for the player is also set at 9600.
The final command turns on the front LED. I found this useful when testing the loco, to check that the Arduino board was switched on and functioning as expected.
The main loop is exactly the same as in Part 3. This parses the information sent by the app which arrives as a series of individual characters, and then when the terminal character for each string is receiver ('/n'), puts the characters together as a string.
void loop() {
// ---- Intrepreting app commands
if(Serial.available()) {
char c = (char)Serial.read();
if(c=='\n') {
cmd[cmdIndex] = 0;
exeCmd(); // execute the command
cmdIndex = 0; // reset the cmdIndex
} else {
cmd[cmdIndex] = c;
if(cmdIndex<99) cmdIndex++;
}
}
}
There is only one function (exeCmd), the first part of which was explained in Part 3.
//// FUNCTIONS ////
void exeCmd() {
if(cmd[0]=='s') {
oldval = val;
val = 0;
for(int i=2; cmd[i]!=0; i++) { // number begins at 2
val = val*10 + (cmd[i]-'0'); // if cmd is "speed 100", val will be 100
}
}
The next statement is new: if(val<loSound-10) val=0; //Sets speed to zero to stop motor buzz on default PWM setting
I find that the motor buzzes because the default PWM setting (490Hz) is quite low. So when stationary, I turn off the motor to stop it from buzzing. I am in the process of playing around with the PWM settings and intend to increase the setting to help prevent excess noise from the motor.
The next section is almost as it was in Part 3.
// Direction and Stop
if (cmd[0] =='f') { // (f) Forward
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
digitalWrite(fwdLED, HIGH);
digitalWrite(revLED, LOW);
}
if (cmd[0] =='b') { // (b) Backward
digitalWrite(IN1, LOW);
digitalWrite(IN2, HIGH);
digitalWrite(fwdLED, LOW);
digitalWrite(revLED, HIGH);
}
if (cmd[0] =='x') { // (x) Stop button
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
digitalWrite(fwdLED, LOW);
digitalWrite(revLED, LOW);
myMP3.loop(1);
Track = 1;
val = 0;
oldval = 0;
}
analogWrite(motorSpeedPin, val); // Throttle
The main differences are that in each sub section, there are commands to control the forward and reverse LED lights - turning them on (by setting the output to HIGH) or turning them off (by setting the output to LOW).
In addition, at the end of the STOP sub-section, in addition to turning off both LEDs and setting the speed (val) to zero, the player is instructed to play the first track (silence).
The final section of the Function, controls the SD player.The first part of this section controls what happens if the 'e' instruction is received from the Bluetooth app (ie when the Engine Start/Stop button is pressed).
if(cmd[0]=='e' && Track==1){
myMP3.play(3);
delay(10000);
myMP3.loop(4);//Play 'engine start-up' and then on to 'idle' (Track 4)
Track = 4;
cmd[0]='n';
}
if(cmd[0]=='e' && Track==4){
myMP3.play(12);
delay(7000);
myMP3.loop(1);//Play 'engine shut down' then on to silence (Track 1)
Track = 1;
cmd[0]='n';
}
The first 'if' statement checks whether the player is playing Track 1 (ie silence). If so it will play Track 3 (engine start-up) for seven seconds and then move on play the sound of the engine idling (ie track 4).
The second 'if' statement, checks whether Track 4 is playing (ie the idling sound). If so, it will play the engine shot-down track (Track 12) for seven seconds and then play silence (Track 1).
The next 'if' statement, detects whether the horn button has been pressed on the app. If so, it then checks which track is playing and plays the track following it - for example, if Track 6 (slow run) is playing, it will play track 7 for 3 seconds before returning to play Track 6.
if(cmd[0]=='h' && (Track==1 || Track==4 || Track==6 || Track==8 || Track==10)) {
myMP3.play(Track + 1); //Play relevant horn track
delay(3000);
myMP3.loop(Track);
}
Finally, the last set of 'if' statements change the track which is playing, dependent on the speed of the motor.
if(Track==4 && val>loSound){
myMP3.loop(6);
Track = 6;
}
if(Track==6 && val<loSound){
myMP3.loop(4);
Track = 4;
}
if(Track==6 && val>midSound){
myMP3.loop(8);
Track=8;
}
if(Track==8 && val<midSound){
myMP3.loop(6);
Track=6;
}
if(Track==8 && val>hiSound){
myMP3.loop(10);
Track=10;
}
if(Track==10 && val<hiSound){
myMP3.loop(8);
Track=8;
}
}
As you can see, if one of the trigger values for speed which were set at the beginning of the program is reached, it checks which track is playing and either moves up or down to the next relevant track.
Conclusion
Of source, it is not perfect. In an ideal world, the pitch and speed at which the sound of the engine is played would vary in proportion to the speed of the locomotive. This is not achievable when the sounds are tracks on an SD player. I did try playing a track which accelerated or decelerated the sound as the tracks changed, but this proved impossible to work reliably. Like the horn or the engine start/stop tracks, the Arduino was unable to do something else and, as these tracks were triggered by a speed change, it was unable to detect the new speed until the track finished playing - which either led to some very jerky running or completely confused the system as to which track ought to be playing. If someone with more programming knowledge than me is able to sort out this conundrum I will be very grateful.In the meantime, I am quite pleased with the outcome.
It took me several tries until I got the relative speeds of the sounds matched to the speed of the loco without making the jump from one track to the next too noticeable. If I had more time and more patience, I could add several more speed triggers and more tracks to make the increases and decreases in engine sound less noticeable - but I'll leave that to someone else.