-
-
Save NonaSuomy/e50cfdf079fe1bbbcac30a381e34fd93 to your computer and use it in GitHub Desktop.
//****************************************************************************************** | |
//* Esp_radio -- Webradio receiver for ESP8266, 1.8 color display and VS1053 MP3 module, * | |
//* by Ed Smallenburg ([email protected]) * | |
//* With ESP8266 running at 80 MHz, it is capable of handling up to 256 kb bitrate. * | |
//* With ESP8266 running at 160 MHz, it is capable of handling up to 320 kb bitrate. * | |
//****************************************************************************************** | |
// ESP8266 libraries used: | |
// - ESP8266WiFi - Part of ESP8266 Arduino default libraries. | |
// - SPI - Part of Arduino default libraries. | |
// - Adafruit_GFX - https://github.com/adafruit/Adafruit-GFX-Library | |
// - TFT_ILI9163C - https://github.com/sumotoy/TFT_ILI9163C | |
// - ESPAsyncTCP - https://github.com/me-no-dev/ESPAsyncTCP | |
// - ESPAsyncWebServer - https://github.com/me-no-dev/ESPAsyncWebServer | |
// - FS - https://github.com/esp8266/arduino-esp8266fs-plugin/releases/download/0.2.0/ESP8266FS-0.2.0.zip | |
// - ArduinoOTA - Part of ESP8266 Arduino default libraries. | |
// - AsyncMqttClient - https://github.com/marvinroger/async-mqtt-client | |
// - TinyXML - Fork https://github.com/adafruit/TinyXML | |
// | |
// A library for the VS1053 (for ESP8266) is not available (or not easy to find). Therefore | |
// a class for this module is derived from the maniacbug library and integrated in this sketch. | |
// | |
// See http://www.internet-radio.com for suitable stations. Add the stations of your choice | |
// to the .ini-file. | |
// | |
// Brief description of the program: | |
// First a suitable WiFi network is found and a connection is made. | |
// Then a connection will be made to a shoutcast server. The server starts with some | |
// info in the header in readable ascii, ending with a double CRLF, like: | |
// icy-name:Classic Rock Florida - SHE Radio | |
// icy-genre:Classic Rock 60s 70s 80s Oldies Miami South Florida | |
// icy-url:http://www.ClassicRockFLorida.com | |
// content-type:audio/mpeg | |
// icy-pub:1 | |
// icy-metaint:32768 - Metadata after 32768 bytes of MP3-data | |
// icy-br:128 - in kb/sec (for Ogg this is like "icy-br=Quality 2" | |
// | |
// After de double CRLF is received, the server starts sending mp3- or Ogg-data. For mp3, this | |
// data may contain metadata (non mp3) after every "metaint" mp3 bytes. | |
// The metadata is empty in most cases, but if any is available the content will be presented on the TFT. | |
// Pushing the input button causes the player to select the next preset station present in the .ini file. | |
// | |
// The display used is a Chinese 1.8 color TFT module 128 x 160 pixels. The TFT_ILI9163C.h | |
// file has been changed to reflect this particular module. TFT_ILI9163C.cpp has been | |
// changed to use the full screenwidth if rotated to mode "3". Now there is room for 26 | |
// characters per line and 16 lines. Software will work without installing the display. | |
// If no TFT is used, you may use GPIO2 and GPIO15 as control buttons. See definition of "USETFT" below. | |
// Switches are than programmed as: | |
// GPIO2 : "Goto station 1" | |
// GPIO0 : "Next station" | |
// GPIO15: "Previous station". Note that GPIO15 has to be LOW when starting the ESP8266. | |
// The button for GPIO15 must therefore be connected to VCC (3.3V) instead of GND. | |
// | |
// For configuration of the WiFi network(s): see the global data section further on. | |
// | |
// The SPI interface for VS1053 and TFT uses hardware SPI. | |
// | |
// Wiring: | |
// NodeMCU GPIO Pin to program Wired to LCD Wired to VS1053 Wired to rest | |
// ------- ------ -------------- --------------- ------------------- --------------------- | |
// D0 GPIO16 16 - pin 1 DCS - | |
// D1 GPIO5 5 - pin 2 CS LED on nodeMCU | |
// D2 GPIO4 4 - pin 4 DREQ - | |
// D3 GPIO0 0 FLASH - - Control button "Next station" | |
// D4 GPIO2 2 pin 3 (D/C) - (OR)Control button "Station 1" | |
// D5 GPIO14 14 SCLK pin 5 (CLK) pin 5 SCK - | |
// D6 GPIO12 12 MISO - pin 7 MISO - | |
// D7 GPIO13 13 MOSI pin 4 (DIN) pin 6 MOSI - | |
// D8 GPIO15 15 pin 2 (CS) - (OR)Control button "Previous station" | |
// D9 GPI03 3 RXD0 - - Reserved serial input | |
// D10 GPIO1 1 TXD0 - - Reserved serial output | |
// ------- ------ -------------- --------------- ------------------- --------------------- | |
// GND - - pin 8 (GND) pin 8 GND Power supply | |
// VCC 3.3 - - pin 6 (VCC) - LDO 3.3 Volt | |
// VCC 5 V - - pin 7 (BL) pin 9 5V Power supply | |
// RST - - pin 1 (RST) pin 3 RESET Reset circuit | |
// | |
// The reset circuit is a circuit with 2 diodes to GPIO5 and GPIO16 and a resistor to ground | |
// (wired OR gate) because there was not a free GPIO output available for this function. | |
// This circuit is included in the documentation. | |
// Issues: | |
// Webserver produces error "LmacRxBlk:1" after some time. After that it will work very slow. | |
// The program will reset the ESP8266 in such a case. Now we have switched to async webserver, | |
// the problem still exists, but the program will not crash anymore. | |
// Upload to ESP8266 not reliable. | |
// | |
// 31-03-2016, ES: First set-up. | |
// 01-04-2016, ES: Detect missing VS1053 at start-up. | |
// 05-04-2016, ES: Added commands through http server on port 80. | |
// 14-04-2016, ES: Added icon and switch preset on stream error. | |
// 18-04-2016, ES: Added SPIFFS for webserver. | |
// 19-04-2016, ES: Added ringbuffer. | |
// 20-04-2016, ES: WiFi Passwords through SPIFFS files, enable OTA. | |
// 21-04-2016, ES: Switch to Async Webserver. | |
// 27-04-2016, ES: Save settings, so same volume and preset will be used after restart. | |
// 03-05-2016, ES: Add bass/treble settings (see also new index.html). | |
// 04-05-2016, ES: Allow stations like "skonto.ls.lv:8002/mp3". | |
// 06-05-2016, ES: Allow hiddens WiFi station if this is the only .pw file. | |
// 07-05-2016, ES: Added preset selection in webserver. | |
// 12-05-2016, ES: Added support for Ogg-encoder. | |
// 13-05-2016, ES: Better Ogg detection. | |
// 17-05-2016, ES: Analog input for commands, extra buttons if no TFT required. | |
// 26-05-2016, ES: Fixed BUTTON3 bug (no TFT). | |
// 27-05-2016, ES: Fixed restore station at restart. | |
// 04-07-2016, ES: WiFi.disconnect clears old connection now (thanks to Juppit). | |
// 23-09-2016, ES: Added commands via MQTT and Serial input, Wifi set-up in AP mode. | |
// 04-10-2016, ES: Configuration in .ini file. No more use of EEPROM and .pw files. | |
// 11-10-2016, ES: Allow stations that have no bitrate in header like icecast.err.ee/raadio2.mp3. | |
// 14-10-2016, ES: Updated for async-mqtt-client-master 0.5.0 | |
// 22-10-2016, ES: Correction mute/unmute. | |
// 15-11-2016, ES: Support for .m3u playlists. | |
// 22-12-2016, ES: Support for localhost (play from SPIFFS). | |
// 28-12-2016, ES: Implement "Resume" request. | |
// 31-12-2016, ES: Allow ContentType "text/css". | |
// 02-01-2017, ES: Webinterface in PROGMEM. | |
// 16-01-2017, ES: Correction playlists. | |
// 17-01-2017, ES: Bugfix config page and playlist. | |
// 23-01-2017, ES: Bugfix playlist. | |
// 26-01-2017, ES: Check on wrong icy-metaint. | |
// 30-01-2017, ES: Allow chunked transfer encoding. | |
// 01-02-2017, ES: Bugfix file upload. | |
// 26-04-2017, ES: Better output webinterface on preset change. | |
// 03-05-2017, ES: Prevent to start inputstream if no network. | |
// 04-05-2017, ES: Integrate iHeartRadio, thanks to NonaSuomy. | |
// 09-05-2017, ES: Fixed abs problem. | |
// 11-05-2017, ES: Convert UTF8 characters before display, thanks to everyb313. | |
// 24-05-2017, ES: Correction. Do not skip first part of .mp3 file. | |
// 26-05-2017, ES: Correction playing from .m3u playlist and LC/UC problem. | |
// 31-05-2017, ES: Volume indicator on TFT. | |
// | |
// Define the version number, also used for webserver as Last-Modified header: | |
#define VERSION "Wed, 31 May 2017 12:35:00 GMT" | |
// TFT. Define USETFT if required. | |
//#define USETFT | |
#include <ESP8266WiFi.h> | |
#include <ESPAsyncTCP.h> | |
#include <ESPAsyncWebServer.h> | |
#include <AsyncMqttClient.h> | |
#include <SPI.h> | |
#if defined ( USETFT ) | |
#include <Adafruit_GFX.h> | |
#include <TFT_ILI9163C.h> | |
#endif | |
#include <Ticker.h> | |
#include <stdio.h> | |
#include <string.h> | |
#include <FS.h> | |
#include <ArduinoOTA.h> | |
#include <TinyXML.h> | |
extern "C" | |
{ | |
#include "user_interface.h" | |
} | |
// Definitions for 3 control switches on analog input | |
// You can test the analog input values by holding down the switch and select /?analog=1 | |
// in the web interface. See schematics in the documentation. | |
// Switches are programmed as "Goto station 1", "Next station" and "Previous station" respectively. | |
// Set these values to 2000 if not used or tie analog input to ground. | |
#define NUMANA 3 // Number of analog pins. | |
//#define asw1 437 // Go to first preset. | |
//#define asw2 507 // Go to next preset. | |
//#define asw3 875 // Go to previous preset. | |
#define asw1 2000 | |
#define asw2 2000 | |
#define asw3 2000 | |
// Color definitions for the TFT screen (if used) | |
#define BLACK 0x0000 | |
#define BLUE 0xF800 | |
#define RED 0x001F | |
#define GREEN 0x07E0 | |
#define CYAN GREEN | BLUE | |
#define MAGENTA RED | BLUE | |
#define YELLOW RED | GREEN | |
#define WHITE BLUE | RED | GREEN | |
// Digital I/O used | |
// Pins for VS1053 module | |
#define VS1053_CS 5 | |
#define VS1053_DCS 16 | |
#define VS1053_DREQ 4 | |
// Pins CS and DC for TFT module (if used, see definition of "USETFT") | |
#define TFT_CS 15 | |
#define TFT_DC 2 | |
// Control button (GPIO) for controlling station | |
#define BUTTON1 2 | |
#define BUTTON2 0 | |
#define BUTTON3 15 | |
// Ringbuffer for smooth playing. 20000 bytes is 160 Kbits, about 1.5 seconds at 128kb bitrate. | |
#define RINGBFSIZ 20000 | |
// Debug buffer size | |
#define DEBUG_BUFFER_SIZE 100 | |
// Name of the ini file | |
#define INIFILENAME "/radio.ini" | |
// Access point name if connection to WiFi network fails. Also the hostname for WiFi and OTA. | |
// Not that the password of an AP must be at least as long as 8 characters. | |
// Also used for other naming. | |
#define NAME "ESPRadio" | |
// Maximum number of MQTT reconnects before give-up | |
#define MAXMQTTCONNECTS 20 | |
// | |
// Subscription topics for MQTT. The topic will be pefixed by "PREFIX/", where PREFIX is replaced | |
// by the the mqttprefix in the preferences. The next definition will yield the topic "espradio/command" | |
// if mqttprefix is "espradio". | |
#define MQTT_SUBTOPIC "command" // Command to receive from MQTT | |
// | |
//****************************************************************************************** | |
// Forward declaration of various functions * | |
//****************************************************************************************** | |
void displayinfo ( const char* str, uint16_t pos, uint16_t height, uint16_t color ) ; | |
void showstreamtitle ( const char* ml, bool full = false ) ; | |
void handlebyte ( uint8_t b, bool force = false ) ; | |
void handlebyte_ch ( uint8_t b, bool force = false ) ; | |
void handleFS ( AsyncWebServerRequest* request ) ; | |
void handleFSf ( AsyncWebServerRequest* request, const String& filename ) ; | |
void handleCmd ( AsyncWebServerRequest* request ) ; | |
void handleFileUpload ( AsyncWebServerRequest* request, String filename, | |
size_t index, uint8_t* data, size_t len, bool final ) ; | |
char* dbgprint( const char* format, ... ) ; | |
char* analyzeCmd ( const char* str ) ; | |
char* analyzeCmd ( const char* par, const char* val ) ; | |
String chomp ( String str ) ; | |
void mqttpublish( String topic, uint8_t qos, bool retain, String message ) ; | |
String xmlparse ( String mount ) ; | |
// | |
//****************************************************************************************** | |
// Global data section. * | |
//****************************************************************************************** | |
// There is a block ini-data that contains some configuration. Configuration data is * | |
// saved in the SPIFFS file radio.ini by the webinterface. On restart the new data will * | |
// de read from this file. * | |
// Items in ini_block can be changed by commands from webserver/MQTT/Serial. * | |
//****************************************************************************************** | |
struct ini_struct | |
{ | |
String mqttbroker ; // The name of the MQTT broker server | |
String mqttprefix ; // Prefix to use for topics | |
uint16_t mqttport ; // Port, default 1883 | |
String mqttuser ; // User for MQTT authentication | |
String mqttpasswd ; // Password for MQTT authentication | |
String mqtttopic ; // Topic to subscribe to | |
uint8_t reqvol ; // Requested volume | |
uint8_t rtone[4] ; // Requested bass/treble settings | |
int8_t newpreset ; // Requested preset | |
String ssid ; // SSID of WiFi network to connect to | |
String passwd ; // Password for WiFi network | |
} ; | |
struct mqttpub_struct // For MQTT publishing | |
{ | |
const char* topic ; // Topic as partial string (without prefix) | |
String* payload ; // Payload for this topic | |
bool trigger ; | |
} ; | |
enum datamode_t { INIT = 1, HEADER = 2, DATA = 4, | |
METADATA = 8, PLAYLISTINIT = 16, | |
PLAYLISTHEADER = 32, PLAYLISTDATA = 64, | |
STOPREQD = 128, STOPPED = 256 | |
} ; // State for datastream | |
// Global variables | |
int DEBUG = 1 ; // Debug on/off. | |
ini_struct ini_block ; // Holds configurable data | |
WiFiClient mp3client ; // An instance of the mp3 client | |
AsyncWebServer cmdserver ( 80 ) ; // Instance of embedded webserver on port 80 | |
AsyncMqttClient mqttclient ; // Client for MQTT subscriber | |
IPAddress mqtt_server_IP ; // IP address of MQTT broker | |
char cmd[130] ; // Command from MQTT or Serial | |
#if defined ( USETFT ) | |
TFT_ILI9163C tft = TFT_ILI9163C ( TFT_CS, TFT_DC ) ; | |
#endif | |
Ticker tckr ; // For timing 100 msec | |
TinyXML xml; // For XML parser. | |
uint32_t totalcount = 0 ; // Counter mp3 data | |
datamode_t datamode ; // State of datastream | |
int metacount ; // Number of bytes in metadata | |
int datacount ; // Counter databytes before metadata | |
String metaline ; // Readable line in metadata | |
String icystreamtitle ; // Streamtitle from metadata | |
String icyname ; // Icecast station name | |
String ipaddress ; // Own IP-address. | |
String icyurl ; // Icecast station url. | |
String icygenre ; // Icecast station genre. | |
String icydescription ; // Icecast station description. | |
String icybr ; // Icecast station bitrate. | |
String icysr ; // Icecast station sr. | |
String contenttype ; // Stream content type. | |
String state ; // Player state playing, stopped, paused, etc. | |
String nowplaying ; // Name and Title. | |
int bitrate ; // Bitrate in kb/sec | |
int metaint = 0 ; // Number of databytes between metadata | |
int8_t currentpreset = -1 ; // Preset station playing | |
String curvolm ; // Current Volume. | |
String curprem ; // Current Preset. | |
String host ; // The URL to connect to or file to play | |
String playlist ; // The URL of the specified playlist | |
bool xmlreq = false ; // Request for XML parse. | |
bool hostreq = false ; // Request for new host | |
bool reqtone = false ; // New tone setting requested | |
bool muteflag = false ; // Mute output | |
uint8_t* ringbuf ; // Ringbuffer for VS1053 | |
uint16_t rbwindex = 0 ; // Fill pointer in ringbuffer | |
uint16_t rbrindex = RINGBFSIZ - 1 ; // Emptypointer in ringbuffer | |
uint16_t rcount = 0 ; // Number of bytes in ringbuffer | |
uint16_t analogsw[NUMANA] = { asw1, asw2, asw3 } ; // 3 levels of analog input | |
uint16_t analogrest ; // Rest value of analog input | |
bool resetreq = false ; // Request to reset the ESP8266 | |
bool NetworkFound = false ; // True if WiFi network connected | |
bool mqtt_on = false ; // MQTT in use | |
String networks ; // Found networks | |
String anetworks ; // Aceptable networks (present in .ini file) | |
String presetlist ; // List for webserver | |
uint8_t num_an ; // Number of acceptable networks in .ini file | |
String testfilename = "" ; // File to test (SPIFFS speed) | |
uint16_t mqttcount = 0 ; // Counter MAXMQTTCONNECTS | |
int8_t playlist_num = 0 ; // Nonzero for selection from playlist | |
File mp3file ; // File containing mp3 on SPIFFS | |
bool localfile = false ; // Play from local mp3-file or not | |
bool chunked = false ; // Station provides chunked transfer | |
int chunkcount = 0 ; // Counter for chunked transfer | |
// XML parse globals. | |
const char* xmlhost = "playerservices.streamtheworld.com" ;// XML data source | |
const char* xmlget = "GET /api/livestream" // XML get parameters | |
"?version=1.9" // API Version of IHeartRadio | |
"&mount=%sAAC" // MountPoint with Station Callsign | |
"&lang=en" ; // Language | |
int xmlport = 80 ; // XML Port | |
uint8_t xmlbuffer[150] ; // For XML decoding | |
String xmlOpen ; // Opening XML tag | |
String xmlTag ; // Current XML tag | |
String xmlData ; // Data inside tag | |
String stationServer( "" ) ; // Radio stream server | |
String stationPort( "" ) ; // Radio stream port | |
String stationMount( "" ) ; // Radio stream Callsign | |
//****************************************************************************************** | |
// End of global data section. * | |
//****************************************************************************************** | |
//****************************************************************************************** | |
// Pages and CSS for the webinterface. * | |
//****************************************************************************************** | |
#include "about_html.h" | |
#include "config_html.h" | |
#include "index_html.h" | |
#include "radio_css.h" | |
#include "favicon_ico.h" | |
// | |
//****************************************************************************************** | |
// VS1053 stuff. Based on maniacbug library. * | |
//****************************************************************************************** | |
// VS1053 class definition. * | |
//****************************************************************************************** | |
class VS1053 | |
{ | |
private: | |
uint8_t cs_pin ; // Pin where CS line is connected | |
uint8_t dcs_pin ; // Pin where DCS line is connected | |
uint8_t dreq_pin ; // Pin where DREQ line is connected | |
uint8_t curvol ; // Current volume setting 0..100% | |
const uint8_t vs1053_chunk_size = 32 ; | |
// SCI Register | |
const uint8_t SCI_MODE = 0x0 ; | |
const uint8_t SCI_BASS = 0x2 ; | |
const uint8_t SCI_CLOCKF = 0x3 ; | |
const uint8_t SCI_AUDATA = 0x5 ; | |
const uint8_t SCI_WRAM = 0x6 ; | |
const uint8_t SCI_WRAMADDR = 0x7 ; | |
const uint8_t SCI_AIADDR = 0xA ; | |
const uint8_t SCI_VOL = 0xB ; | |
const uint8_t SCI_AICTRL0 = 0xC ; | |
const uint8_t SCI_AICTRL1 = 0xD ; | |
const uint8_t SCI_num_registers = 0xF ; | |
// SCI_MODE bits | |
const uint8_t SM_SDINEW = 11 ; // Bitnumber in SCI_MODE always on | |
const uint8_t SM_RESET = 2 ; // Bitnumber in SCI_MODE soft reset | |
const uint8_t SM_CANCEL = 3 ; // Bitnumber in SCI_MODE cancel song | |
const uint8_t SM_TESTS = 5 ; // Bitnumber in SCI_MODE for tests | |
const uint8_t SM_LINE1 = 14 ; // Bitnumber in SCI_MODE for Line input | |
SPISettings VS1053_SPI ; // SPI settings for this slave | |
uint8_t endFillByte ; // Byte to send when stopping song | |
protected: | |
inline void await_data_request() const | |
{ | |
while ( !digitalRead ( dreq_pin ) ) | |
{ | |
yield() ; // Very short delay | |
} | |
} | |
inline void control_mode_on() const | |
{ | |
SPI.beginTransaction ( VS1053_SPI ) ; // Prevent other SPI users | |
digitalWrite ( dcs_pin, HIGH ) ; // Bring slave in control mode | |
digitalWrite ( cs_pin, LOW ) ; | |
} | |
inline void control_mode_off() const | |
{ | |
digitalWrite ( cs_pin, HIGH ) ; // End control mode | |
SPI.endTransaction() ; // Allow other SPI users | |
} | |
inline void data_mode_on() const | |
{ | |
SPI.beginTransaction ( VS1053_SPI ) ; // Prevent other SPI users | |
digitalWrite ( cs_pin, HIGH ) ; // Bring slave in data mode | |
digitalWrite ( dcs_pin, LOW ) ; | |
} | |
inline void data_mode_off() const | |
{ | |
digitalWrite ( dcs_pin, HIGH ) ; // End data mode | |
SPI.endTransaction() ; // Allow other SPI users | |
} | |
uint16_t read_register ( uint8_t _reg ) const ; | |
void write_register ( uint8_t _reg, uint16_t _value ) const ; | |
void sdi_send_buffer ( uint8_t* data, size_t len ) ; | |
void sdi_send_fillers ( size_t length ) ; | |
void wram_write ( uint16_t address, uint16_t data ) ; | |
uint16_t wram_read ( uint16_t address ) ; | |
public: | |
// Constructor. Only sets pin values. Doesn't touch the chip. Be sure to call begin()! | |
VS1053 ( uint8_t _cs_pin, uint8_t _dcs_pin, uint8_t _dreq_pin ) ; | |
void begin() ; // Begin operation. Sets pins correctly, | |
// and prepares SPI bus. | |
void startSong() ; // Prepare to start playing. Call this each | |
// time a new song starts. | |
void playChunk ( uint8_t* data, size_t len ) ; // Play a chunk of data. Copies the data to | |
// the chip. Blocks until complete. | |
void stopSong() ; // Finish playing a song. Call this after | |
// the last playChunk call. | |
void setVolume ( uint8_t vol ) ; // Set the player volume.Level from 0-100, | |
// higher is louder. | |
void setTone ( uint8_t* rtone ) ; // Set the player baas/treble, 4 nibbles for | |
// treble gain/freq and bass gain/freq | |
uint8_t getVolume() ; // Get the current volume setting. | |
// higher is louder. | |
void printDetails ( const char *header ) ; // Print configuration details to serial output. | |
void softReset() ; // Do a soft reset | |
bool testComm ( const char *header ) ; // Test communication with module | |
inline bool data_request() const | |
{ | |
return ( digitalRead ( dreq_pin ) == HIGH ) ; | |
} | |
} ; | |
//****************************************************************************************** | |
// VS1053 class implementation. * | |
//****************************************************************************************** | |
VS1053::VS1053 ( uint8_t _cs_pin, uint8_t _dcs_pin, uint8_t _dreq_pin ) : | |
cs_pin(_cs_pin), dcs_pin(_dcs_pin), dreq_pin(_dreq_pin) | |
{ | |
} | |
uint16_t VS1053::read_register ( uint8_t _reg ) const | |
{ | |
uint16_t result ; | |
control_mode_on() ; | |
SPI.write ( 3 ) ; // Read operation | |
SPI.write ( _reg ) ; // Register to write (0..0xF) | |
// Note: transfer16 does not seem to work | |
result = ( SPI.transfer ( 0xFF ) << 8 ) | // Read 16 bits data | |
( SPI.transfer ( 0xFF ) ) ; | |
await_data_request() ; // Wait for DREQ to be HIGH again | |
control_mode_off() ; | |
return result ; | |
} | |
void VS1053::write_register ( uint8_t _reg, uint16_t _value ) const | |
{ | |
control_mode_on( ); | |
SPI.write ( 2 ) ; // Write operation | |
SPI.write ( _reg ) ; // Register to write (0..0xF) | |
SPI.write16 ( _value ) ; // Send 16 bits data | |
await_data_request() ; | |
control_mode_off() ; | |
} | |
void VS1053::sdi_send_buffer ( uint8_t* data, size_t len ) | |
{ | |
size_t chunk_length ; // Length of chunk 32 byte or shorter | |
data_mode_on() ; | |
while ( len ) // More to do? | |
{ | |
await_data_request() ; // Wait for space available | |
chunk_length = len ; | |
if ( len > vs1053_chunk_size ) | |
{ | |
chunk_length = vs1053_chunk_size ; | |
} | |
len -= chunk_length ; | |
SPI.writeBytes ( data, chunk_length ) ; | |
data += chunk_length ; | |
} | |
data_mode_off() ; | |
} | |
void VS1053::sdi_send_fillers ( size_t len ) | |
{ | |
size_t chunk_length ; // Length of chunk 32 byte or shorter | |
data_mode_on() ; | |
while ( len ) // More to do? | |
{ | |
await_data_request() ; // Wait for space available | |
chunk_length = len ; | |
if ( len > vs1053_chunk_size ) | |
{ | |
chunk_length = vs1053_chunk_size ; | |
} | |
len -= chunk_length ; | |
while ( chunk_length-- ) | |
{ | |
SPI.write ( endFillByte ) ; | |
} | |
} | |
data_mode_off(); | |
} | |
void VS1053::wram_write ( uint16_t address, uint16_t data ) | |
{ | |
write_register ( SCI_WRAMADDR, address ) ; | |
write_register ( SCI_WRAM, data ) ; | |
} | |
uint16_t VS1053::wram_read ( uint16_t address ) | |
{ | |
write_register ( SCI_WRAMADDR, address ) ; // Start reading from WRAM | |
return read_register ( SCI_WRAM ) ; // Read back result | |
} | |
bool VS1053::testComm ( const char *header ) | |
{ | |
// Test the communication with the VS1053 module. The result wille be returned. | |
// If DREQ is low, there is problably no VS1053 connected. Pull the line HIGH | |
// in order to prevent an endless loop waiting for this signal. The rest of the | |
// software will still work, but readbacks from VS1053 will fail. | |
int i ; // Loop control | |
uint16_t r1, r2, cnt = 0 ; | |
uint16_t delta = 300 ; // 3 for fast SPI | |
if ( !digitalRead ( dreq_pin ) ) | |
{ | |
dbgprint ( "VS1053 not properly installed!" ) ; | |
// Allow testing without the VS1053 module | |
pinMode ( dreq_pin, INPUT_PULLUP ) ; // DREQ is now input with pull-up | |
return false ; // Return bad result | |
} | |
// Further TESTING. Check if SCI bus can write and read without errors. | |
// We will use the volume setting for this. | |
// Will give warnings on serial output if DEBUG is active. | |
// A maximum of 20 errors will be reported. | |
if ( strstr ( header, "Fast" ) ) | |
{ | |
delta = 3 ; // Fast SPI, more loops | |
} | |
dbgprint ( header ) ; // Show a header | |
for ( i = 0 ; ( i < 0xFFFF ) && ( cnt < 20 ) ; i += delta ) | |
{ | |
write_register ( SCI_VOL, i ) ; // Write data to SCI_VOL | |
r1 = read_register ( SCI_VOL ) ; // Read back for the first time | |
r2 = read_register ( SCI_VOL ) ; // Read back a second time | |
if ( r1 != r2 || i != r1 || i != r2 ) // Check for 2 equal reads | |
{ | |
dbgprint ( "VS1053 error retry SB:%04X R1:%04X R2:%04X", i, r1, r2 ) ; | |
cnt++ ; | |
delay ( 10 ) ; | |
} | |
yield() ; // Allow ESP firmware to do some bookkeeping | |
} | |
return ( cnt == 0 ) ; // Return the result | |
} | |
void VS1053::begin() | |
{ | |
pinMode ( dreq_pin, INPUT ) ; // DREQ is an input | |
pinMode ( cs_pin, OUTPUT ) ; // The SCI and SDI signals | |
pinMode ( dcs_pin, OUTPUT ) ; | |
digitalWrite ( dcs_pin, HIGH ) ; // Start HIGH for SCI en SDI | |
digitalWrite ( cs_pin, HIGH ) ; | |
delay ( 100 ) ; | |
dbgprint ( "Reset VS1053..." ) ; | |
digitalWrite ( dcs_pin, LOW ) ; // Low & Low will bring reset pin low | |
digitalWrite ( cs_pin, LOW ) ; | |
delay ( 2000 ) ; | |
dbgprint ( "End reset VS1053..." ) ; | |
digitalWrite ( dcs_pin, HIGH ) ; // Back to normal again | |
digitalWrite ( cs_pin, HIGH ) ; | |
delay ( 500 ) ; | |
// Init SPI in slow mode ( 0.2 MHz ) | |
VS1053_SPI = SPISettings ( 200000, MSBFIRST, SPI_MODE0 ) ; | |
//printDetails ( "Right after reset/startup" ) ; | |
delay ( 20 ) ; | |
//printDetails ( "20 msec after reset" ) ; | |
testComm ( "Slow SPI,Testing VS1053 read/write registers..." ) ; | |
// Most VS1053 modules will start up in midi mode. The result is that there is no audio | |
// when playing MP3. You can modify the board, but there is a more elegant way: | |
wram_write ( 0xC017, 3 ) ; // GPIO DDR = 3 | |
wram_write ( 0xC019, 0 ) ; // GPIO ODATA = 0 | |
delay ( 100 ) ; | |
//printDetails ( "After test loop" ) ; | |
softReset() ; // Do a soft reset | |
// Switch on the analog parts | |
write_register ( SCI_AUDATA, 44100 + 1 ) ; // 44.1kHz + stereo | |
// The next clocksetting allows SPI clocking at 5 MHz, 4 MHz is safe then. | |
write_register ( SCI_CLOCKF, 6 << 12 ) ; // Normal clock settings multiplyer 3.0 = 12.2 MHz | |
//SPI Clock to 4 MHz. Now you can set high speed SPI clock. | |
VS1053_SPI = SPISettings ( 4000000, MSBFIRST, SPI_MODE0 ) ; | |
write_register ( SCI_MODE, _BV ( SM_SDINEW ) | _BV ( SM_LINE1 ) ) ; | |
testComm ( "Fast SPI, Testing VS1053 read/write registers again..." ) ; | |
delay ( 10 ) ; | |
await_data_request() ; | |
endFillByte = wram_read ( 0x1E06 ) & 0xFF ; | |
dbgprint ( "endFillByte is %X", endFillByte ) ; | |
//printDetails ( "After last clocksetting" ) ; | |
delay ( 100 ) ; | |
} | |
void VS1053::setVolume ( uint8_t vol ) | |
{ | |
// Set volume. Both left and right. | |
// Input value is 0..100. 100 is the loudest. | |
// Clicking reduced by using 0xf8 to 0x00 as limits. | |
uint16_t value ; // Value to send to SCI_VOL | |
if ( vol != curvol ) | |
{ | |
curvol = vol ; // Save for later use | |
value = map ( vol, 0, 100, 0xF8, 0x00 ) ; // 0..100% to one channel | |
value = ( value << 8 ) | value ; | |
write_register ( SCI_VOL, value ) ; // Volume left and right | |
} | |
} | |
void VS1053::setTone ( uint8_t *rtone ) // Set bass/treble (4 nibbles) | |
{ | |
// Set tone characteristics. See documentation for the 4 nibbles. | |
uint16_t value = 0 ; // Value to send to SCI_BASS | |
int i ; // Loop control | |
for ( i = 0 ; i < 4 ; i++ ) | |
{ | |
value = ( value << 4 ) | rtone[i] ; // Shift next nibble in | |
} | |
write_register ( SCI_BASS, value ) ; // Volume left and right | |
} | |
uint8_t VS1053::getVolume() // Get the currenet volume setting. | |
{ | |
return curvol ; | |
} | |
void VS1053::startSong() | |
{ | |
sdi_send_fillers ( 10 ) ; | |
} | |
void VS1053::playChunk ( uint8_t* data, size_t len ) | |
{ | |
sdi_send_buffer ( data, len ) ; | |
} | |
void VS1053::stopSong() | |
{ | |
uint16_t modereg ; // Read from mode register | |
int i ; // Loop control | |
sdi_send_fillers ( 2052 ) ; | |
delay ( 10 ) ; | |
write_register ( SCI_MODE, _BV ( SM_SDINEW ) | _BV ( SM_CANCEL ) ) ; | |
for ( i = 0 ; i < 200 ; i++ ) | |
{ | |
sdi_send_fillers ( 32 ) ; | |
modereg = read_register ( SCI_MODE ) ; // Read status | |
if ( ( modereg & _BV ( SM_CANCEL ) ) == 0 ) | |
{ | |
sdi_send_fillers ( 2052 ) ; | |
dbgprint ( "Song stopped correctly after %d msec", i * 10 ) ; | |
return ; | |
} | |
delay ( 10 ) ; | |
} | |
printDetails ( "Song stopped incorrectly!" ) ; | |
} | |
void VS1053::softReset() | |
{ | |
write_register ( SCI_MODE, _BV ( SM_SDINEW ) | _BV ( SM_RESET ) ) ; | |
delay ( 10 ) ; | |
await_data_request() ; | |
} | |
void VS1053::printDetails ( const char *header ) | |
{ | |
uint16_t regbuf[16] ; | |
uint8_t i ; | |
dbgprint ( header ) ; | |
dbgprint ( "REG Contents" ) ; | |
dbgprint ( "--- -----" ) ; | |
for ( i = 0 ; i <= SCI_num_registers ; i++ ) | |
{ | |
regbuf[i] = read_register ( i ) ; | |
} | |
for ( i = 0 ; i <= SCI_num_registers ; i++ ) | |
{ | |
delay ( 5 ) ; | |
dbgprint ( "%3X - %5X", i, regbuf[i] ) ; | |
} | |
} | |
// The object for the MP3 player | |
VS1053 vs1053player ( VS1053_CS, VS1053_DCS, VS1053_DREQ ) ; | |
//****************************************************************************************** | |
// End VS1053 stuff. * | |
//****************************************************************************************** | |
//****************************************************************************************** | |
// M Q T T P U B _ C L A S S * | |
//****************************************************************************************** | |
// ID's for the items to publish to MQTT. Is index in amqttpub[] | |
// mqttpub.trigger ( MQTT_STREAMTITLE ) ; // Request publishing to MQTT | |
enum { MQTT_IP, MQTT_ICYNAME, MQTT_STREAMTITLE, MQTT_ICYURL, MQTT_ICYGENRE, MQTT_ICYDESCRIPTION, MQTT_ICYSR, | |
MQTT_ICYCONTENT, MQTT_ICYBITRATE, MQTT_NOWPLAYING, MQTT_VOLUME, MQTT_PRESET, MQTT_PLAYSTATE } ; | |
class mqttpubc // For MQTT publishing | |
{ | |
struct mqttpub_struct | |
{ | |
const char* topic ; // Topic as partial string (without prefix) | |
String* payload ; // Payload for this topic | |
bool topictrigger ; | |
} ; | |
// Publication topics for MQTT. The topic will be pefixed by "PREFIX/", where PREFIX is replaced | |
// by the the mqttprefix in the preferences. | |
protected: | |
mqttpub_struct amqttpub[14] = // Definitions of various MQTT topic to publish | |
{ // Index is equal to enum above | |
{ "ip", &ipaddress, false }, // Definition for MQTT_IP | |
{ "icy/name", &icyname, false }, // Definition for MQTT_ICYNAME | |
{ "icy/streamtitle", &icystreamtitle, false }, // Definition for MQTT_STREAMTITLE | |
{ "icy/url", &icyurl, false }, // Definition for MQTT_ICYURL | |
{ "icy/genre", &icygenre, false }, // Definition for MQTT_ICYGENRE | |
{ "icy/description", &icydescription, false }, // Definition for MQTT_ICYDESCRIPTION | |
{ "icy/sr", &icysr, false }, // Definition for MQTT_ICYSR | |
{ "icy/content", &contenttype, false }, // Definition for MQTT_ICYCONTENT | |
{ "icy/bitrate", &icybr, false }, // Definition for MQTT_UCYBITRATE | |
{ "nowplaying", &nowplaying, false }, // Definition for MQTT_NOWPLAYING (not active) | |
{ "volume", &curvolm, false }, // Definition for MQTT_VOLUME | |
{ "preset", &curprem, false }, // Definition for MQTT_PRESET | |
{ "state", &state, false }, // Definition for MQTT_PLAYSTATE | |
{ NULL, NULL, false } // End of definitions | |
} ; | |
public: | |
void trigger ( uint8_t item ) ; // Trigger publishig for one item | |
void publishtopic() ; // Publish triggerer items | |
//curvolm = vs1053player.getVolume() ; //(char*)ini_block.reqvol ; | |
//String strcurvolm = String(intcurvolm) ; | |
//int intcurprem = ini_block.newpreset ; | |
//curprem = String(curprem) ; | |
} ; | |
//****************************************************************************************** | |
// MQTTPUB class implementation. * | |
//****************************************************************************************** | |
//****************************************************************************************** | |
// T R I G G E R * | |
//****************************************************************************************** | |
// Set request for an item to publish to MQTT. * | |
//****************************************************************************************** | |
void mqttpubc::trigger ( uint8_t item ) // Trigger publishig for one item | |
{ | |
amqttpub[item].topictrigger = true ; // Request re-publish for an item | |
} | |
//****************************************************************************************** | |
// P U B L I S H T O P I C * | |
//****************************************************************************************** | |
// Publish a topic to MQTT broker. * | |
//****************************************************************************************** | |
void mqttpubc::publishtopic() | |
{ | |
int i = 0 ; // Loop control | |
char topic[40] ; // Topic to send | |
const char* payload ; // Points to payload | |
while ( amqttpub[i].topic ) | |
{ | |
if ( amqttpub[i].topictrigger ) // Topic ready to send? | |
{ | |
sprintf ( topic, "%s/%s", ini_block.mqttprefix.c_str(), | |
amqttpub[i].topic ) ; // Add prefix to topic | |
payload = (*amqttpub[i].payload).c_str() ; // Get payload | |
dbgprint ( "Publish to topic %s : %s", // Show for debug | |
topic, payload ) ; | |
if ( mqttclient.publish ( topic, 1, false, payload ) ) // Publish! | |
{ | |
amqttpub[i].topictrigger = false ; // Success, clear trigger | |
return ; // Do the rest later | |
} | |
dbgprint ( "MQTT publish failed!" ) ; // Failed | |
} | |
i++ ; | |
yield() ; | |
// Next entry | |
} | |
} | |
mqttpubc mqttpub ; // Instance for mqttpubc | |
//****************************************************************************************** | |
// Ringbuffer (fifo) routines. * | |
//****************************************************************************************** | |
//****************************************************************************************** | |
// R I N G S P A C E * | |
//****************************************************************************************** | |
inline bool ringspace() | |
{ | |
return ( rcount < RINGBFSIZ ) ; // True is at least one byte of free space is available | |
} | |
//****************************************************************************************** | |
// R I N G A V A I L * | |
//****************************************************************************************** | |
inline uint16_t ringavail() | |
{ | |
return rcount ; // Return number of bytes available for getring() | |
} | |
//****************************************************************************************** | |
// P U T R I N G * | |
//****************************************************************************************** | |
void putring ( uint8_t b ) // Put one byte in the ringbuffer | |
{ | |
// No check on available space. See ringspace() | |
*(ringbuf + rbwindex) = b ; // Put byte in ringbuffer | |
if ( ++rbwindex == RINGBFSIZ ) // Increment pointer and | |
{ | |
rbwindex = 0 ; // wrap at end | |
} | |
rcount++ ; // Count number of bytes in the | |
} | |
//****************************************************************************************** | |
// G E T R I N G * | |
//****************************************************************************************** | |
uint8_t getring() | |
{ | |
// Assume there is always something in the bufferpace. See ringavail() | |
if ( ++rbrindex == RINGBFSIZ ) // Increment pointer and | |
{ | |
rbrindex = 0 ; // wrap at end | |
} | |
rcount-- ; // Count is now one less | |
return *(ringbuf + rbrindex) ; // return the oldest byte | |
} | |
//****************************************************************************************** | |
// E M P T Y R I N G * | |
//****************************************************************************************** | |
void emptyring() | |
{ | |
rbwindex = 0 ; // Reset ringbuffer administration | |
rbrindex = RINGBFSIZ - 1 ; | |
rcount = 0 ; | |
} | |
//****************************************************************************************** | |
// U T F 8 A S C I I * | |
//****************************************************************************************** | |
// UTF8-Decoder: convert UTF8-string to extended ASCII. * | |
// Convert a single Character from UTF8 to Extended ASCII. * | |
// Return "0" if a byte has to be ignored. * | |
//****************************************************************************************** | |
byte utf8ascii ( byte ascii ) | |
{ | |
static const byte lut_C3[] = | |
{ "AAAAAAACEEEEIIIIDNOOOOO#0UUUU###aaaaaaaceeeeiiiidnooooo##uuuuyyy" } ; | |
static byte c1 ; // Last character buffer | |
byte res = 0 ; // Result, default 0 | |
if ( ascii <= 0x7F ) // Standard ASCII-set 0..0x7F handling | |
{ | |
c1 = 0 ; | |
res = ascii ; // Return unmodified | |
} | |
else | |
{ | |
switch ( c1 ) // Conversion depending on first UTF8-character | |
{ | |
case 0xC2: res = '~' ; | |
break ; | |
case 0xC3: res = lut_C3[ascii-128] ; | |
break ; | |
case 0x82: if ( ascii == 0xAC ) | |
{ | |
res = 'E' ; // Special case Euro-symbol | |
} | |
} | |
c1 = ascii ; // Remember actual character | |
} | |
return res ; // Otherwise: return zero, if character has to be ignored | |
} | |
//****************************************************************************************** | |
// U T F 8 A S C I I * | |
//****************************************************************************************** | |
// In Place conversion UTF8-string to Extended ASCII (ASCII is shorter!). * | |
//****************************************************************************************** | |
void utf8ascii ( char* s ) | |
{ | |
int i, k = 0 ; // Indexes for in en out string | |
char c ; | |
for ( i = 0 ; s[i] ; i++ ) // For every input character | |
{ | |
c = utf8ascii ( s[i] ) ; // Translate if necessary | |
if ( c ) // Good translation? | |
{ | |
s[k++] = c ; // Yes, put in output string | |
} | |
} | |
s[k] = 0 ; // Take care of delimeter | |
} | |
//****************************************************************************************** | |
// D B G P R I N T * | |
//****************************************************************************************** | |
// Send a line of info to serial output. Works like vsprintf(), but checks the DEBUG flag.* | |
// Print only if DEBUG flag is true. Always returns the the formatted string. * | |
//****************************************************************************************** | |
char* dbgprint ( const char* format, ... ) | |
{ | |
static char sbuf[DEBUG_BUFFER_SIZE] ; // For debug lines | |
va_list varArgs ; // For variable number of params | |
va_start ( varArgs, format ) ; // Prepare parameters | |
vsnprintf ( sbuf, sizeof(sbuf), format, varArgs ) ; // Format the message | |
va_end ( varArgs ) ; // End of using parameters | |
if ( DEBUG ) // DEBUG on? | |
{ | |
Serial.print ( "D: " ) ; // Yes, print prefix | |
Serial.println ( sbuf ) ; // and the info | |
} | |
return sbuf ; // Return stored string | |
} | |
//****************************************************************************************** | |
// G E T E N C R Y P T I O N T Y P E * | |
//****************************************************************************************** | |
// Read the encryption type of the network and return as a 4 byte name * | |
//****************************************************************************************** | |
const char* getEncryptionType ( int thisType ) | |
{ | |
switch (thisType) | |
{ | |
case ENC_TYPE_WEP: | |
return "WEP " ; | |
case ENC_TYPE_TKIP: | |
return "WPA " ; | |
case ENC_TYPE_CCMP: | |
return "WPA2" ; | |
case ENC_TYPE_NONE: | |
return "None" ; | |
case ENC_TYPE_AUTO: | |
return "Auto" ; | |
} | |
return "????" ; | |
} | |
//****************************************************************************************** | |
// L I S T N E T W O R K S * | |
//****************************************************************************************** | |
// List the available networks and select the strongest. * | |
// Acceptable networks are those who have a "SSID.pw" file in the SPIFFS. * | |
// SSIDs of available networks will be saved for use in webinterface. * | |
//****************************************************************************************** | |
void listNetworks() | |
{ | |
int maxsig = -1000 ; // Used for searching strongest WiFi signal | |
int newstrength ; | |
byte encryption ; // TKIP(WPA)=2, WEP=5, CCMP(WPA)=4, NONE=7, AUTO=8 | |
const char* acceptable ; // Netwerk is acceptable for connection | |
int i ; // Loop control | |
String sassid ; // Search string in anetworks | |
// Scan for nearby networks | |
dbgprint ( "* Scan Networks *" ) ; | |
int numSsid = WiFi.scanNetworks() ; | |
dbgprint ( "Scan completed" ) ; | |
if ( numSsid == -1 ) | |
{ | |
dbgprint ( "Couldn't get a wifi connection" ) ; | |
return ; | |
} | |
// print the list of networks seen: | |
dbgprint ( "Number of available networks: %d", | |
numSsid ) ; | |
// Print the network number and name for each network found and | |
// find the strongest acceptable network | |
for ( i = 0 ; i < numSsid ; i++ ) | |
{ | |
acceptable = "" ; // Assume not acceptable | |
newstrength = WiFi.RSSI ( i ) ; // Get the signal strenght | |
sassid = WiFi.SSID ( i ) + String ( "|" ) ; // For search string | |
if ( anetworks.indexOf ( sassid ) >= 0 ) // Is this SSID acceptable? | |
{ | |
acceptable = "Acceptable" ; | |
if ( newstrength > maxsig ) // This is a better Wifi | |
{ | |
maxsig = newstrength ; | |
ini_block.ssid = WiFi.SSID ( i ) ; // Remember SSID name | |
} | |
} | |
encryption = WiFi.encryptionType ( i ) ; | |
dbgprint ( "%2d - %-25s Signal: %3d dBm Encryption %4s %s", | |
i + 1, WiFi.SSID ( i ).c_str(), WiFi.RSSI ( i ), | |
getEncryptionType ( encryption ), | |
acceptable ) ; | |
// Remember this network for later use | |
networks += WiFi.SSID ( i ) + String ( "|" ) ; | |
} | |
dbgprint ( "--------------------------------------" ) ; | |
} | |
//****************************************************************************************** | |
// T I M E R 1 0 S E C * | |
//****************************************************************************************** | |
// Extra watchdog. Called every 10 seconds. * | |
// If totalcount has not been changed, there is a problem and playing will stop. * | |
// Note that a "yield()" within this routine or in called functions will cause a crash! * | |
//****************************************************************************************** | |
void timer10sec() | |
{ | |
static uint32_t oldtotalcount = 7321 ; // Needed foor change detection | |
static uint8_t morethanonce = 0 ; // Counter for succesive fails | |
if ( datamode & ( INIT | HEADER | DATA | // Test op playing | |
METADATA | PLAYLISTINIT | | |
PLAYLISTHEADER | | |
PLAYLISTDATA ) ) | |
{ | |
if ( totalcount == oldtotalcount ) // Still playing? | |
{ | |
dbgprint ( "No data input" ) ; // No data detected! | |
if ( morethanonce > 10 ) // Happened too many times? | |
{ | |
dbgprint ( "Going to restart..." ) ; | |
ESP.restart() ; // Reset the CPU, probably no return | |
} | |
if ( datamode & ( PLAYLISTDATA | // In playlist mode? | |
PLAYLISTINIT | | |
PLAYLISTHEADER ) ) | |
{ | |
playlist_num = 0 ; // Yes, end of playlist | |
} | |
if ( ( morethanonce > 0 ) || // Happened more than once? | |
( playlist_num > 0 ) ) // Or playlist active? | |
{ | |
datamode = STOPREQD ; // Stop player | |
ini_block.newpreset++ ; // Yes, try next channel | |
dbgprint ( "Trying other station/file..." ) ; | |
} | |
morethanonce++ ; // Count the fails | |
} | |
else | |
{ | |
if ( morethanonce ) // Recovered from data loss? | |
{ | |
dbgprint ( "Recovered from dataloss" ) ; | |
morethanonce = 0 ; // Data see, reset failcounter | |
} | |
oldtotalcount = totalcount ; // Save for comparison in next cycle | |
} | |
} | |
} | |
//****************************************************************************************** | |
// A N A G E T S W * | |
//****************************************************************************************** | |
// Translate analog input to switch number. 0 is inactive. * | |
// Note that it is advised to avoid expressions/math as the argument for the abs function. * | |
//****************************************************************************************** | |
uint8_t anagetsw ( uint16_t v ) | |
{ | |
int i ; // Loop control | |
int oldmindist = 1000 ; // Detection least difference | |
int newdist ; // New found difference | |
uint8_t sw = 0 ; // Number of switch detected (0 or 1..3) | |
if ( v > analogrest ) // Inactive level? | |
{ | |
for ( i = 0 ; i < NUMANA ; i++ ) | |
{ | |
newdist = analogsw[i] - v ; // Compute difference | |
newdist = abs ( newdist ) ; // Make it absolute | |
if ( newdist < oldmindist ) // New least difference? | |
{ | |
oldmindist = newdist ; // Yes, remember | |
sw = i + 1 ; // Remember switch | |
} | |
} | |
} | |
return sw ; // Return active switch | |
} | |
//****************************************************************************************** | |
// T E S T F I L E * | |
//****************************************************************************************** | |
// Test the performance of SPIFFS read. * | |
//****************************************************************************************** | |
void testfile ( String fspec ) | |
{ | |
String path ; // Full file spec | |
File tfile ; // File containing mp3 | |
uint32_t len, savlen ; // File length | |
uint32_t t0, t1, told ; // For time test | |
uint32_t t_error = 0 ; // Number of slow reads | |
dbgprint ( "Start test of file %s", fspec.c_str() ) ; | |
t0 = millis() ; // Timestamp at start | |
t1 = t0 ; // Prevent uninitialized value | |
told = t0 ; // For report | |
path = String ( "/" ) + fspec ; // Form full path | |
tfile = SPIFFS.open ( path, "r" ) ; // Open the file | |
if ( tfile ) | |
{ | |
len = tfile.available() ; // Get file length | |
savlen = len ; // Save for result print | |
while ( len-- ) // Any data left? | |
{ | |
t1 = millis() ; // To meassure read time | |
tfile.read() ; // Read one byte | |
if ( ( millis() - t1 ) > 5 ) // Read took more than 5 msec? | |
{ | |
t_error++ ; // Yes, count slow reads | |
} | |
if ( ( len % 100 ) == 0 ) // Yield reguarly | |
{ | |
yield() ; | |
} | |
if ( ( ( t1 - told ) / 1000 ) > 0 || len == 0 ) | |
{ | |
// Show results for debug | |
dbgprint ( "Read %s, length %d/%d took %d seconds, %d slow reads", | |
fspec.c_str(), savlen - len, savlen, ( t1 - t0 ) / 1000, t_error ) ; | |
told = t1 ; | |
} | |
if ( ( t1 - t0 ) > 100000 ) // Give up after 100 seconds | |
{ | |
dbgprint ( "Give up..." ) ; | |
break ; | |
} | |
} | |
tfile.close() ; | |
dbgprint ( "EOF" ) ; // End of file | |
} | |
} | |
//****************************************************************************************** | |
// T I M E R 1 0 0 * | |
//****************************************************************************************** | |
// Examine button every 100 msec. * | |
//****************************************************************************************** | |
void timer100() | |
{ | |
static int count10sec = 0 ; // Counter for activatie 10 seconds process | |
static int oldval2 = HIGH ; // Previous value of digital input button 2 | |
#if ( not ( defined ( USETFT ) ) ) | |
static int oldval1 = HIGH ; // Previous value of digital input button 1 | |
static int oldval3 = HIGH ; // Previous value of digital input button 3 | |
#endif | |
int newval ; // New value of digital input switch | |
uint16_t v ; // Analog input value 0..1023 | |
static uint8_t aoldval = 0 ; // Previous value of analog input switch | |
uint8_t anewval ; // New value of analog input switch (0..3) | |
if ( ++count10sec == 100 ) // 10 seconds passed? | |
{ | |
timer10sec() ; // Yes, do 10 second procedure | |
count10sec = 0 ; // Reset count | |
} | |
else | |
{ | |
newval = digitalRead ( BUTTON2 ) ; // Test if below certain level | |
if ( newval != oldval2 ) // Change? | |
{ | |
oldval2 = newval ; // Yes, remember value | |
if ( newval == LOW ) // Button pushed? | |
{ | |
ini_block.newpreset = currentpreset + 1 ; // Yes, goto next preset station | |
dbgprint ( "Digital button 2 pushed" ) ; | |
} | |
return ; | |
} | |
#if ( not ( defined ( USETFT ) ) ) | |
newval = digitalRead ( BUTTON1 ) ; // Test if below certain level | |
if ( newval != oldval1 ) // Change? | |
{ | |
oldval1 = newval ; // Yes, remember value | |
if ( newval == LOW ) // Button pushed? | |
{ | |
ini_block.newpreset = 0 ; // Yes, goto first preset station | |
dbgprint ( "Digital button 1 pushed" ) ; | |
} | |
return ; | |
} | |
// Note that BUTTON3 has inverted input | |
newval = digitalRead ( BUTTON3 ) ; // Test if below certain level | |
newval = HIGH + LOW - newval ; // Reverse polarity | |
if ( newval != oldval3 ) // Change? | |
{ | |
oldval3 = newval ; // Yes, remember value | |
if ( newval == LOW ) // Button pushed? | |
{ | |
ini_block.newpreset = currentpreset - 1 ; // Yes, goto previous preset station | |
dbgprint ( "Digital button 3 pushed" ) ; | |
} | |
return ; | |
} | |
#endif | |
v = analogRead ( A0 ) ; // Read analog value | |
anewval = anagetsw ( v ) ; // Check analog value for program switches | |
if ( anewval != aoldval ) // Change? | |
{ | |
aoldval = anewval ; // Remember value for change detection | |
if ( anewval != 0 ) // Button pushed? | |
{ | |
dbgprint ( "Analog button %d pushed, v = %d", anewval, v ) ; | |
if ( anewval == 1 ) // Button 1? | |
{ | |
ini_block.newpreset = 0 ; // Yes, goto first preset | |
} | |
else if ( anewval == 2 ) // Button 2? | |
{ | |
ini_block.newpreset = currentpreset + 1 ; // Yes, goto next preset | |
} | |
else if ( anewval == 3 ) // Button 3? | |
{ | |
ini_block.newpreset = currentpreset - 1 ; // Yes, goto previous preset | |
} | |
} | |
} | |
} | |
} | |
//****************************************************************************************** | |
// D I S P L A Y V O L U M E * | |
//****************************************************************************************** | |
// Show the current volume as an indicator on the screen. * | |
//****************************************************************************************** | |
void displayvolume() | |
{ | |
#if defined ( USETFT ) | |
static uint8_t oldvol = 0 ; // Previous volume | |
uint8_t pos ; // Positon of volume indicator | |
if ( vs1053player.getVolume() != oldvol ) | |
{ | |
pos = map ( vs1053player.getVolume(), 0, 100, 0, 160 ) ; | |
} | |
tft.fillRect ( 0, 126, pos, 2, RED ) ; // Paint red part | |
tft.fillRect ( pos, 126, 160 - pos, 2, GREEN ) ; // Paint green part | |
#endif | |
} | |
//****************************************************************************************** | |
// D I S P L A Y I N F O * | |
//****************************************************************************************** | |
// Show a string on the LCD at a specified y-position in a specified color * | |
//****************************************************************************************** | |
void displayinfo ( const char* str, uint16_t pos, uint16_t height, uint16_t color ) | |
{ | |
#if defined ( USETFT ) | |
char buf [ strlen ( str ) + 1 ] ; // Need some buffer space | |
strcpy ( buf, str ) ; // Make a local copy of the string | |
utf8ascii ( buf ) ; // Convert possible UTF8 | |
tft.fillRect ( 0, pos, 160, height, BLACK ) ; // Clear the space for new info | |
tft.setTextColor ( color ) ; // Set the requested color | |
tft.setCursor ( 0, pos ) ; // Prepare to show the info | |
tft.println ( buf ) ; // Show the string | |
#endif | |
} | |
//****************************************************************************************** | |
// S H O W S T R E A M T I T L E * | |
//****************************************************************************************** | |
// Show artist and songtitle if present in metadata. * | |
// Show always if full=true. * | |
//****************************************************************************************** | |
void showstreamtitle ( const char *ml, bool full ) | |
{ | |
char* p1 ; | |
char* p2 ; | |
char streamtitle[150] ; // Streamtitle from metadata | |
if ( strstr ( ml, "StreamTitle=" ) ) | |
{ | |
dbgprint ( "Streamtitle found, %d bytes", strlen ( ml ) ) ; | |
dbgprint ( ml ) ; | |
p1 = (char*)ml + 12 ; // Begin of artist and title | |
if ( ( p2 = strstr ( ml, ";" ) ) ) // Search for end of title | |
{ | |
if ( *p1 == '\'' ) // Surrounded by quotes? | |
{ | |
p1++ ; | |
p2-- ; | |
} | |
*p2 = '\0' ; // Strip the rest of the line | |
} | |
// Save last part of string as streamtitle. Protect against buffer overflow | |
strncpy ( streamtitle, p1, sizeof ( streamtitle ) ) ; | |
streamtitle[sizeof ( streamtitle ) - 1] = '\0' ; | |
} | |
else if ( full ) | |
{ | |
// Info probably from playlist | |
strncpy ( streamtitle, ml, sizeof ( streamtitle ) ) ; | |
streamtitle[sizeof ( streamtitle ) - 1] = '\0' ; | |
} | |
else | |
{ | |
icystreamtitle = "" ; // Unknown type | |
return ; // Do not show | |
} | |
// Save for status request from browser and for MQTT | |
icystreamtitle = streamtitle ; | |
if ( ( p1 = strstr ( streamtitle, " - " ) ) ) // look for artist/title separator | |
{ | |
*p1++ = '\n' ; // Found: replace 3 characters by newline | |
p2 = p1 + 2 ; | |
if ( *p2 == ' ' ) // Leading space in title? | |
{ | |
p2++ ; | |
} | |
strcpy ( p1, p2 ) ; // Shift 2nd part of title 2 or 3 places | |
} | |
displayinfo ( streamtitle, 20, 40, CYAN ) ; // Show title at position 20 | |
if ( ( icyname.length() == 0 ) && ( icystreamtitle.length() > 0 ) ) | |
{ | |
nowplaying = icystreamtitle ; | |
mqttpub.trigger ( MQTT_NOWPLAYING ) ; | |
} | |
else if ( ( icystreamtitle.length() > 0) && ( icyname.length() > 0 ) ) | |
{ | |
nowplaying = icyname + " - " + icystreamtitle ; | |
mqttpub.trigger ( MQTT_NOWPLAYING ) ; | |
} | |
else | |
{ | |
nowplaying = "Unknown" ; | |
mqttpub.trigger ( MQTT_NOWPLAYING ) ; | |
} | |
} | |
//****************************************************************************************** | |
// S T O P _ M P 3 C L I E N T * | |
//****************************************************************************************** | |
// Disconnect from the server. * | |
//****************************************************************************************** | |
void stop_mp3client () | |
{ | |
while ( mp3client.connected() ) | |
{ | |
dbgprint ( "Stopping client" ) ; // Stop connection to host | |
mp3client.flush() ; | |
mp3client.stop() ; | |
delay ( 500 ) ; | |
} | |
mp3client.flush() ; // Flush stream client | |
mp3client.stop() ; // Stop stream client | |
} | |
//****************************************************************************************** | |
// C O N N E C T T O H O S T * | |
//****************************************************************************************** | |
// Connect to the Internet radio server specified by newpreset. * | |
//****************************************************************************************** | |
bool connecttohost() | |
{ | |
int inx ; // Position of ":" in hostname | |
char* pfs ; // Pointer to formatted string | |
int port = 80 ; // Port number for host | |
String extension = "/" ; // May be like "/mp3" in "skonto.ls.lv:8002/mp3" | |
String hostwoext ; // Host without extension and portnumber | |
stop_mp3client() ; // Disconnect if still connected | |
dbgprint ( "Connect to new host %s", host.c_str() ) ; | |
displayinfo ( " ** Internet radio **", 0, 20, WHITE ) ; | |
datamode = INIT ; // Start default in metamode | |
chunked = false ; // Assume not chunked | |
if ( host.endsWith ( ".m3u" ) ) // Is it an m3u playlist? | |
{ | |
playlist = host ; // Save copy of playlist URL | |
datamode = PLAYLISTINIT ; // Yes, start in PLAYLIST mode | |
if ( playlist_num == 0 ) // First entry to play? | |
{ | |
playlist_num = 1 ; // Yes, set index | |
} | |
dbgprint ( "Playlist request, entry %d", playlist_num ) ; | |
} | |
// In the URL there may be an extension | |
inx = host.indexOf ( "/" ) ; // Search for begin of extension | |
if ( inx > 0 ) // Is there an extension? | |
{ | |
extension = host.substring ( inx ) ; // Yes, change the default | |
hostwoext = host.substring ( 0, inx ) ; // Host without extension | |
} | |
// In the URL there may be a portnumber | |
inx = host.indexOf ( ":" ) ; // Search for separator | |
if ( inx >= 0 ) // Portnumber available? | |
{ | |
port = host.substring ( inx + 1 ).toInt() ; // Get portnumber as integer | |
hostwoext = host.substring ( 0, inx ) ; // Host without portnumber | |
} | |
pfs = dbgprint ( "Connect to %s on port %d, extension %s", | |
hostwoext.c_str(), port, extension.c_str() ) ; | |
displayinfo ( pfs, 60, 66, YELLOW ) ; // Show info at position 60..125 | |
if ( mp3client.connect ( hostwoext.c_str(), port ) ) | |
{ | |
dbgprint ( "Connected to server" ) ; | |
// This will send the request to the server. Request metadata. | |
mp3client.print ( String ( "GET " ) + | |
extension + | |
String ( " HTTP/1.1\r\n" ) + | |
String ( "Host: " ) + | |
hostwoext + | |
String ( "\r\n" ) + | |
String ( "Icy-MetaData:1\r\n" ) + | |
String ( "Connection: close\r\n\r\n" ) ) ; | |
state = "Playing" ; | |
mqttpub.trigger ( MQTT_PLAYSTATE ) ; // Request publishing to MQTT | |
return true ; | |
} | |
dbgprint ( "Request %s failed!", host.c_str() ) ; | |
return false ; | |
} | |
//****************************************************************************************** | |
// C O N N E C T T O F I L E * | |
//****************************************************************************************** | |
// Open the local mp3-file. * | |
//****************************************************************************************** | |
bool connecttofile() | |
{ | |
String path ; // Full file spec | |
char* p ; // Pointer to filename | |
displayinfo ( " **** MP3 Player ****", 0, 20, WHITE ) ; | |
path = host.substring ( 9 ) ; // Path, skip the "localhost" part | |
mp3file = SPIFFS.open ( path, "r" ) ; // Open the file | |
if ( !mp3file ) | |
{ | |
dbgprint ( "Error opening file %s", path.c_str() ) ; // No luck | |
return false ; | |
} | |
p = (char*)path.c_str() + 1 ; // Point to filename | |
showstreamtitle ( p, true ) ; // Show the filename as title | |
mqttpub.trigger ( MQTT_STREAMTITLE ) ; // Request publishing to MQTT | |
state = "Playing" ; | |
mqttpub.trigger ( MQTT_PLAYSTATE ) ; // Request publishing to MQTT | |
displayinfo ( "Playing from local file", | |
60, 68, YELLOW ) ; // Show Source at position 60 | |
icyname = "" ; // No icy name yet | |
chunked = false ; // File not chunked | |
return true ; | |
} | |
//****************************************************************************************** | |
// C O N N E C T W I F I * | |
//****************************************************************************************** | |
// Connect to WiFi using passwords available in the SPIFFS. * | |
// If connection fails, an AP is created and the function returns false. * | |
//****************************************************************************************** | |
bool connectwifi() | |
{ | |
char* pfs ; // Pointer to formatted string | |
WiFi.disconnect() ; // After restart the router could | |
WiFi.softAPdisconnect(true) ; // still keep the old connection | |
WiFi.begin ( ini_block.ssid.c_str(), | |
ini_block.passwd.c_str() ) ; // Connect to selected SSID | |
dbgprint ( "Try WiFi %s", ini_block.ssid.c_str() ) ; // Message to show during WiFi connect | |
if ( WiFi.waitForConnectResult() != WL_CONNECTED ) // Try to connect | |
{ | |
dbgprint ( "WiFi Failed! Trying to setup AP with name %s and password %s.", NAME, NAME ) ; | |
WiFi.softAP ( NAME, NAME ) ; // This ESP will be an AP | |
delay ( 5000 ) ; | |
pfs = dbgprint ( "IP = 192.168.4.1" ) ; // Address if AP | |
return false ; | |
} | |
dbgprint ( "Connected to %s", WiFi.SSID().c_str() ) ; | |
pfs = dbgprint ( "IP = %d.%d.%d.%d", | |
WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3] ) ; | |
#if defined ( USETFT ) | |
tft.println ( pfs ) ; | |
#endif | |
return true ; | |
} | |
//****************************************************************************************** | |
// O T A S T A R T * | |
//****************************************************************************************** | |
// Update via WiFi has been started by Arduino IDE. * | |
//****************************************************************************************** | |
void otastart() | |
{ | |
dbgprint ( "OTA Started" ) ; | |
} | |
//****************************************************************************************** | |
// R E A D H O S T F R O M I N I F I L E * | |
//****************************************************************************************** | |
// Read the mp3 host from the ini-file specified by the parameter. * | |
// The host will be returned. * | |
//****************************************************************************************** | |
String readhostfrominifile ( int8_t preset ) | |
{ | |
String path ; // Full file spec as string | |
File inifile ; // File containing URL with mp3 | |
char tkey[10] ; // Key as an array of chars | |
String line ; // Input line from .ini file | |
String linelc ; // Same, but lowercase | |
int inx ; // Position within string | |
String res = "" ; // Assume not found | |
path = String ( INIFILENAME ) ; // Form full path | |
inifile = SPIFFS.open ( path, "r" ) ; // Open the file | |
if ( inifile ) | |
{ | |
sprintf ( tkey, "preset_%02d", preset ) ; // Form the search key | |
while ( inifile.available() ) | |
{ | |
line = inifile.readStringUntil ( '\n' ) ; // Read next line | |
linelc = line ; // Copy for lowercase | |
linelc.toLowerCase() ; // Set to lowercase | |
if ( linelc.startsWith ( tkey ) ) // Found the key? | |
{ | |
inx = line.indexOf ( "=" ) ; // Get position of "=" | |
if ( inx > 0 ) // Equal sign present? | |
{ | |
line.remove ( 0, inx + 1 ) ; // Yes, remove key | |
res = chomp ( line ) ; // Remove garbage | |
break ; // End the while loop | |
} | |
} | |
} | |
inifile.close() ; // Close the file | |
} | |
else | |
{ | |
dbgprint ( "File %s not found, please create one!", INIFILENAME ) ; | |
} | |
return res ; | |
} | |
//****************************************************************************************** | |
// R E A D I N I F I L E * | |
//****************************************************************************************** | |
// Read the .ini file and interpret the commands. * | |
//****************************************************************************************** | |
void readinifile() | |
{ | |
String path ; // Full file spec as string | |
File inifile ; // File containing URL with mp3 | |
String line ; // Input line from .ini file | |
path = String ( INIFILENAME ) ; // Form full path | |
inifile = SPIFFS.open ( path, "r" ) ; // Open the file | |
if ( inifile ) | |
{ | |
while ( inifile.available() ) | |
{ | |
line = inifile.readStringUntil ( '\n' ) ; // Read next line | |
analyzeCmd ( line.c_str() ) ; | |
} | |
inifile.close() ; // Close the file | |
} | |
else | |
{ | |
dbgprint ( "File %s not found, use save command to create one!", INIFILENAME ) ; | |
} | |
} | |
//****************************************************************************************** | |
// O N M Q T T C O N N E C T * | |
//****************************************************************************************** | |
// Will be called on connection to the broker. Subscribe to our topic and publish a topic.* | |
//****************************************************************************************** | |
void onMqttConnect( bool sessionPresent ) | |
{ | |
uint16_t packetIdSub ; | |
const char* present = "is" ; // Assume Session is present | |
if ( !sessionPresent ) | |
{ | |
present = "is not" ; // Session is NOT present | |
} | |
dbgprint ( "MQTT Connected to the broker %s, session %s present", | |
ini_block.mqttbroker.c_str(), present ) ; | |
packetIdSub = mqttclient.subscribe ( ini_block.mqtttopic.c_str(), 0 ) ; | |
dbgprint ( "Subscribing to %s at QoS 0, packetId = %d ", | |
ini_block.mqtttopic.c_str(), | |
packetIdSub ) ; | |
} | |
//****************************************************************************************** | |
// O N M Q T T D I S C O N N E C T * | |
//****************************************************************************************** | |
// Will be called on disconnect. * | |
//****************************************************************************************** | |
void onMqttDisconnect ( AsyncMqttClientDisconnectReason reason ) | |
{ | |
dbgprint ( "MQTT Disconnected from the broker, reason %d, reconnecting...", | |
reason ) ; | |
//if ( mqttcount < MAXMQTTCONNECTS ) // Try again? | |
//{ | |
//mqttcount++ ; // Yes, count number of tries | |
//mqttclient.connect() ; // Reconnect | |
//dbgprint ( "MQTT reconnection attempt: %d", | |
// mqttcount ) ; | |
//yield() ; | |
//} | |
} | |
//****************************************************************************************** | |
// O N M Q T T S U B S C R I B E * | |
//****************************************************************************************** | |
// Will be called after a successful subscribe. * | |
//****************************************************************************************** | |
void onMqttSubscribe ( uint16_t packetId, uint8_t qos ) | |
{ | |
dbgprint ( "MQTT Subscribe acknowledged, packetId = %d, QoS = %d", | |
packetId, qos ) ; | |
} | |
//****************************************************************************************** | |
// O N M Q T T U N S U B S C R I B E * | |
//****************************************************************************************** | |
// Will be executed if this program unsubscribes from a topic. * | |
// Not used at the moment. * | |
//****************************************************************************************** | |
void onMqttUnsubscribe ( uint16_t packetId ) | |
{ | |
dbgprint ( "MQTT Unsubscribe acknowledged, packetId = %d", | |
packetId ) ; | |
} | |
//****************************************************************************************** | |
// O N M Q T T M E S S A G E * | |
//****************************************************************************************** | |
// Executed when a subscribed message is received. * | |
// Note that message is not delimited by a '\0'. * | |
// Note that cmd buffer is shared with serial input. * | |
//****************************************************************************************** | |
void onMqttMessage ( char* topic, char* payload, AsyncMqttClientMessageProperties properties, | |
size_t len, size_t index, size_t total ) | |
{ | |
char* reply ; // Result from analyzeCmd | |
// Available properties.qos, properties.dup, properties.retain | |
if ( len >= sizeof(cmd) ) // Message may not be too long | |
{ | |
len = sizeof(cmd) - 1 ; | |
} | |
strncpy ( cmd, payload, len ) ; // Make copy of message | |
cmd[len] = '\0' ; // Take care of delimeter | |
dbgprint ( "MQTT message arrived [%s], length = %d, %s", topic, len, cmd ) ; | |
reply = analyzeCmd ( cmd ) ; // Analyze command and handle it | |
dbgprint ( reply ) ; // Result for debugging | |
} | |
//****************************************************************************************** | |
// O N M Q T T P U B L I S H * | |
//****************************************************************************************** | |
// Will be executed if a message is published by this program. * | |
// Not used at the moment. * | |
//****************************************************************************************** | |
void onMqttPublish ( uint16_t packetId ) | |
{ | |
dbgprint ( "MQTT Publish acknowledged, packetId = %d", | |
packetId ) ; | |
} | |
//****************************************************************************************** | |
// S C A N S E R I A L * | |
//****************************************************************************************** | |
// Listen to commands on the Serial inputline. * | |
//****************************************************************************************** | |
void scanserial() | |
{ | |
static String serialcmd ; // Command from Serial input | |
char c ; // Input character | |
char* reply ; // Reply string from analyzeCmd | |
uint16_t len ; // Length of input string | |
while ( Serial.available() ) // Any input seen? | |
{ | |
c = (char)Serial.read() ; // Yes, read the next input character | |
//Serial.write ( c ) ; // Echo | |
len = serialcmd.length() ; // Get the length of the current string | |
if ( ( c == '\n' ) || ( c == '\r' ) ) | |
{ | |
if ( len ) | |
{ | |
strncpy ( cmd, serialcmd.c_str(), sizeof(cmd) ) ; | |
reply = analyzeCmd ( cmd ) ; // Analyze command and handle it | |
dbgprint ( reply ) ; // Result for debugging | |
serialcmd = "" ; // Prepare for new command | |
} | |
} | |
if ( c >= ' ' ) // Only accept useful characters | |
{ | |
serialcmd += c ; // Add to the command | |
} | |
if ( len >= ( sizeof(cmd) - 2 ) ) // Check for excessive length | |
{ | |
serialcmd = "" ; // Too long, reset | |
} | |
} | |
} | |
//****************************************************************************************** | |
// M K _ L S A N * | |
//****************************************************************************************** | |
// Make a list of acceptable networks in .ini file. * | |
// The result will be stored in anetworks like "|SSID1|SSID2|......|SSIDN|". * | |
// The number of acceptable networks will be stored in num_an. * | |
//****************************************************************************************** | |
void mk_lsan() | |
{ | |
String path ; // Full file spec as string | |
File inifile ; // File containing URL with mp3 | |
String line ; // Input line from .ini file | |
String ssid ; // SSID in line | |
int inx ; // Place of "/" | |
num_an = 0 ; // Count acceptable networks | |
anetworks = "|" ; // Initial value | |
path = String ( INIFILENAME ) ; // Form full path | |
inifile = SPIFFS.open ( path, "r" ) ; // Open the file | |
if ( inifile ) | |
{ | |
while ( inifile.available() ) | |
{ | |
line = inifile.readStringUntil ( '\n' ) ; // Read next line | |
ssid = line ; // Copy holds original upper/lower case | |
line.toLowerCase() ; // Case insensitive | |
if ( line.startsWith ( "wifi" ) ) // Line with WiFi spec? | |
{ | |
inx = line.indexOf ( "/" ) ; // Find separator between ssid and password | |
if ( inx > 0 ) // Separator found? | |
{ | |
ssid = ssid.substring ( 5, inx ) ; // Line holds SSID now | |
dbgprint ( "Added SSID %s to acceptable networks", | |
ssid.c_str() ) ; | |
anetworks += ssid ; // Add to list | |
anetworks += "|" ; // Separator | |
num_an++ ; // Count number of acceptable networks | |
} | |
} | |
} | |
inifile.close() ; // Close the file | |
} | |
else | |
{ | |
dbgprint ( "File %s not found!", INIFILENAME ) ; // No .ini file | |
} | |
} | |
//****************************************************************************************** | |
// G E T P R E S E T S * | |
//****************************************************************************************** | |
// Make a list of all preset stations. * | |
// The result will be stored in the String presetlist (global data). * | |
//****************************************************************************************** | |
void getpresets() | |
{ | |
String path ; // Full file spec as string | |
File inifile ; // File containing URL with mp3 | |
String line ; // Input line from .ini file | |
int inx ; // Position of search char in line | |
int i ; // Loop control | |
char vnr[3] ; // 2 digit presetnumber as string | |
presetlist = String ( "" ) ; // No result yet | |
path = String ( INIFILENAME ) ; // Form full path | |
inifile = SPIFFS.open ( path, "r" ) ; // Open the file | |
if ( inifile ) | |
{ | |
while ( inifile.available() ) | |
{ | |
line = inifile.readStringUntil ( '\n' ) ; // Read next line | |
if ( line.startsWith ( "preset_" ) ) // Found the key? | |
{ | |
i = line.substring(7, 9).toInt() ; // Get index 00..99 | |
// Show just comment if available. Otherwise the preset itself. | |
inx = line.indexOf ( "#" ) ; // Get position of "#" | |
if ( inx > 0 ) // Hash sign present? | |
{ | |
line.remove ( 0, inx + 1 ) ; // Yes, remove non-comment part | |
} | |
else | |
{ | |
inx = line.indexOf ( "=" ) ; // Get position of "=" | |
if ( inx > 0 ) // Equal sign present? | |
{ | |
line.remove ( 0, inx + 1 ) ; // Yes, remove first part of line | |
} | |
} | |
line = chomp ( line ) ; // Remove garbage from description | |
sprintf ( vnr, "%02d", i ) ; // Preset number | |
presetlist += ( String ( vnr ) + line + // 2 digits plus description | |
String ( "|" ) ) ; | |
} | |
} | |
inifile.close() ; // Close the file | |
} | |
} | |
//****************************************************************************************** | |
// S E T U P * | |
//****************************************************************************************** | |
// Setup for the program. * | |
//****************************************************************************************** | |
void setup() | |
{ | |
FSInfo fs_info ; // Info about SPIFFS | |
Dir dir ; // Directory struct for SPIFFS | |
File f ; // Filehandle | |
String filename ; // Name of file found in SPIFFS | |
byte mac[6] ; // WiFi mac address | |
char tmpstr[20] ; // For version and Mac address | |
Serial.begin ( 115200 ) ; // For debug | |
Serial.println() ; | |
#if defined ( SDCARDCS ) && ( SDCARDCS >= 0 ) | |
pinMode ( SDCARDCS, OUTPUT ) ; // Deselect SDCARD | |
digitalWrite ( SDCARDCS, HIGH ) ; | |
#endif | |
system_update_cpu_freq ( 160 ) ; // Set to 80/160 MHz | |
ringbuf = (uint8_t *) malloc ( RINGBFSIZ ) ; // Create ring buffer | |
xml.init ( xmlbuffer, sizeof(xmlbuffer), // Initilize XML stream. | |
&XML_callback ) ; | |
memset ( &ini_block, 0, sizeof(ini_block) ) ; // Init ini_block | |
ini_block.mqttport = 1883 ; // Default port for MQTT | |
SPIFFS.begin() ; // Enable file system | |
// Show some info about the SPIFFS | |
SPIFFS.info ( fs_info ) ; | |
dbgprint ( "FS Total %d, used %d", fs_info.totalBytes, fs_info.usedBytes ) ; | |
if ( fs_info.totalBytes == 0 ) | |
{ | |
dbgprint ( "No SPIFFS found! See documentation." ) ; | |
} | |
dir = SPIFFS.openDir("/") ; // Show files in FS | |
while ( dir.next() ) // All files | |
{ | |
f = dir.openFile ( "r" ) ; | |
filename = dir.fileName() ; | |
dbgprint ( "%-32s - %7d", // Show name and size | |
filename.c_str(), f.size() ) ; | |
} | |
mk_lsan() ; // Make al list of acceptable networks in ini file. | |
listNetworks() ; // Search for WiFi networks | |
readinifile() ; // Read .ini file | |
getpresets() ; // Get the presets from .ini-file | |
WiFi.persistent ( false ) ; // Do not save SSID and password | |
WiFi.disconnect() ; // After restart the router could still keep the old connection | |
WiFi.mode ( WIFI_STA ) ; // This ESP is a station | |
wifi_station_set_hostname ( (char*)NAME ) ; | |
SPI.begin() ; // Init SPI bus | |
// Print some memory and sketch info | |
dbgprint ( "Starting ESP Version %s... Free memory %d", | |
VERSION, | |
system_get_free_heap_size() ) ; | |
dbgprint ( "Sketch size %d, free size %d", | |
ESP.getSketchSize(), | |
ESP.getFreeSketchSpace() ) ; | |
pinMode ( BUTTON2, INPUT_PULLUP ) ; // Input for control button 2 | |
vs1053player.begin() ; // Initialize VS1053 player | |
# if defined ( USETFT ) | |
tft.begin() ; // Init TFT interface | |
tft.fillRect ( 0, 0, 160, 128, BLACK ) ; // Clear screen does not work when rotated | |
tft.setRotation ( 3 ) ; // Use landscape format | |
tft.clearScreen() ; // Clear screen | |
tft.setTextSize ( 1 ) ; // Small character font | |
tft.setTextColor ( WHITE ) ; // Info in white | |
tft.println ( "Starting" ) ; | |
tft.println ( "Version:" ) ; | |
tft.println ( VERSION ) ; | |
#else | |
pinMode ( BUTTON1, INPUT_PULLUP ) ; // Input for control button 1 | |
pinMode ( BUTTON3, INPUT_PULLUP ) ; // Input for control button 3 | |
#endif | |
delay(10); | |
analogrest = ( analogRead ( A0 ) + asw1 ) / 2 ; // Assumed inactive analog input | |
tckr.attach ( 0.100, timer100 ) ; // Every 100 msec | |
dbgprint ( "Selected network: %-25s", ini_block.ssid.c_str() ) ; | |
NetworkFound = connectwifi() ; // Connect to WiFi network | |
//NetworkFound = false ; // TEST, uncomment for no network test | |
dbgprint ( "Start server for commands" ) ; | |
cmdserver.on ( "/", handleCmd ) ; // Handle startpage | |
cmdserver.onNotFound ( handleFS ) ; // Handle file from FS | |
cmdserver.onFileUpload ( handleFileUpload ) ; // Handle file uploads | |
cmdserver.begin() ; | |
if ( NetworkFound ) // OTA and MQTT only if Wifi network found | |
{ | |
mqtt_on = ( ini_block.mqttbroker.length() > 0 ) && // Use MQTT if broker specified | |
( ini_block.mqttbroker != "none" ) ; | |
ArduinoOTA.setHostname ( NAME ) ; // Set the hostname | |
ArduinoOTA.onStart ( otastart ) ; | |
ArduinoOTA.begin() ; // Allow update over the air | |
if ( mqtt_on ) // Broker specified? | |
{ | |
if ( ( ini_block.mqttprefix.length() == 0 ) || // No prefix? | |
( ini_block.mqttprefix == "none" ) ) | |
{ | |
WiFi.macAddress ( mac ) ; // Get mac-adress | |
sprintf ( tmpstr, "P%02X%02X%02X%02X", // Generate string from last part | |
mac[3], mac[2], | |
mac[1], mac[0] ) ; | |
ini_block.mqttprefix = String ( tmpstr ) ; // Save for further use | |
} | |
dbgprint ( "MQTT uses prefix %s", ini_block.mqttprefix.c_str() ) ; | |
dbgprint ( "Init MQTT" ) ; | |
// Initialize the MQTT client | |
WiFi.hostByName ( ini_block.mqttbroker.c_str(), | |
mqtt_server_IP ) ; // Lookup IP of MQTT server | |
mqttclient.onConnect ( onMqttConnect ) ; | |
mqttclient.onDisconnect ( onMqttDisconnect ) ; | |
mqttclient.onSubscribe ( onMqttSubscribe ) ; | |
mqttclient.onUnsubscribe ( onMqttUnsubscribe ) ; | |
mqttclient.onMessage ( onMqttMessage ) ; | |
mqttclient.onPublish ( onMqttPublish ) ; | |
mqttclient.setServer ( mqtt_server_IP, // Specify the broker | |
ini_block.mqttport ) ; // And the port | |
mqttclient.setCredentials ( ini_block.mqttuser.c_str(), | |
ini_block.mqttpasswd.c_str() ) ; | |
mqttclient.setClientId ( NAME ) ; | |
dbgprint ( "Connecting to MQTT %s, port %d, user %s, password %s...", | |
ini_block.mqttbroker.c_str(), | |
ini_block.mqttport, | |
ini_block.mqttuser.c_str(), | |
ini_block.mqttpasswd.c_str() ) ; | |
mqttclient.connect(); | |
} | |
} | |
else | |
{ | |
currentpreset = ini_block.newpreset ; // No network: do not start radio | |
} | |
delay ( 1000 ) ; // Show IP for a wile | |
analogrest = ( analogRead ( A0 ) + asw1 ) / 2 ; // Assumed inactive analog input | |
} | |
//****************************************************************************************** | |
// X M L C A L L B A C K * | |
//****************************************************************************************** | |
// Process XML tags into variables. * | |
//****************************************************************************************** | |
void XML_callback ( uint8_t statusflags, char* tagName, uint16_t tagNameLen, | |
char* data, uint16_t dataLen ) | |
{ | |
if ( statusflags & STATUS_START_TAG ) | |
{ | |
if ( tagNameLen ) | |
{ | |
xmlOpen = String ( tagName ) ; | |
//dbgprint ( "Start tag %s",tagName ) ; | |
} | |
} | |
else if ( statusflags & STATUS_END_TAG ) | |
{ | |
//dbgprint ( "End tag %s", tagName ) ; | |
} | |
else if ( statusflags & STATUS_TAG_TEXT ) | |
{ | |
xmlTag = String( tagName ) ; | |
xmlData = String( data ) ; | |
//dbgprint ( Serial.print( "Tag: %s, text: %s", tagName, data ) ; | |
} | |
else if ( statusflags & STATUS_ATTR_TEXT ) | |
{ | |
//dbgprint ( "Attribute: %s, text: %s", tagName, data ) ; | |
} | |
else if ( statusflags & STATUS_ERROR ) | |
{ | |
//dbgprint ( "XML Parsing error Tag: %s, text: %s", tagName, data ) ; | |
} | |
} | |
//****************************************************************************************** | |
// X M L P A R S E * | |
//****************************************************************************************** | |
// Parses streams from XML data. * | |
//****************************************************************************************** | |
String xmlparse ( String mount ) | |
{ | |
// Example URL for XML Data Stream: | |
// http://playerservices.streamtheworld.com/api/livestream?version=1.9&mount=IHR_TRANAAC&lang=en | |
// Clear all variables for use. | |
char tmpstr[200] ; // Full GET command, later stream URL | |
char c ; // Next input character from reply | |
String urlout ; // Result URL | |
bool urlfound = false ; // Result found | |
stationServer = "" ; | |
stationPort = "" ; | |
stationMount = "" ; | |
xmlTag = "" ; | |
xmlData = "" ; | |
stop_mp3client() ; // Stop any current wificlient connections. | |
dbgprint ( "Connect to new iHeartRadio host: %s", mount.c_str() ) ; | |
datamode = INIT ; // Start default in metamode | |
chunked = false ; // Assume not chunked | |
// Create a GET commmand for the request. | |
sprintf ( tmpstr, xmlget, mount.c_str() ) ; | |
dbgprint ( "%s", tmpstr ) ; | |
// Connect to XML stream. | |
if ( mp3client.connect ( xmlhost, xmlport ) ) | |
{ | |
dbgprint ( "Connected!" ) ; | |
mp3client.print ( String ( tmpstr ) + " HTTP/1.1\r\n" | |
"Host: " + xmlhost + "\r\n" | |
"User-Agent: Mozilla/5.0\r\n" | |
"Connection: close\r\n\r\n" ) ; | |
// Check for XML Data. | |
while ( true ) | |
{ | |
if ( mp3client.available() ) | |
{ | |
char c = mp3client.read() ; | |
if ( c == '<' ) | |
{ | |
c = mp3client.read() ; | |
if ( c == '?' ) | |
{ | |
xml.processChar ( '<' ) ; | |
xml.processChar ( '?' ) ; | |
break ; | |
} | |
} | |
} | |
yield() ; | |
} | |
// Process XML Data. | |
dbgprint ( "XML parser processing..." ) ; | |
while (true) | |
{ | |
if ( mp3client.available() ) | |
{ | |
c = mp3client.read() ; | |
xml.processChar ( c ) ; | |
if ( xmlTag != "" ) // Tag seen? | |
{ | |
if ( xmlTag.endsWith ( "/status-code" ) ) // Status code seen? | |
{ | |
if ( xmlData != "200" ) // Good result? | |
{ | |
dbgprint ( "Bad xml status-code %s", // No, show and stop interpreting | |
xmlData.c_str() ) ; | |
break ; | |
} | |
} | |
if ( xmlTag.endsWith ( "/ip" ) ) | |
{ | |
stationServer = xmlData ; | |
} | |
else if ( xmlTag.endsWith ( "/port" ) ) | |
{ | |
stationPort = xmlData ; | |
} | |
else if ( xmlTag.endsWith ( "/mount" ) ) | |
{ | |
stationMount = xmlData ; | |
} | |
} | |
} | |
// Check if all the station values are stored. | |
urlfound = ( stationServer != "" && stationPort != "" && stationMount != "" ) ; | |
if ( urlfound ) | |
{ | |
xml.reset() ; | |
break ; | |
} | |
yield() ; | |
} | |
tmpstr[0] = '\0' ; | |
if ( urlfound ) | |
{ | |
sprintf ( tmpstr, "%s:%s/%s_SC", // Build URL for ESP-Radio to stream. | |
stationServer.c_str(), | |
stationPort.c_str(), | |
stationMount.c_str() ) ; | |
dbgprint ( "Found: %s", tmpstr ) ; | |
} | |
dbgprint ( "Closing XML connection." ) ; | |
} | |
else | |
{ | |
dbgprint ( "Can't connect to XML host!" ) ; | |
tmpstr[0] = '\0' ; | |
} | |
return String ( tmpstr ) ; // Return final streaming URL. | |
} | |
//****************************************************************************************** | |
// H A N D L E I P P U B * | |
//****************************************************************************************** | |
// Handle publish op IP to MQTT. This will happen every 10 minutes. * | |
//****************************************************************************************** | |
void handleIpPub() | |
{ | |
static uint32_t pubtime = 300000 ; // Limit save to once per 10 minutes | |
if ( ( millis() - pubtime ) < 600000 ) // 600 sec is 10 minutes | |
{ | |
return ; | |
} | |
pubtime = millis() ; // Set time of last publish | |
if ( !mqttclient.connected() ) // See if connected | |
{ | |
mqttclient.connect() ; // No, reconnect | |
} | |
else | |
{ | |
mqttpub.trigger ( MQTT_IP ) ; // Request re-publish IP | |
// Check if any publishing to do | |
} | |
//mqttclient.loop() ; // Handling of MQTT connection | |
} | |
//****************************************************************************************** | |
// M P 3 L O O P * | |
//****************************************************************************************** | |
// Called from the main loop() for the mp3 functions. * | |
// A connection to an MP3 server is active and we are ready to receive data. * | |
// Normally there is about 2 to 4 kB available in the data stream. This depends on the * | |
// sender. * | |
//****************************************************************************************** | |
void mp3loop() | |
{ | |
static uint8_t tcpbuff[1024] ; // Input buffer from mp3client stream | |
uint32_t maxfilechunk ; // Max number of bytes to read from file | |
uint32_t maxtcpchunk ; // Max number to read from mp3 stream | |
int res ; // Result reading from mp3 stream | |
int i ; // Index in tcpbuff | |
uint32_t rs ; // Free space in ringbuffer | |
uint32_t av ; // Available in stream | |
// Try to keep the ringbuffer filled up by adding as much bytes as possible | |
if ( datamode & ( INIT | HEADER | DATA | // Test op playing | |
METADATA | PLAYLISTINIT | | |
PLAYLISTHEADER | | |
PLAYLISTDATA ) ) | |
{ | |
if ( localfile ) | |
{ | |
maxfilechunk = mp3file.available() ; // Bytes left in file | |
if ( maxfilechunk > 1024 ) // Reduce byte count for this mp3loop() | |
{ | |
maxfilechunk = 1024 ; | |
} | |
while ( ringspace() && maxfilechunk-- ) | |
{ | |
putring ( mp3file.read() ) ; // Yes, store one byte in ringbuffer | |
yield() ; | |
} | |
} | |
else | |
{ | |
maxfilechunk = mp3client.available() ; // Bytes available from mp3 server | |
if ( maxfilechunk > 1024 ) // Reduce byte count for this loop() | |
{ | |
maxfilechunk = 1024 ; | |
} | |
while ( ringspace() && maxfilechunk-- ) | |
{ | |
putring ( mp3client.read() ) ; // Yes, store one byte in ringbuffer | |
yield() ; | |
} | |
} | |
yield() ; | |
} | |
while ( vs1053player.data_request() && ringavail() ) // Try to keep VS1053 filled | |
{ | |
handlebyte_ch ( getring() ) ; // Yes, handle it | |
} | |
yield() ; | |
if ( datamode == STOPREQD ) // STOP requested? | |
{ | |
dbgprint ( "STOP requested" ) ; | |
if ( localfile ) | |
{ | |
mp3file.close() ; | |
} | |
else | |
{ | |
stop_mp3client() ; // Disconnect if still connected | |
} | |
handlebyte_ch ( 0, true ) ; // Force flush of buffer | |
vs1053player.setVolume ( 0 ) ; // Mute | |
vs1053player.stopSong() ; // Stop playing | |
emptyring() ; // Empty the ringbuffer | |
datamode = STOPPED ; // Yes, state becomes STOPPED | |
mqttclr() ; | |
//state = "Stopped"; | |
//mqttpub.trigger ( MQTT_PLAYSTATE ) ; // Request publishing to MQTT | |
#if defined ( USETFT ) | |
tft.fillRect ( 0, 0, 160, 128, BLACK ) ; // Clear screen does not work when rotated | |
#endif | |
delay ( 500 ) ; | |
} | |
if ( localfile ) | |
{ | |
if ( datamode & ( INIT | HEADER | DATA | // Test op playing | |
METADATA | PLAYLISTINIT | | |
PLAYLISTHEADER | | |
PLAYLISTDATA ) ) | |
{ | |
if ( ( mp3file.available() == 0 ) && ( ringavail() == 0 ) ) | |
{ | |
datamode = STOPREQD ; // End of local mp3-file detected | |
} | |
} | |
} | |
if ( ini_block.newpreset != currentpreset ) // New station or next from playlist requested? | |
{ | |
if ( datamode != STOPPED ) // Yes, still busy? | |
{ | |
datamode = STOPREQD ; // Yes, request STOP | |
} | |
else | |
{ | |
if ( playlist_num ) // Playing from playlist? | |
{ // Yes, retrieve URL of playlist | |
playlist_num += ini_block.newpreset - | |
currentpreset ; // Next entry in playlist | |
ini_block.newpreset = currentpreset ; // Stay at current preset | |
} | |
else | |
{ | |
host = readhostfrominifile(ini_block.newpreset) ; // Lookup preset in ini-file | |
} | |
dbgprint ( "New preset/file requested (%d/%d) from %s", | |
currentpreset, playlist_num, host.c_str() ) ; | |
if ( host != "" ) // Preset in ini-file? | |
{ | |
hostreq = true ; // Force this station as new preset | |
} | |
else | |
{ | |
// This preset is not available, return to preset 0, will be handled in next mp3loop() | |
dbgprint ( "No host for this preset" ) ; | |
ini_block.newpreset = 0 ; // Wrap to first station | |
} | |
} | |
} | |
if ( hostreq ) // New preset or station? | |
{ | |
hostreq = false ; | |
currentpreset = ini_block.newpreset ; // Remember current preset | |
localfile = host.startsWith ( "localhost/" ) ; // Find out if this URL is on localhost | |
if ( localfile ) // Play file from localhost? | |
{ | |
if ( connecttofile() ) // Yes, open mp3-file | |
{ | |
datamode = DATA ; // Start in DATA mode | |
} | |
} | |
else | |
{ | |
if ( host.startsWith ( "ihr/" ) ) // iHeartRadio station requested? | |
{ | |
host = host.substring ( 4 ) ; // Yes, remove "ihr/" | |
host = xmlparse ( host ) ; // Parse the xml to get the host | |
} | |
connecttohost() ; // Switch to new host | |
} | |
} | |
if ( xmlreq ) // Directly xml requested? | |
{ | |
xmlreq = false ; // Yes, clear request flag | |
host = xmlparse ( host ) ; // Parse the xml to get the host | |
connecttohost() ; // and connect to this host | |
} | |
} | |
//****************************************************************************************** | |
// L O O P * | |
//****************************************************************************************** | |
// Main loop of the program. * | |
//****************************************************************************************** | |
void loop() | |
{ | |
mp3loop() ; // Do mp3 related actions | |
if ( reqtone ) // Request to change tone? | |
{ | |
reqtone = false ; | |
vs1053player.setTone ( ini_block.rtone ) ; // Set SCI_BASS to requested value | |
} | |
if ( resetreq ) // Reset requested? | |
{ | |
delay ( 1000 ) ; // Yes, wait some time | |
ESP.restart() ; // Reboot | |
} | |
if ( muteflag ) | |
{ | |
vs1053player.setVolume ( 0 ) ; // Mute | |
} | |
else | |
{ | |
vs1053player.setVolume ( ini_block.reqvol ) ; // Unmute | |
} | |
displayvolume() ; // Show volume on display | |
if ( testfilename.length() ) // File to test? | |
{ | |
testfile ( testfilename ) ; // Yes, do the test | |
testfilename = "" ; // Clear test request | |
} | |
scanserial() ; // Handle serial input | |
ArduinoOTA.handle() ; // Check for OTA | |
// Handle MQTT. | |
if ( mqtt_on ) | |
{ | |
if ( !mqttclient.connected() ) // See if connected | |
{ | |
;//mqttclient.connect() ; // No, reconnect | |
} | |
else | |
{ | |
mqttpub.publishtopic() ; // Check if any publishing to do | |
} | |
//mqttclient.loop() ; // Handling of MQTT connection | |
} | |
//handleSaveReq() ; // See if time to save settings | |
handleIpPub() ; // See if time to publish IP | |
} | |
//****************************************************************************************** | |
// C H K H D R L I N E * | |
//****************************************************************************************** | |
// Check if a line in the header is a reasonable headerline. * | |
// Normally it should contain something like "icy-xxxx:abcdef". * | |
//****************************************************************************************** | |
bool chkhdrline ( const char* str ) | |
{ | |
char b ; // Byte examined | |
int len = 0 ; // Lengte van de string | |
while ( ( b = *str++ ) ) // Search to end of string | |
{ | |
len++ ; // Update string length | |
if ( ! isalpha ( b ) ) // Alpha (a-z, A-Z) | |
{ | |
if ( b != '-' ) // Minus sign is allowed | |
{ | |
if ( b == ':' ) // Found a colon? | |
{ | |
return ( ( len > 5 ) && ( len < 50 ) ) ; // Yes, okay if length is okay | |
} | |
else | |
{ | |
return false ; // Not a legal character | |
} | |
} | |
} | |
} | |
return false ; // End of string without colon | |
} | |
//****************************************************************************************** | |
// H A N D L E B Y T E _ C H * | |
//****************************************************************************************** | |
// Handle the next byte of data from server. * | |
// Chunked transfer encoding aware. Chunk extensions are not supported. * | |
//****************************************************************************************** | |
void handlebyte_ch ( uint8_t b, bool force ) | |
{ | |
static int chunksize = 0 ; // Chunkcount read from stream | |
if ( chunked && !force && | |
( datamode & ( DATA | // Test op DATA handling | |
METADATA | | |
PLAYLISTDATA ) ) ) | |
{ | |
if ( chunkcount == 0 ) // Expecting a new chunkcount? | |
{ | |
if ( b == '\r' ) // Skip CR | |
{ | |
return ; | |
} | |
else if ( b == '\n' ) // LF ? | |
{ | |
chunkcount = chunksize ; // Yes, set new count | |
chunksize = 0 ; // For next decode | |
return ; | |
} | |
// We have received a hexadecimal character. Decode it and add to the result. | |
b = toupper ( b ) - '0' ; // Be sure we have uppercase | |
if ( b > 9 ) | |
{ | |
b = b - 7 ; // Translate A..F to 10..15 | |
} | |
chunksize = ( chunksize << 4 ) + b ; | |
} | |
else | |
{ | |
handlebyte ( b, force ) ; // Normal data byte | |
chunkcount-- ; // Update count to next chunksize block | |
} | |
} | |
else | |
{ | |
handlebyte ( b, force ) ; // Normal handling of this byte | |
} | |
} | |
//****************************************************************************************** | |
// M Q T T C L R * | |
//****************************************************************************************** | |
// Clear MQTT information on changes, other wise you will get stuck nfo. * | |
//****************************************************************************************** | |
void mqttclr() | |
{ | |
//ipaddress = "" ; | |
//mqttpub.trigger ( MQTT_IP ) ; // Request publishing to MQTT | |
icyname = "" ; | |
mqttpub.trigger ( MQTT_ICYNAME ) ; // Request publishing to MQTT | |
icystreamtitle = "" ; | |
mqttpub.trigger ( MQTT_STREAMTITLE ) ; // Request publishing to MQTT | |
icyurl = "" ; | |
mqttpub.trigger ( MQTT_ICYURL ) ; // Request publishing to MQTT | |
icygenre = "" ; | |
mqttpub.trigger ( MQTT_ICYGENRE ) ; // Request publishing to MQTT | |
//icydescription = "" ; | |
//mqttpub.trigger ( MQTT_ICYDESCRIPTION ) ; // Request publishing to MQTT | |
icysr = "" ; | |
mqttpub.trigger ( MQTT_ICYSR ) ; // Request publishing to MQTT | |
contenttype = "" ; | |
mqttpub.trigger ( MQTT_ICYCONTENT ) ; // Request publishing to MQTT | |
icybr = "" ; | |
mqttpub.trigger ( MQTT_ICYBITRATE ) ; // Request publishing to MQTT | |
nowplaying = "Player stopped." ; | |
mqttpub.trigger ( MQTT_NOWPLAYING ) ; // Request publishing to MQTT | |
//curvol = "" ; | |
//mqttpub.trigger ( MQTT_VOLUME ) ; // Request publishing to MQTT | |
//curpre = "" ; | |
//mqttpub.trigger ( MQTT_PRESET ) ; // Request publishing to MQTT | |
state = "Stopped." ; | |
mqttpub.trigger ( MQTT_PLAYSTATE ) ; // Request publishing to MQTT | |
} | |
//****************************************************************************************** | |
// H A N D L E B Y T E * | |
//****************************************************************************************** | |
// Handle the next byte of data from server. * | |
// This byte will be send to the VS1053 most of the time. * | |
// Note that the buffer the data chunk must start at an address that is a muttiple of 4. * | |
// Set force to true if chunkbuffer must be flushed. * | |
//****************************************************************************************** | |
void handlebyte ( uint8_t b, bool force ) | |
{ | |
static uint16_t playlistcnt ; // Counter to find right entry in playlist | |
static bool firstmetabyte ; // True if first metabyte (counter) | |
static int LFcount ; // Detection of end of header | |
static __attribute__((aligned(4))) uint8_t buf[32] ; // Buffer for chunk | |
static int bufcnt = 0 ; // Data in chunk | |
static bool firstchunk = true ; // First chunk as input | |
String lcml ; // Lower case metaline | |
String ct ; // Contents type | |
static bool ctseen = false ; // First line of header seen or not | |
int inx ; // Pointer in metaline | |
int i ; // Loop control | |
if ( datamode == INIT ) // Initialize for header receive | |
{ | |
ctseen = false ; // Contents type not seen yet | |
metaint = 0 ; // No metaint found | |
LFcount = 0 ; // For detection end of header | |
bitrate = 0 ; // Bitrate still unknown | |
dbgprint ( "Switch to HEADER" ) ; | |
datamode = HEADER ; // Handle header | |
totalcount = 0 ; // Reset totalcount | |
metaline = "" ; // No metadata yet | |
contenttype = ""; // No content type yet. | |
icyurl = ""; // No icy-url yet. | |
icygenre = ""; // No icy-genre yet. | |
icybr = ""; // No icy-br yet. | |
icysr = ""; // No icy-sr yet. | |
firstchunk = true ; // First chunk expected | |
} | |
if ( datamode == DATA ) // Handle next byte of MP3/Ogg data | |
{ | |
buf[bufcnt++] = b ; // Save byte in chunkbuffer | |
if ( bufcnt == sizeof(buf) || force ) // Buffer full? | |
{ | |
if ( firstchunk ) | |
{ | |
firstchunk = false ; | |
dbgprint ( "First chunk:" ) ; // Header for printout of first chunk | |
for ( i = 0 ; i < 32 ; i += 8 ) // Print 4 lines | |
{ | |
dbgprint ( "%02X %02X %02X %02X %02X %02X %02X %02X", | |
buf[i], buf[i + 1], buf[i + 2], buf[i + 3], | |
buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7] ) ; | |
} | |
} | |
vs1053player.playChunk ( buf, bufcnt ) ; // Yes, send to player | |
bufcnt = 0 ; // Reset count | |
} | |
totalcount++ ; // Count number of bytes, ignore overflow | |
if ( metaint != 0 ) // No METADATA on Ogg streams or mp3 files | |
{ | |
if ( --datacount == 0 ) // End of datablock? | |
{ | |
if ( bufcnt ) // Yes, still data in buffer? | |
{ | |
vs1053player.playChunk ( buf, bufcnt ) ; // Yes, send to player | |
bufcnt = 0 ; // Reset count | |
} | |
datamode = METADATA ; | |
firstmetabyte = true ; // Expecting first metabyte (counter) | |
} | |
} | |
return ; | |
} | |
if ( datamode == HEADER ) // Handle next byte of MP3 header | |
{ | |
if ( ( b > 0x7F ) || // Ignore unprintable characters | |
( b == '\r' ) || // Ignore CR | |
( b == '\0' ) ) // Ignore NULL | |
{ | |
// Yes, ignore | |
} | |
else if ( b == '\n' ) // Linefeed ? | |
{ | |
LFcount++ ; // Count linefeeds | |
if ( chkhdrline ( metaline.c_str() ) ) // Reasonable input? | |
{ | |
lcml = metaline ; // Use lower case for compare | |
lcml.toLowerCase() ; | |
dbgprint ( metaline.c_str() ) ; // Yes, Show it | |
if ( lcml.indexOf ( "content-type" ) >= 0 ) // Line with "Content-Type: xxxx/yyy" | |
{ | |
ctseen = true ; // Yes, remember seeing this | |
ct = metaline.substring ( 14 ) ; // Set contentstype. Not used yet | |
dbgprint ( "%s seen.", ct.c_str() ) ; | |
} | |
if ( lcml.startsWith ( "icy-br:" ) ) | |
{ | |
bitrate = metaline.substring(7).toInt() ; // Found bitrate tag, read the bitrate | |
if ( bitrate == 0 ) // For Ogg br is like "Quality 2" | |
{ | |
bitrate = 87 ; // Dummy bitrate | |
} | |
icybr = String( bitrate ) ; // Get station bitrate. | |
mqttpub.trigger ( MQTT_ICYBITRATE ) ; // Request publishing to MQTT | |
} | |
else if ( lcml.startsWith ( "icy-metaint:" ) ) | |
{ | |
metaint = metaline.substring(12).toInt() ; // Found metaint tag, read the value | |
} | |
else if ( lcml.startsWith ( "icy-name:" ) ) | |
{ | |
icyname = metaline.substring(9) ; // Get station name | |
icyname.trim() ; // Remove leading and trailing spaces | |
mqttpub.trigger ( MQTT_ICYNAME ) ; // Request publishing to MQTT | |
displayinfo ( icyname.c_str(), 60, 68, | |
YELLOW ) ; // Show station name at position 60 | |
} | |
else if ( metaline.startsWith ( "icy-genre:" ) ) | |
{ | |
icygenre = metaline.substring(10) ; // Get icy-genre. | |
icygenre.trim() ; // Remove leading and trailing spaces | |
mqttpub.trigger ( MQTT_ICYGENRE ) ; // Request publishing to MQTT | |
} | |
else if ( metaline.startsWith ( "content-type:" ) || metaline.startsWith ( "Content-Type:" ) ) | |
{ | |
contenttype = metaline.substring(13) ; // Get Content-Type. | |
contenttype.trim() ; // Remove leading and trailing spaces | |
mqttpub.trigger ( MQTT_ICYCONTENT ) ; // Request publishing to MQTT | |
} | |
else if ( metaline.startsWith ( "icy-sr:" ) ) | |
{ | |
icysr = metaline.substring(7) ; // Get icy-sr. | |
icysr.trim() ; // Remove leading and trailing spaces | |
mqttpub.trigger ( MQTT_ICYSR ) ; // Request publishing to MQTT | |
} | |
else if ( metaline.startsWith ( "icy-url:" ) ) | |
{ | |
icyurl = metaline.substring(8) ; // Get icy-url. | |
icyurl.trim() ; // Remove leading and trailing spaces | |
mqttpub.trigger ( MQTT_ICYURL ) ; // Request publishing to MQTT | |
} | |
else if ( lcml.startsWith ( "transfer-encoding:" ) ) | |
{ | |
// Station provides chunked transfer | |
if ( lcml.endsWith ( "chunked" ) ) | |
{ | |
chunked = true ; // Remember chunked transfer mode | |
chunkcount = 0 ; // Expect chunkcount in DATA | |
} | |
} | |
} | |
metaline = "" ; // Reset this line | |
if ( ( LFcount == 2 ) && ctseen ) // Some data seen and a double LF? | |
{ | |
dbgprint ( "Switch to DATA, bitrate is %d" // Show bitrate | |
", metaint is %d", // and metaint | |
bitrate, metaint ) ; | |
datamode = DATA ; // Expecting data now | |
datacount = metaint ; // Number of bytes before first metadata | |
bufcnt = 0 ; // Reset buffer count | |
vs1053player.startSong() ; // Start a new song | |
} | |
} | |
else | |
{ | |
metaline += (char)b ; // Normal character, put new char in metaline | |
LFcount = 0 ; // Reset double CRLF detection | |
} | |
return ; | |
} | |
if ( datamode == METADATA ) // Handle next byte of metadata | |
{ | |
if ( firstmetabyte ) // First byte of metadata? | |
{ | |
firstmetabyte = false ; // Not the first anymore | |
metacount = b * 16 + 1 ; // New count for metadata including length byte | |
if ( metacount > 1 ) | |
{ | |
dbgprint ( "Metadata block %d bytes", | |
metacount - 1 ) ; // Most of the time there are zero bytes of metadata | |
} | |
metaline = "" ; // Set to empty | |
} | |
else | |
{ | |
metaline += (char)b ; // Normal character, put new char in metaline | |
} | |
if ( --metacount == 0 ) | |
{ | |
if ( metaline.length() ) // Any info present? | |
{ | |
// metaline contains artist and song name. For example: | |
// "StreamTitle='Don McLean - American Pie';StreamUrl='';" | |
// Sometimes it is just other info like: | |
// "StreamTitle='60s 03 05 Magic60s';StreamUrl='';" | |
// Isolate the StreamTitle, remove leading and trailing quotes if present. | |
showstreamtitle ( metaline.c_str() ) ; // Show artist and title if present in metadata | |
mqttpub.trigger ( MQTT_STREAMTITLE ) ; // Request publishing to MQTT | |
} | |
if ( metaline.length() > 1500 ) // Unlikely metaline length? | |
{ | |
dbgprint ( "Metadata block too long! Skipping all Metadata from now on." ) ; | |
metaint = 0 ; // Probably no metadata | |
metaline = "" ; // Do not waste memory on this | |
} | |
datacount = metaint ; // Reset data count | |
bufcnt = 0 ; // Reset buffer count | |
datamode = DATA ; // Expecting data | |
} | |
} | |
if ( datamode == PLAYLISTINIT ) // Initialize for receive .m3u file | |
{ | |
// We are going to use metadata to read the lines from the .m3u file | |
metaline = "" ; // Prepare for new line | |
LFcount = 0 ; // For detection end of header | |
datamode = PLAYLISTHEADER ; // Handle playlist data | |
playlistcnt = 1 ; // Reset for compare | |
totalcount = 0 ; // Reset totalcount | |
dbgprint ( "Read from playlist" ) ; | |
} | |
if ( datamode == PLAYLISTHEADER ) // Read header | |
{ | |
if ( ( b > 0x7F ) || // Ignore unprintable characters | |
( b == '\r' ) || // Ignore CR | |
( b == '\0' ) ) // Ignore NULL | |
{ | |
// Yes, ignore | |
} | |
else if ( b == '\n' ) // Linefeed ? | |
{ | |
LFcount++ ; // Count linefeeds | |
dbgprint ( "Playlistheader: %s", // Show playlistheader | |
metaline.c_str() ) ; | |
metaline = "" ; // Ready for next line | |
if ( LFcount == 2 ) | |
{ | |
dbgprint ( "Switch to PLAYLISTDATA" ) ; | |
datamode = PLAYLISTDATA ; // Expecting data now | |
return ; | |
} | |
} | |
else | |
{ | |
metaline += (char)b ; // Normal character, put new char in metaline | |
LFcount = 0 ; // Reset double CRLF detection | |
} | |
} | |
if ( datamode == PLAYLISTDATA ) // Read next byte of .m3u file data | |
{ | |
if ( ( b > 0x7F ) || // Ignore unprintable characters | |
( b == '\r' ) || // Ignore CR | |
( b == '\0' ) ) // Ignore NULL | |
{ | |
// Yes, ignore | |
} | |
else if ( b == '\n' ) // Linefeed ? | |
{ | |
dbgprint ( "Playlistdata: %s", // Show playlistheader | |
metaline.c_str() ) ; | |
if ( metaline.length() < 5 ) // Skip short lines | |
{ | |
return ; | |
} | |
if ( metaline.indexOf ( "#EXTINF:" ) >= 0 ) // Info? | |
{ | |
if ( playlist_num == playlistcnt ) // Info for this entry? | |
{ | |
inx = metaline.indexOf ( "," ) ; // Comma in this line? | |
if ( inx > 0 ) | |
{ | |
// Show artist and title if present in metadata | |
showstreamtitle ( metaline.substring ( inx + 1 ).c_str(), true ) ; | |
mqttpub.trigger ( MQTT_STREAMTITLE ) ; // Request publishing to MQTT | |
} | |
} | |
} | |
if ( metaline.startsWith ( "#" ) ) // Commentline? | |
{ | |
metaline = "" ; | |
return ; // Ignore commentlines | |
} | |
// Now we have an URL for a .mp3 file or stream. Is it the rigth one? | |
dbgprint ( "Entry %d in playlist found: %s", playlistcnt, metaline.c_str() ) ; | |
if ( playlist_num == playlistcnt ) | |
{ | |
inx = metaline.indexOf ( "http://" ) ; // Search for "http://" | |
if ( inx >= 0 ) // Does URL contain "http://"? | |
{ | |
host = metaline.substring ( inx + 7 ) ; // Yes, remove it and set host | |
} | |
else | |
{ | |
host = metaline ; // Yes, set new host | |
} | |
connecttohost() ; // Connect to it | |
} | |
metaline = "" ; | |
host = playlist ; // Back to the .m3u host | |
playlistcnt++ ; // Next entry in playlist | |
} | |
else | |
{ | |
metaline += (char)b ; // Normal character, add it to metaline | |
} | |
return ; | |
} | |
} | |
//****************************************************************************************** | |
// G E T C O N T E N T T Y P E * | |
//****************************************************************************************** | |
// Returns the contenttype of a file to send. * | |
//****************************************************************************************** | |
String getContentType ( String filename ) | |
{ | |
if ( filename.endsWith ( ".html" ) ) return "text/html" ; | |
else if ( filename.endsWith ( ".png" ) ) return "image/png" ; | |
else if ( filename.endsWith ( ".gif" ) ) return "image/gif" ; | |
else if ( filename.endsWith ( ".jpg" ) ) return "image/jpeg" ; | |
else if ( filename.endsWith ( ".ico" ) ) return "image/x-icon" ; | |
else if ( filename.endsWith ( ".css" ) ) return "text/css" ; | |
else if ( filename.endsWith ( ".zip" ) ) return "application/x-zip" ; | |
else if ( filename.endsWith ( ".gz" ) ) return "application/x-gzip" ; | |
else if ( filename.endsWith ( ".mp3" ) ) return "audio/mpeg" ; | |
else if ( filename.endsWith ( ".pw" ) ) return "" ; // Passwords are secret | |
return "text/plain" ; | |
} | |
//****************************************************************************************** | |
// H A N D L E F I L E U P L O A D * | |
//****************************************************************************************** | |
// Handling of upload request. Write file to SPIFFS. * | |
//****************************************************************************************** | |
void handleFileUpload ( AsyncWebServerRequest *request, String filename, | |
size_t index, uint8_t *data, size_t len, bool final ) | |
{ | |
String path ; // Filename including "/" | |
static File f ; // File handle output file | |
char* reply ; // Reply for webserver | |
static uint32_t t ; // Timer for progress messages | |
uint32_t t1 ; // For compare | |
static uint32_t totallength ; // Total file length | |
static size_t lastindex ; // To test same index | |
if ( index == 0 ) | |
{ | |
path = String ( "/" ) + filename ; // Form SPIFFS filename | |
SPIFFS.remove ( path ) ; // Remove old file | |
f = SPIFFS.open ( path, "w" ) ; // Create new file | |
t = millis() ; // Start time | |
totallength = 0 ; // Total file lengt still zero | |
lastindex = 0 ; // Prepare test | |
} | |
t1 = millis() ; // Current timestamp | |
// Yes, print progress | |
dbgprint ( "File upload %s, t = %d msec, len %d, index %d", | |
filename.c_str(), t1 - t, len, index ) ; | |
if ( len ) // Something to write? | |
{ | |
if ( ( index != lastindex ) || ( index == 0 ) ) // New chunk? | |
{ | |
f.write ( data, len ) ; // Yes, transfer to SPIFFS | |
totallength += len ; // Update stored length | |
lastindex = index ; // Remenber this part | |
} | |
} | |
if ( final ) // Was this last chunk? | |
{ | |
f.close() ; // Yes, clode the file | |
reply = dbgprint ( "File upload %s, %d bytes finished", | |
filename.c_str(), totallength ) ; | |
request->send ( 200, "", reply ) ; | |
} | |
} | |
//****************************************************************************************** | |
// H A N D L E F S F * | |
//****************************************************************************************** | |
// Handling of requesting files from the SPIFFS/PROGMEM. Example: /favicon.ico * | |
//****************************************************************************************** | |
void handleFSf ( AsyncWebServerRequest* request, const String& filename ) | |
{ | |
static String ct ; // Content type | |
AsyncWebServerResponse *response ; // For extra headers | |
dbgprint ( "FileRequest received %s", filename.c_str() ) ; | |
ct = getContentType ( filename ) ; // Get content type | |
if ( ( ct == "" ) || ( filename == "" ) ) // Empty is illegal | |
{ | |
request->send ( 404, "text/plain", "File not found" ) ; | |
} | |
else | |
{ | |
if ( filename.indexOf ( "index.html" ) >= 0 ) // Index page is in PROGMEM | |
{ | |
response = request->beginResponse_P ( 200, ct, index_html ) ; | |
} | |
else if ( filename.indexOf ( "radio.css" ) >= 0 ) // CSS file is in PROGMEM | |
{ | |
response = request->beginResponse_P ( 200, ct, radio_css ) ; | |
} | |
else if ( filename.indexOf ( "config.html" ) >= 0 ) // Config page is in PROGMEM | |
{ | |
response = request->beginResponse_P ( 200, ct, config_html ) ; | |
} | |
else if ( filename.indexOf ( "about.html" ) >= 0 ) // About page is in PROGMEM | |
{ | |
response = request->beginResponse_P ( 200, ct, about_html ) ; | |
} | |
else if ( filename.indexOf ( "favicon.ico" ) >= 0 ) // Favicon icon is in PROGMEM | |
{ | |
response = request->beginResponse_P ( 200, ct, favicon_ico, sizeof ( favicon_ico ) ) ; | |
} | |
else | |
{ | |
response = request->beginResponse ( SPIFFS, filename, ct ) ; | |
} | |
// Add extra headers | |
response->addHeader ( "Server", NAME ) ; | |
response->addHeader ( "Cache-Control", "max-age=3600" ) ; | |
response->addHeader ( "Last-Modified", VERSION ) ; | |
request->send ( response ) ; | |
} | |
dbgprint ( "Response sent" ) ; | |
} | |
//****************************************************************************************** | |
// H A N D L E F S * | |
//****************************************************************************************** | |
// Handling of requesting files from the SPIFFS. Example: /favicon.ico * | |
//****************************************************************************************** | |
void handleFS ( AsyncWebServerRequest* request ) | |
{ | |
handleFSf ( request, request->url() ) ; // Rest of handling | |
} | |
//****************************************************************************************** | |
// A N A L Y Z E C M D * | |
//****************************************************************************************** | |
// Handling of the various commands from remote webclient, Serial or MQTT. * | |
// Version for handling string with: <parameter>=<value> * | |
//****************************************************************************************** | |
char* analyzeCmd ( const char* str ) | |
{ | |
char* value ; // Points to value after equalsign in command | |
value = strstr ( str, "=" ) ; // See if command contains a "=" | |
if ( value ) | |
{ | |
*value = '\0' ; // Separate command from value | |
value++ ; // Points to value after "=" | |
} | |
else | |
{ | |
value = (char*) "0" ; // No value, assume zero | |
} | |
return analyzeCmd ( str, value ) ; // Analyze command and handle it | |
} | |
//****************************************************************************************** | |
// C H O M P * | |
//****************************************************************************************** | |
// Do some filtering on de inputstring: * | |
// - String comment part (starting with "#"). * | |
// - Strip trailing CR. * | |
// - Strip leading spaces. * | |
// - Strip trailing spaces. * | |
//****************************************************************************************** | |
String chomp ( String str ) | |
{ | |
int inx ; // Index in de input string | |
if ( ( inx = str.indexOf ( "#" ) ) >= 0 ) // Comment line or partial comment? | |
{ | |
str.remove ( inx ) ; // Yes, remove | |
} | |
str.trim() ; // Remove spaces and CR | |
return str ; // Return the result | |
} | |
//****************************************************************************************** | |
// A N A L Y Z E C M D * | |
//****************************************************************************************** | |
// Handling of the various commands from remote webclient, serial or MQTT. * | |
// par holds the parametername and val holds the value. * | |
// "wifi_00" and "preset_00" may appear more than once, like wifi_01, wifi_02, etc. * | |
// Examples with available parameters: * | |
// preset = 12 // Select start preset to connect to * | |
// preset_00 = <mp3 stream> // Specify station for a preset 00-99 *) * | |
// uppreset = 1 // Go up a preset. * | |
// downpreset = 1 // Go down a preset. * | |
// volume = 95 // Percentage between 0 and 100 * | |
// upvolume = 2 // Add percentage to current volume * | |
// downvolume = 2 // Subtract percentage from current volume * | |
// toneha = <0..15> // Setting treble gain * | |
// tonehf = <0..15> // Setting treble frequency * | |
// tonela = <0..15> // Setting bass gain * | |
// tonelf = <0..15> // Setting treble frequency * | |
// station = <mp3 stream> // Select new station (will not be saved) * | |
// station = <URL>.mp3 // Play standalone .mp3 file (not saved) * | |
// station = <URL>.m3u // Select playlist (will not be saved) * | |
// xml = IHR_TRAN // Select new xml station (will not be saved) * | |
// stop // Stop playing * | |
// resume // Resume playing * | |
// mute // Mute the music * | |
// unmute // Unmute the music * | |
// wifi_00 = mySSID/mypassword // Set WiFi SSID and password *) * | |
// mqttbroker = mybroker.com // Set MQTT broker to use *) * | |
// mqttprefix = XP93g // Set MQTT broker to use * | |
// mqttport = 1883 // Set MQTT port to use, default 1883 *) * | |
// mqttuser = myuser // Set MQTT user for authentication *) * | |
// mqttpasswd = mypassword // Set MQTT password for authentication *) * | |
// mqtttopic = mytopic // Set MQTT topic to subscribe to *) * | |
// mqttpubtopic = mypubtopic // Set MQTT topic to publish to *) * | |
// status // Show current URL to play * | |
// testfile = <file on SPIFFS> // Test SPIFFS reads for debugging purpose * | |
// test // For test purposes * | |
// debug = 0 or 1 // Switch debugging on or off * | |
// reset // Restart the ESP8266 * | |
// analog // Show current analog input * | |
// Commands marked with "*)" are sensible in ini-file only * | |
// Note that it is adviced to avoid expressions as the argument for the abs function. * | |
//****************************************************************************************** | |
char* analyzeCmd ( const char* par, const char* val ) | |
{ | |
String argument ; // Argument as string | |
String value ; // Value of an argument as a string | |
int ivalue ; // Value of argument as an integer | |
static char reply[150] ; // Reply to client, will be returned | |
//static char topic[250] ; // topic for MQTT. | |
//static char message[250] ; // message for MQTT. | |
uint8_t oldvol ; // Current volume | |
bool relative ; // Relative argument (+ or -) | |
int inx ; // Index in string | |
String tmpstr ; // Temporary for value | |
strcpy ( reply, "Command accepted" ) ; // Default reply | |
argument = chomp ( par ) ; // Get the argument | |
if ( argument.length() == 0 ) // Lege commandline (comment)? | |
{ | |
return reply ; // Ignore | |
} | |
argument.toLowerCase() ; // Force to lower case | |
value = chomp ( val ) ; // Get the specified value | |
ivalue = value.toInt() ; // Also as an integer. | |
ivalue = abs ( ivalue ) ; // Make it absolute. | |
relative = argument.indexOf ( "up" ) == 0 ; // + relative setting? | |
if ( argument.indexOf ( "down" ) == 0 ) // - relative setting? | |
{ | |
relative = true ; // It's relative | |
ivalue = - ivalue ; // But with negative value | |
} | |
if ( value.startsWith ( "http://" ) ) // Does (possible) URL contain "http://"? | |
{ | |
value.remove ( 0, 7 ) ; // Yes, remove it | |
} | |
if ( value.length() ) | |
{ | |
tmpstr = value ; // Make local copy of value | |
if ( argument.indexOf ( "passw" ) >= 0 ) // Password in value? | |
{ | |
tmpstr = String ( "******" ) ; // Yes, hide it | |
} | |
dbgprint ( "Command: %s with parameter %s", | |
argument.c_str(), tmpstr.c_str() ) ; | |
} | |
else | |
{ | |
dbgprint ( "Command: %s (without parameter)", | |
argument.c_str() ) ; | |
} | |
if ( argument.indexOf ( "volume" ) >= 0 ) // Volume setting? | |
{ | |
// Volume may be of the form "upvolume", "downvolume" or "volume" for relative or absolute setting | |
oldvol = vs1053player.getVolume() ; // Get current volume | |
if ( relative ) // + relative setting? | |
{ | |
ini_block.reqvol = oldvol + ivalue ; // Up by 0.5 or more dB | |
} | |
else | |
{ | |
ini_block.reqvol = ivalue ; // Absolue setting | |
} | |
if ( ini_block.reqvol > 100 ) | |
{ | |
ini_block.reqvol = 100 ; // Limit to normal values | |
} | |
sprintf ( reply, "Volume is now %d", // Reply new volume | |
ini_block.reqvol ) ; | |
curvolm = vs1053player.getVolume() ; //(char*)ini_block.reqvol ; | |
mqttpub.trigger ( MQTT_VOLUME ) ; // Request publishing to MQTT | |
} | |
else if ( argument == "mute" ) // Mute request | |
{ | |
muteflag = true ; // Request volume to zero | |
state = "Muted" ; // Change player state. | |
mqttpub.trigger ( MQTT_PLAYSTATE ) ; // Request publishing to MQTT | |
} | |
else if ( argument == "unmute" ) // Unmute request? | |
{ | |
muteflag = false ; // Request normal volume | |
state = "Playing" ; // Change player state. | |
mqttpub.trigger ( MQTT_PLAYSTATE ) ; // Request publishing to MQTT | |
} | |
else if ( argument.indexOf ( "preset" ) >= 0 ) // Preset station? | |
{ | |
if ( !argument.startsWith ( "preset_" ) ) // But not a station URL | |
{ | |
if ( relative ) // Relative argument? | |
{ | |
ini_block.newpreset += ivalue ; // Yes, adjust currentpreset | |
} | |
else | |
{ | |
ini_block.newpreset = ivalue ; // Otherwise set preset station | |
} | |
sprintf ( reply, "Preset is now %d", // Reply new preset | |
ini_block.newpreset ) ; | |
curprem = String( ini_block.newpreset ) ; | |
mqttpub.trigger ( MQTT_PRESET ) ; // Request publishing to MQTT | |
playlist_num = 0 ; | |
} | |
} | |
else if ( argument == "stop" ) // Stop requested? | |
{ | |
if ( datamode & ( HEADER | DATA | METADATA | PLAYLISTINIT | | |
PLAYLISTHEADER | PLAYLISTDATA ) ) | |
{ | |
datamode = STOPREQD ; // Request STOP | |
mqttclr() ; | |
//state = "Stopped" ; // Change state to stopped. | |
//mqttpub.trigger ( MQTT_PLAYSTATE ) ; // Request publishing to MQTT | |
} | |
else | |
{ | |
strcpy ( reply, "Command not accepted!" ) ; // Error reply | |
} | |
} | |
else if ( argument == "resume" ) // Request to resume? | |
{ | |
if ( datamode == STOPPED ) // Yes, are we stopped? | |
{ | |
hostreq = true ; // Yes, request restart | |
} | |
} | |
else if ( argument == "station" ) // Station in the form address:port | |
{ | |
if ( datamode & ( HEADER | DATA | METADATA | PLAYLISTINIT | | |
PLAYLISTHEADER | PLAYLISTDATA ) ) | |
{ | |
datamode = STOPREQD ; // Request STOP | |
} | |
host = value ; // Save it for storage and selection later | |
hostreq = true ; // Force this station as new preset | |
sprintf ( reply, | |
"New preset station %s accepted", // Format reply | |
host.c_str() ) ; | |
} | |
else if ( argument == "xml" ) | |
{ | |
if ( datamode & ( HEADER | DATA | METADATA | PLAYLISTINIT | | |
PLAYLISTHEADER | PLAYLISTDATA ) ) | |
{ | |
datamode = STOPREQD ; // Request STOP | |
} | |
host = value ; // Save it for storage and selection later | |
xmlreq = true ; // Run XML parsing process. | |
sprintf ( reply, | |
"New xml preset station %s accepted", // Format reply | |
host.c_str() ) ; | |
} | |
else if ( argument == "status" ) // Status request | |
{ | |
if ( datamode == STOPPED ) | |
{ | |
sprintf ( reply, "Player stopped" ) ; // Format reply | |
} | |
else | |
{ | |
sprintf ( reply, "%s - %s", icyname.c_str(), | |
icystreamtitle.c_str() ) ; // Streamtitle from metadata | |
} | |
} | |
else if ( argument.startsWith ( "reset" ) ) // Reset request | |
{ | |
resetreq = true ; // Reset all | |
} | |
else if ( argument == "testfile" ) // Testfile command? | |
{ | |
testfilename = value ; // Yes, set file to test accordingly | |
} | |
else if ( argument == "test" ) // Test command | |
{ | |
sprintf ( reply, "Free memory is %d, ringbuf %d, stream %d", | |
system_get_free_heap_size(), rcount, mp3client.available() ) ; | |
} | |
// Commands for bass/treble control | |
else if ( argument.startsWith ( "tone" ) ) // Tone command | |
{ | |
if ( argument.indexOf ( "ha" ) > 0 ) // High amplitue? (for treble) | |
{ | |
ini_block.rtone[0] = ivalue ; // Yes, prepare to set ST_AMPLITUDE | |
} | |
if ( argument.indexOf ( "hf" ) > 0 ) // High frequency? (for treble) | |
{ | |
ini_block.rtone[1] = ivalue ; // Yes, prepare to set ST_FREQLIMIT | |
} | |
if ( argument.indexOf ( "la" ) > 0 ) // Low amplitue? (for bass) | |
{ | |
ini_block.rtone[2] = ivalue ; // Yes, prepare to set SB_AMPLITUDE | |
} | |
if ( argument.indexOf ( "lf" ) > 0 ) // High frequency? (for bass) | |
{ | |
ini_block.rtone[3] = ivalue ; // Yes, prepare to set SB_FREQLIMIT | |
} | |
reqtone = true ; // Set change request | |
sprintf ( reply, "Parameter for bass/treble %s set to %d", | |
argument.c_str(), ivalue ) ; | |
} | |
else if ( argument.startsWith ( "mqtt" ) ) // Parameter fo MQTT? | |
{ | |
strcpy ( reply, "MQTT broker parameter changed. Save and restart to have effect" ) ; | |
if ( argument.indexOf ( "broker" ) > 0 ) // Broker specified? | |
{ | |
ini_block.mqttbroker = value.c_str() ; // Yes, set broker accordingly | |
} | |
else if ( argument.indexOf ( "prefix" ) > 0 ) // Port specified? | |
{ | |
ini_block.mqttprefix = value ; // Yes, set port user accordingly | |
} | |
else if ( argument.indexOf ( "port" ) > 0 ) // Port specified? | |
{ | |
ini_block.mqttport = ivalue ; // Yes, set port user accordingly | |
} | |
else if ( argument.indexOf ( "user" ) > 0 ) // User specified? | |
{ | |
ini_block.mqttuser = value ; // Yes, set user accordingly | |
} | |
else if ( argument.indexOf ( "passwd" ) > 0 ) // Password specified? | |
{ | |
ini_block.mqttpasswd = value.c_str() ; // Yes, set broker password accordingly | |
} | |
/* | |
else if ( argument.indexOf ( "pubtopic" ) > 0 ) // Publish topic specified? | |
{ | |
ini_block.mqttpubtopic = value.c_str() ; // Yes, set broker password accordingly | |
} | |
*/ | |
else if ( argument.indexOf ( "topic" ) > 0 ) // Topic specified? | |
{ | |
ini_block.mqtttopic = value.c_str() ; // Yes, set broker topic accordingly | |
} | |
} | |
else if ( argument == "debug" ) // debug on/off request? | |
{ | |
DEBUG = ivalue ; // Yes, set flag accordingly | |
} | |
else if ( argument == "analog" ) // Show analog request? | |
{ | |
sprintf ( reply, "Analog input = %d units", // Read the analog input for test | |
analogRead ( A0 ) ) ; | |
} | |
else if ( argument.startsWith ( "wifi" ) ) // WiFi SSID and passwd? | |
{ | |
inx = value.indexOf ( "/" ) ; // Find separator between ssid and password | |
// Was this the strongest SSID or the only acceptable? | |
if ( num_an == 1 ) | |
{ | |
ini_block.ssid = value.substring ( 0, inx ) ; // Only one. Set as the strongest | |
} | |
if ( value.substring ( 0, inx ) == ini_block.ssid ) | |
{ | |
ini_block.passwd = value.substring ( inx + 1 ) ; // Yes, set password | |
} | |
} | |
else if ( argument == "getnetworks" ) // List all WiFi networks? | |
{ | |
sprintf ( reply, networks.c_str() ) ; // Reply is SSIDs | |
} | |
else | |
{ | |
sprintf ( reply, "%s called with illegal parameter: %s", | |
NAME, argument.c_str() ) ; | |
} | |
return reply ; // Return reply to the caller | |
} | |
//****************************************************************************************** | |
// H A N D L E C M D * | |
//****************************************************************************************** | |
// Handling of the various commands from remote (case sensitive). All commands have the * | |
// form "/?parameter[=value]". Example: "/?volume=50". * | |
// The startpage will be returned if no arguments are given. * | |
// Multiple parameters are ignored. An extra parameter may be "version=<random number>" * | |
// in order to prevent browsers like Edge and IE to use their cache. This "version" is * | |
// ignored. * | |
// Example: "/?upvolume=5&version=0.9775479450590543" * | |
// The save and the list commands are handled specially. * | |
//****************************************************************************************** | |
void handleCmd ( AsyncWebServerRequest* request ) | |
{ | |
AsyncWebParameter* p ; // Points to parameter structure | |
static String argument ; // Next argument in command | |
static String value ; // Value of an argument | |
const char* reply ; // Reply to client | |
//uint32_t t ; // For time test | |
int params ; // Number of params | |
static File f ; // Handle for writing /radio.ini to SPIFFS | |
//t = millis() ; // Timestamp at start | |
params = request->params() ; // Get number of arguments | |
if ( params == 0 ) // Any arguments | |
{ | |
if ( NetworkFound ) | |
{ | |
handleFSf ( request, String( "/index.html") ) ; // No parameters, send the startpage | |
} | |
else | |
{ | |
handleFSf ( request, String( "/config.html") ) ; // Or the configuration page if in AP mode | |
} | |
return ; | |
} | |
p = request->getParam ( 0 ) ; // Get pointer to parameter structure | |
argument = p->name() ; // Get the argument | |
argument.toLowerCase() ; // Force to lower case | |
value = p->value() ; // Get the specified value | |
// For the "save" command, the contents is the value of the next parameter | |
if ( argument.startsWith ( "save" ) && ( params > 1 ) ) | |
{ | |
reply = "Error saving " INIFILENAME ; // Default reply | |
p = request->getParam ( 1 ) ; // Get pointer to next parameter structure | |
if ( p->isPost() ) // Does it have a POST? | |
{ | |
f = SPIFFS.open ( INIFILENAME, "w" ) ; // Save to inifile | |
if ( f ) | |
{ | |
f.print ( p->value() ) ; | |
f.close() ; | |
reply = dbgprint ( "%s saved", INIFILENAME ) ; | |
} | |
} | |
} | |
else if ( argument.startsWith ( "list" ) ) // List all presets? | |
{ | |
dbgprint ( "list request from browser" ) ; | |
request->send ( 200, "text/plain", presetlist ) ; // Send the reply | |
return ; | |
} | |
else | |
{ | |
reply = analyzeCmd ( argument.c_str(), // Analyze it | |
value.c_str() ) ; | |
} | |
request->send ( 200, "text/plain", reply ) ; // Send the reply | |
//t = millis() - t ; | |
// If it takes too long to send a reply, we run into the "LmacRxBlk:1"-problem. | |
// Reset the ESP8266..... | |
//if ( t > 8000 ) | |
//{ | |
// ESP.restart() ; // Last resource | |
//} | |
} |
I see still various "reconnect to MQTT"s. This is allowed only on setup() and in onMqttConnect(). There's no change that this will contribute to the stability.
Sorry Github didn't seem to notify me that you were commenting here. Also, GitHub has an unlimited size for repos, only recommended limitations of 1GB which is only for the code repository so it should not fill up your GitHub account no matter the length of posting issues if you were worried about that. I didn't know how hardcore you were for debug information, should be removed now.
I can't seem to find any information pertaining to that you can't reconnect to ASync MQTT but in those locations and not any other place in your code. Can you show me a reference to this?
I will remove the mqttclient.connect() ; because of what you say but the issue is the client never actually gets properly disconnected from the broker just the client crashes internally on the esp8266 and never tries to reconnect but still continues to play music, the mqtt connect counter never increases past 1. When I added the extra re connect on disconnect other place in the code the first time a while back it would actually try to connect again in the 10min timer which worked, when I added it to the end of the loop like you did with the esp32 version I thought this was a better idea so I switched to that which I think causes it to crash because it doesn't give it enough time to connect before it retries like how I had it previously in a different function.
If I turn off the broker then it will increase the reconnect counter as we tested this with your server when you reset it.
Were you able to run this code base on your esp8266 with vs1053 + ihr radio station xxxxFM etc (not an ihr local station IHR_xxxx) ?
I see various "reconnect to MQTT". You are using the async version, so only reconnect on disconnect.
And please do no fill my Github with long stackdumps.