//// SD card setup
// MP3/WAV files need to be named cafefully:
// All main music files should be in the root folder of the SD card.
// All advert music files should be in a folder called "ADVERT".
// Each file should have a 4 digit zero padded number, optionally with extra naming appended after, with no gaps in numbering e.g.:
// 0001.mp3
// 0002-River Sounds.mp3
//
//// IMPORTANT: Files need uploading to the SD card in their numbered order ////
//
// Bulk Rename Utility is a good program for numbering files.
////
//// DMX input
// Channel 1 (Track select):
// 0-119: Single play x track number
// 120-239: Repeat play x - 120 (i.e. 0-119) track number
// 251: Random play (Possibly always starts with the first track when triggered)
// 252: Unpause (Return to last playing track position)
// 253: Pause (Allow returning to current track position)
// 254: Stop
// 255: Continue (Default/null state)
//
// Channel 2 (Advert select):
// 0-239: Play advert track number
// 254: Return to normal play immediately
// 255: Finish advert then return (Default/null state)
//
// Channel 3 (Volume):
// 0-30: Volume setting
// 31-255: Keep current volume (Default/null state)
////
//// Notes
// The main music files can either be played a single time, returning to silence afterwards, or looped continuously.
// Advert music files are ones that can interrupt the main files to play once,
// and return to the same position in the interrupted file when finished.
//
// Adverts will only play if a normal track is currently playing.
// Can be helped with a silent helper track to start playing before the advert.
//
// Either keeping a channel's DMX value set to its previous value or to 255 will act as a null setting
// that doesn't do anything and keeps the current state of this module.
//
// Select the single play version of the currently looping track to stop play after the current loop,
// otherwise set stop to stop immediately, then set track to play the desired track.
//
// Using WAV files instead of MP3s might give smoother results
//
// There is also an optional gain setting, possibly allowing louder volumes.
// And also an EQ setting.
//
// The inbuilt led in the Arduino flashes to indicate an error, then reboots after 4 seconds and tries again.
////
//////// Config ////////
// No debug logging since we're using the usb serial for the Max485 chip instead
// Set this to a unique address in the range 0-169
#define MODULE_ADDRESS 0
// Uses 3 channels for each module, uses above address to set starting DMX address for this module (Don't change this)
#define DMX_START_ADDRESS MODULE_ADDRESS * 3 + 1
#define COMMAND_DELAY 20 // ms to wait after a command is run to allow the DFPlayer to handle it fully
//// Pins
// DFPlayer Mini:
// The RX line needs either a 1k resistor or a level shifter since its logic is 3.3v and the nano is 5v
// TX/RX of the mini can only be connected to pins 8/9 since using AltSoftSerial for better software serial
// The busy pin can be any other D or A pin except 1/A6/A7
#define DFPLAYER_BUSY_PIN 10
// Max-485:
// RO has to be D0(RX0) for hardware serial
// Needs either a pullup resistor on its RE pin, or needs unplugging from the nano when it gets programmed
// RE (Receive Enable) pin can be any other D or A pin except 1/A6/A7
#define MAX485_ENABLE_PIN 2
#define DEFAULT_VOLUME 10 // Initial volume value, 0 to 30
//////// End Config ////////
// DFPlayer info: https://wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299
// https://github.com/DFRobot/DFRobotDFPlayerMini/blob/master/doc/FN-M16P%2BEmbedded%2BMP3%2BAudio%2BModule%2BDatasheet.pdf
// TODO: Potentially use https://github.com/PowerBroker2/DFPlayerMini_Fast
// TODO: Check how many files are in root folder and limit selection to just them, make sure this doesn't count files in the advert folder
// myDFPlayer.readFileCounts()
// TODO: Check actual behaviour of each transition between all the possible states
// TODO: What happens if try to play a non-existant file?
// TODO: Maybe handle command when not playing by starting a silent track to allow advert to play?
#define TRACK_ADDRESS (DMX_START_ADDRESS)
#define ADVERT_ADDRESS (DMX_START_ADDRESS)+1
#define VOLUME_ADDRESS (DMX_START_ADDRESS)+2
#define NULL_VAULE 255
#include <AltSoftSerial.h>
#include <DFRobotDFPlayerMini.h>
#include <DMXSerial.h>
#include <avr/wdt.h>
AltSoftSerial DFSerial; // RX,TX = 8,9
DFRobotDFPlayerMini myDFPlayer;
typedef struct dmx_data_t {
uint8_t track_value;
uint8_t advert_value;
uint8_t volume;
} dmx_data_t;
dmx_data_t prevData;
uint8_t currTrack = NULL_VAULE;
uint8_t pauseTrack = NULL_VAULE;
// Flashes the inbuilt LED to indicate an error until 4 second watchdog timer resets the device
void errorFlashReset(int interval) {
wdt_enable(WDTO_4S);
while (true) {
digitalWrite(LED_BUILTIN, HIGH);
delay(interval);
digitalWrite(LED_BUILTIN, LOW);
delay(interval);
}
}
bool isPlaying() {
return digitalRead(DFPLAYER_BUSY_PIN) == LOW;
}
void handleVolume(uint8_t newValue) {
if (newValue != prevData.volume) {
if (newValue <= 30) {
myDFPlayer.volume(newValue);
delay(COMMAND_DELAY);
}
prevData.volume = newValue;
}
}
void handleTrack(uint8_t newValue) {
if (newValue != prevData.track_value) {
if (newValue < 120) {
// Single play numbered track
if (newValue == currTrack && pauseTrack == NULL_VAULE && isPlaying()) {
// Changed to single play for the currently playing track, so disable looping the current track
myDFPlayer.disableLoop();
} else {
myDFPlayer.play(newValue);
currTrack = newValue;
pauseTrack = NULL_VAULE;
}
} else if (newValue < 240) {
// Repeat play numbered track
myDFPlayer.loop(newValue - 120);
currTrack = newValue - 120;
pauseTrack = NULL_VAULE;
} else if (newValue == 251) {
// Random play
myDFPlayer.randomAll();
currTrack = NULL_VAULE;
pauseTrack = NULL_VAULE;
} else if (newValue == 252) {
// Unpause
if (pauseTrack != NULL_VAULE) {
myDFPlayer.start();
pauseTrack = NULL_VAULE;
}
} else if (newValue == 253) {
// Pause
if (isPlaying()) {
myDFPlayer.pause();
pauseTrack = currTrack;
}
} else if (newValue == 254) {
// Stop
myDFPlayer.stop();
currTrack = NULL_VAULE;
pauseTrack = NULL_VAULE;
} else {
// A null value so not sending any commands and not waiting
prevData.track_value = newValue;
return;
}
prevData.track_value = newValue;
delay(COMMAND_DELAY);
}
}
void handleAdvert(uint8_t newValue) {
if (newValue != prevData.advert_value) {
// Won't play advert unless a normal track is already currently playing
if (isPlaying()) {
if (newValue < 240) {
myDFPlayer.advertise(newValue);
} else if (newValue == 254) {
myDFPlayer.stopAdvertise();
} else {
// A null value so not sending any commands and not waiting
prevData.advert_value = newValue;
return;
}
delay(COMMAND_DELAY);
}
prevData.advert_value = newValue;
}
}
void setup() {
DFSerial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
pinMode(DFPLAYER_BUSY_PIN, INPUT_PULLUP);
if (!myDFPlayer.begin(DFSerial, /*isACK = */true, /*doReset = */true)) {
errorFlashReset(200);
}
delay(COMMAND_DELAY);
myDFPlayer.volume(DEFAULT_VOLUME); //Set initial volume value. From 0 to 30
delay(COMMAND_DELAY);
// Enable DMX input after boot to allow serial to be used for flashing properly
pinMode(MAX485_ENABLE_PIN, OUTPUT);
digitalWrite(MAX485_ENABLE_PIN, LOW);
DMXSerial.init(DMXReceiver, NOT_A_PIN); // Disable mode toggle pin handling in library since we're doing it ourselves
// Set default values for our DMX channels
DMXSerial.write(TRACK_ADDRESS, NULL_VAULE);
prevData.track_value = NULL_VAULE;
DMXSerial.write(ADVERT_ADDRESS, NULL_VAULE);
prevData.advert_value = NULL_VAULE;
DMXSerial.write(VOLUME_ADDRESS, NULL_VAULE);
prevData.volume = NULL_VAULE;
}
void loop() {
if (DMXSerial.dataUpdated()) {
DMXSerial.resetUpdated();
dmx_data_t newData = {
DMXSerial.read(TRACK_ADDRESS),
DMXSerial.read(ADVERT_ADDRESS),
DMXSerial.read(VOLUME_ADDRESS)
};
handleVolume(newData.volume);
handleTrack(newData.track_value);
handleAdvert(newData.advert_value);
}
delay(10);
}