/* ============================================================================================================== MrDIY is an locally-controlled over MQTT, internet-independent media notifier that can play MP3s, stream icecast radios, read text and play RTTTL ringtones without any external components. Connect your audio jack to Rx and GND - an external amplifier if required. You can build a simple amp with a single 1kΩ resistor and an NPN 2N3904 transistor, like this: 2N3904 (NPN) +---------+ | | +--| | E B C | / S | +-|--|--|-+ | P | | | +------+ E | | | | A | ESP8266-GND ------------------+ | +------+ K | | | | E | ESP8266-I2SOUT (Rx) -----/\/\/\--+ | \ R | 1kΩ | +--| USB 5V -----------------------------+ Commands, to: - Play MP3 MQTT topic: "/mqttFullTopic()/play" MQTT load: http://url-to-the-mp3-file/file.mp3 - Play Icecast Stream MQTT topic: "/mqttFullTopic()/stream" MQTT load: http://url-to-the-icecast-stream/file.mp3, example: http://22203.live.streamtheworld.com/WHTAFM.mp3 - Play Ringtone MQTT topic: "/mqttFullTopic()/tone" MQTT load: RTTTL formated text, example: Soap:d=8,o=5,b=125:g,a,c6,p,a,4c6,4p,a,g,e,c,4p,4g,a - Say Text MQTT topic: "/mqttFullTopic()/say" MQTT load: Text to be read, example: Hello There. How. Are. You? - Change Volume MQTT topic: "/mqttFullTopic()/volume" MQTT load: a double between 0.00 and 1.00, example: 0.7 - Stop Playing MQTT topic: "/mqttFullTopic()/stop" To get status: - The notifier sends status update on this MQTT topic: "/mqttFullTopic()/status" "playing" either paying an mp3, streaming, playing a ringtone or saying a text "idle" waiting for a command "error" error when receiving a command: example: MP3 file URL can't be loaded "connected" device just connected to MQTT server - The notifier plays a 2 second audio clip when it is first booted and connected to Wifi & MQTT To Upload to Wesmo D1 Mini: - Set CPU Frequency to 160MHz - Set IwIP to V2 Higher Bandwidth - Update the SSID info - Connect Rx to an AMP Dependencies: - ESP8266 https://github.com/esp8266/Arduino - ESP8266Audio https://github.com/earlephilhower/ESP8266Audio - ESP8266SAM https://github.com/earlephilhower/ESP8266SAM - IotWebConf https://github.com/prampec/IotWebConf - PubSubClient https://github.com/knolleary/pubsubclient Many thanks to all the authors and contributors to the above libraries - you have done an amazing job! For more info visit me at MrDIY.ca and find the Notifier Video on my YouTube channel: https://youtu.be/SPa9SMyPU58 ============================================================================================================== */ //#define DEBUG_FLAG #include "Arduino.h" #include "boot_sound.h" #include "ESP8266WiFi.h" #include "PubSubClient.h" #include "AudioFileSourceHTTPStream.h" #include "AudioFileSourceICYStream.h" #include "AudioFileSourcePROGMEM.h" #include "AudioFileSourceBuffer.h" #include "AudioGeneratorMP3.h" #include "AudioGeneratorWAV.h" #include "AudioGeneratorRTTTL.h" #include "AudioOutputI2SNoDAC.h" #include "ESP8266SAM.h" #include "IotWebConf.h" AudioGeneratorMP3 *mp3 = NULL; AudioGeneratorWAV *wav = NULL; AudioGeneratorRTTTL *rtttl = NULL; AudioFileSourceHTTPStream *file_http = NULL; AudioFileSourcePROGMEM *file_progmem = NULL; AudioFileSourceICYStream *file_icy = NULL; AudioFileSourceBuffer *buff = NULL; AudioOutputI2SNoDAC *out = NULL; WiFiClient wifiClient; PubSubClient mqttClient(wifiClient); #define port 1883 #define MQTT_MSG_SIZE 256 // AudioRelated --------------------------- float volume_level = 0.8; String playing_status; const int preallocateBufferSize = 2048; void *preallocateBuffer = NULL; byte i; // WifiManager ----------------------------- #define thingName "MrDIY Notifier" #define wifiInitialApPassword "mrdiy.ca" char mqttServer[16]; char mqttUserName[32]; char mqttUserPassword[32]; char mqttTopicPrefix[32]; char mqttTopic[MQTT_MSG_SIZE]; DNSServer dnsServer; WebServer server(80); IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword, "mrd2"); IotWebConfParameter mqttServerParam = IotWebConfParameter("MQTT server", "mqttServer", mqttServer, sizeof(mqttServer) ); IotWebConfParameter mqttUserNameParam = IotWebConfParameter("MQTT username", "mqttUser", mqttUserName, sizeof(mqttUserName)); IotWebConfParameter mqttUserPasswordParam = IotWebConfParameter("MQTT password", "mqttPass", mqttUserPassword, sizeof(mqttUserPassword), "password"); IotWebConfParameter mqttTopicParam = IotWebConfParameter("MQTT Topic", "mqttTopic", mqttTopicPrefix, sizeof(mqttTopicPrefix)); /* ################################## Setup ############################################# */ void setup() { #ifdef DEBUG_FLAG Serial.begin(115200); #endif iotWebConf.addParameter(&mqttServerParam); iotWebConf.addParameter(&mqttUserNameParam); iotWebConf.addParameter(&mqttUserPasswordParam); iotWebConf.addParameter(&mqttTopicParam); iotWebConf.setWifiConnectionCallback(&wifiConnected); iotWebConf.setFormValidator(&formValidator); iotWebConf.setStatusPin(LED_BUILTIN); iotWebConf.skipApStartup(); boolean validConfig = iotWebConf.init(); if (!validConfig) { mqttServer[0] = '\0'; mqttUserName[0] = '\0'; mqttUserPassword[0] = '\0'; } server.on("/", [] { iotWebConf.handleConfig(); }); server.onNotFound([] { iotWebConf.handleNotFound(); }); out = new AudioOutputI2SNoDAC(); out->SetGain(volume_level); } /* ##################################### Loop ############################################# */ void loop() { mqttReconnect(); mqttClient.loop(); // give processor priority to audio if (!mp3) iotWebConf.doLoop(); if (mp3 && !mp3->loop()) stopPlaying(); if (wav && !wav->loop()) stopPlaying(); if (rtttl && !rtttl->loop()) stopPlaying(); #ifdef DEBUG_FLAG if (mp3 && mp3->isRunning()) { static int unsigned long lastms = 0; if (millis() - lastms > 1000) { lastms = millis(); Serial.print(F("Free: ")); Serial.print(ESP.getFreeHeap(), DEC); Serial.print(F(" (")); Serial.print( ESP.getFreeHeap() - ESP.getMaxFreeBlockSize(), DEC); Serial.print(F(" lost)")); Serial.print(F(" Fragmentation: ")); Serial.print(ESP.getHeapFragmentation(), DEC); Serial.print(F("%")); if ( ESP.getHeapFragmentation() > 40) Serial.print(F(" ----------- ")); Serial.println(); } } #endif } /* ############################### Audio ############################################ */ void playBootSound() { file_progmem = new AudioFileSourcePROGMEM(boot_sound, sizeof(boot_sound)); wav = new AudioGeneratorWAV(); wav->begin(file_progmem, out); } void stopPlaying() { if (mp3) { #ifdef DEBUG_FLAG Serial.print(F("...#")); Serial.println(F("Interrupted!")); Serial.println(); #endif mp3->stop(); delete mp3; mp3 = NULL; } if (wav) { wav->stop(); delete wav; wav = NULL; } if (rtttl) { rtttl->stop(); delete rtttl; rtttl = NULL; } if (buff) { buff->close(); delete buff; buff = NULL; } if (file_http) { file_http->close(); delete file_http; file_http = NULL; } if (file_progmem) { file_progmem->close(); delete file_progmem; file_progmem = NULL; } if (file_icy) { file_icy->close(); delete file_icy; file_icy = NULL; } broadcastStatus("idle"); } /* ################################## MQTT ############################################### */ void onMqttMessage(char* topic, byte* payload, unsigned int length) { char newMsg[MQTT_MSG_SIZE]; if (length > 0) { memset(newMsg, '\0' , sizeof(newMsg)); memcpy(newMsg, payload, length); #ifdef DEBUG_FLAG Serial.println(); Serial.print(F("Requested [")); Serial.print(topic); Serial.print(F("] ")); Serial.println(newMsg); #endif // got a new URL to play ------------------------------------------------ if ( !strcmp(topic, mqttFullTopic("play") ) ) { stopPlaying(); file_http = new AudioFileSourceHTTPStream(); if ( file_http->open(newMsg)) { broadcastStatus("playing"); buff = new AudioFileSourceBuffer(file_http, preallocateBuffer, preallocateBufferSize); mp3 = new AudioGeneratorMP3(); mp3->begin(buff, out); } else { stopPlaying(); broadcastStatus("error"); broadcastStatus("idle"); } } // got a new URL to play ------------------------------------------------ if ( !strcmp(topic, mqttFullTopic("stream"))) { stopPlaying(); file_icy = new AudioFileSourceICYStream(); if ( file_icy->open(newMsg)) { broadcastStatus("playing"); buff = new AudioFileSourceBuffer(file_icy, preallocateBuffer, preallocateBufferSize); mp3 = new AudioGeneratorMP3(); mp3->begin(buff, out); } else { stopPlaying(); broadcastStatus("error"); broadcastStatus("idle"); } } // got a tone request -------------------------------------------------- if ( !strcmp(topic, mqttFullTopic("tone") ) ) { stopPlaying(); broadcastStatus("playing"); file_progmem = new AudioFileSourcePROGMEM( newMsg, sizeof(newMsg) ); rtttl = new AudioGeneratorRTTTL(); rtttl->begin(file_progmem, out); broadcastStatus("idle"); } //got a TTS request ---------------------------------------------------- if ( !strcmp(topic, mqttFullTopic("say"))) { stopPlaying(); broadcastStatus("playing"); ESP8266SAM *sam = new ESP8266SAM; sam->Say(out, newMsg); delete sam; stopPlaying(); broadcastStatus("idle"); } // got a volume request, expecting double [0.0,1.0] --------------------- if ( !strcmp(topic, mqttFullTopic("volume"))) { volume_level = atof(newMsg); if ( volume_level < 0.0 ) volume_level = 0; if ( volume_level > 1.0 ) volume_level = 0.7; out->SetGain(volume_level); } // got a stop request -------------------------------------------------- if ( !strcmp(topic, mqttFullTopic("stop"))) { stopPlaying(); } } } void broadcastStatus(String msg) { if ( playing_status != msg) { char charBuf[msg.length() + 1]; msg.toCharArray(charBuf, msg.length() + 1); mqttClient.publish(mqttFullTopic("status"), charBuf); playing_status = msg; #ifdef DEBUG_FLAG Serial.println(); Serial.print("status: "); Serial.println(msg); #endif } } void mqttReconnect() { if (!mqttClient.connected()) { if (mqttClient.connect("MrDIY Notifier", mqttUserName, mqttUserPassword)) { broadcastStatus("connected"); mqttClient.subscribe(mqttFullTopic("play")); mqttClient.subscribe(mqttFullTopic("stream")); mqttClient.subscribe(mqttFullTopic("tone")); mqttClient.subscribe(mqttFullTopic("say")); mqttClient.subscribe(mqttFullTopic("stop")); mqttClient.subscribe(mqttFullTopic("volume")); #ifdef DEBUG_FLAG Serial.println(); Serial.print(F("Connected to MQTT: ")); Serial.println(F("mrdiynotifier")); #endif broadcastStatus("idle"); } } } /* ############################ WifiManager ############################################# */ void wifiConnected() { #ifdef DEBUG_FLAG Serial.println(); Serial.println(F("==================================================================")); Serial.println(F(" MrDIY Notifier")); Serial.println(F("==================================================================")); Serial.println(); Serial.print(F("Connected to Wifi [")); Serial.print(WiFi.localIP()); Serial.println(F("]")); #endif playBootSound(); mqttClient.setServer(mqttServer, port); mqttClient.setCallback(onMqttMessage); mqttClient.setBufferSize(MQTT_MSG_SIZE); mqttReconnect(); } boolean formValidator() { boolean valid = true; int l = server.arg(mqttServerParam.getId()).length(); if (l == 0) { mqttServerParam.errorMessage = "Please provide the MQTT server address"; valid = false; } return valid; } char* mqttFullTopic(char action[]) { strcpy (mqttTopic, "/"); strcat (mqttTopic, mqttTopicPrefix); strcat (mqttTopic, "/"); strcat (mqttTopic, action); return mqttTopic; }