Diff
checker
Texte
Texte
Images
Documents
Excel
Dossiers
Legal
Enterprise
Application de bureau
Prix
Se connecter
Télécharger Diffchecker Desktop
Comparer le texte
Trouver la différence entre deux fichiers texte
Outils
Historique
Éditeur live
Cacher identiques
Sans retour à la ligne
Vue
Divisé
Unifié
Niveau de précision
Intelligent
Mot
Caractère
Coloration syntaxique
Choisir la syntaxe
Ignorer
Transformer le texte
Aller au premier écart
Modifier l'entrée
Diffchecker Desktop
La façon la plus sécurisée d'utiliser Diffchecker. Obtenez l'application Diffchecker Desktop : vos diffs ne quittent jamais votre ordinateur !
Obtenir Desktop
Untitled diff
Créé
il y a 9 ans
Le diff n'expire jamais
Effacer
Exporter
Partager
Expliquer
39 suppressions
Lignes
Total
Supprimé
Caractères
Total
Supprimé
Pour continuer à utiliser cette fonctionnalité, passez à
Diff
checker
Pro
Voir les prix
495 lignes
Copier tout
26 ajouts
Lignes
Total
Ajouté
Caractères
Total
Ajouté
Pour continuer à utiliser cette fonctionnalité, passez à
Diff
checker
Pro
Voir les prix
474 lignes
Copier tout
//******************************************************************************************
//******************************************************************************************
//* Esp_radio -- Webradio receiver for ESP8266, 1.8 color display and VS1053 MP3 module. *
//* Esp_radio -- Webradio receiver for ESP8266, 1.8 color display and VS1053 MP3 module. *
//* With ESP8266 running at 80 MHz, it is capable of handling up to 256 kb bitrate. *
//* 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. *
//* With ESP8266 running at 160 MHz, it is capable of handling up to 320 kb bitrate. *
//******************************************************************************************
//******************************************************************************************
// ESP8266 libraries used:
// ESP8266 libraries used:
Copier
Copié
Copier
Copié
// - ESP8266WiFi
// - ESP8266WiFi
- Part of ESP8266 Arduino default libraries.
// - SPI
// - SPI
- Part of Arduino default libraries.
// - Adafruit_GFX
// - Adafruit_GFX
- https://github.com/adafruit/Adafruit-GFX-Library
// - TFT_ILI9163C
// - TFT_ILI9163C
- https://github.com/sumotoy/TFT_ILI9163C
// - ESPAsyncTCP
// - ESPAsyncTCP
- https://github.com/me-no-dev/ESPAsyncTCP
// - ESPAsyncWebServer
// - ESPAsyncWebServer
- https://github.com/me-no-dev/ESPAsyncWebServer
// - FS
// - FS
- https://github.com/esp8266/arduino-esp8266fs-plugin/releases/download/0.2.0/ESP8266FS-0.2.0.zip
// - ArduinoOTA
// - ArduinoOTA
Part of ESP8266 Arduino default libraries.
// - AsyncMqttClient
// - AsyncMqttClient
https://github.com/marvinroger/async-mqtt-client
// - Adafruit TinyXML Fork https://github.com/adafruit/TinyXML
//
//
// A library for the VS1053 (for ESP8266) is not available (or not easy to find). Therefore
// 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.
// 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
// See http://www.internet-radio.com for suitable stations. Add the stations of your choice
// to the .ini-file.
// to the .ini-file.
//
//
// Brief description of the program:
// Brief description of the program:
// First a suitable WiFi network is found and a connection is made.
// 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
// 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:
// info in the header in readable ascii, ending with a double CRLF, like:
// icy-name:Classic Rock Florida - SHE Radio
// icy-name:Classic Rock Florida - SHE Radio
// icy-genre:Classic Rock 60s 70s 80s Oldies Miami South Florida
// icy-genre:Classic Rock 60s 70s 80s Oldies Miami South Florida
// icy-url:http://www.ClassicRockFLorida.com
// icy-url:http://www.ClassicRockFLorida.com
// content-type:audio/mpeg
// content-type:audio/mpeg
// icy-pub:1
// icy-pub:1
// icy-metaint:32768 - Metadata after 32768 bytes of MP3-data
// 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"
// 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
// 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.
// 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.
// 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.
// 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
// 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
// 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
// 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.
// 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.
// If no TFT is used, you may use GPIO2 and GPIO15 as control buttons. See definition of "USETFT" below.
// Switches are than programmed as:
// Switches are than programmed as:
// GPIO2 : "Goto station 1"
// GPIO2 : "Goto station 1"
// GPIO0 : "Next station"
// GPIO0 : "Next station"
// GPIO15: "Previous station". Note that GPIO15 has to be LOW when starting the ESP8266.
// 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.
// 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.
// For configuration of the WiFi network(s): see the global data section further on.
//
//
// The SPI interface for VS1053 and TFT uses hardware SPI.
// The SPI interface for VS1053 and TFT uses hardware SPI.
//
//
// Wiring:
// Wiring:
// NodeMCU GPIO Pin to program Wired to LCD Wired to VS1053 Wired to rest
// NodeMCU GPIO Pin to program Wired to LCD Wired to VS1053 Wired to rest
// ------- ------ -------------- --------------- ------------------- ---------------------
// ------- ------ -------------- --------------- ------------------- ---------------------
// D0 GPIO16 16 - pin 1 DCS -
// D0 GPIO16 16 - pin 1 DCS -
// D1 GPIO5 5 - pin 2 CS LED on nodeMCU
// D1 GPIO5 5 - pin 2 CS LED on nodeMCU
// D2 GPIO4 4 - pin 4 DREQ -
// D2 GPIO4 4 - pin 4 DREQ -
// D3 GPIO0 0 FLASH - - Control button "Next station"
// D3 GPIO0 0 FLASH - - Control button "Next station"
// D4 GPIO2 2 pin 3 (D/C) - (OR)Control button "Station 1"
// D4 GPIO2 2 pin 3 (D/C) - (OR)Control button "Station 1"
// D5 GPIO14 14 SCLK pin 5 (CLK) pin 5 SCK -
// D5 GPIO14 14 SCLK pin 5 (CLK) pin 5 SCK -
// D6 GPIO12 12 MISO - pin 7 MISO -
// D6 GPIO12 12 MISO - pin 7 MISO -
// D7 GPIO13 13 MOSI pin 4 (DIN) pin 6 MOSI -
// D7 GPIO13 13 MOSI pin 4 (DIN) pin 6 MOSI -
// D8 GPIO15 15 pin 2 (CS) - (OR)Control button "Previous station"
// D8 GPIO15 15 pin 2 (CS) - (OR)Control button "Previous station"
// D9 GPI03 3 RXD0 - - Reserved serial input
// D9 GPI03 3 RXD0 - - Reserved serial input
// D10 GPIO1 1 TXD0 - - Reserved serial output
// D10 GPIO1 1 TXD0 - - Reserved serial output
// ------- ------ -------------- --------------- ------------------- ---------------------
// ------- ------ -------------- --------------- ------------------- ---------------------
// GND - - pin 8 (GND) pin 8 GND Power supply
// GND - - pin 8 (GND) pin 8 GND Power supply
// VCC 3.3 - - pin 6 (VCC) - LDO 3.3 Volt
// VCC 3.3 - - pin 6 (VCC) - LDO 3.3 Volt
// VCC 5 V - - pin 7 (BL) pin 9 5V Power supply
// VCC 5 V - - pin 7 (BL) pin 9 5V Power supply
// RST - - pin 1 (RST) pin 3 RESET Reset circuit
// 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
// 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.
// (wired OR gate) because there was not a free GPIO output available for this function.
// This circuit is included in the documentation.
// This circuit is included in the documentation.
// Issues:
// Issues:
// Webserver produces error "LmacRxBlk:1" after some time. After that it will work very slow.
// 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 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.
// the problem still exists, but the program will not crash anymore.
// Upload to ESP8266 not reliable.
// Upload to ESP8266 not reliable.
//
//
// 31-03-2016, ES: First set-up.
// 31-03-2016, ES: First set-up.
// 01-04-2016, ES: Detect missing VS1053 at start-up.
// 01-04-2016, ES: Detect missing VS1053 at start-up.
// 05-04-2016, ES: Added commands through http server on port 80.
// 05-04-2016, ES: Added commands through http server on port 80.
// 14-04-2016, ES: Added icon and switch preset on stream error.
// 14-04-2016, ES: Added icon and switch preset on stream error.
// 18-04-2016, ES: Added SPIFFS for webserver.
// 18-04-2016, ES: Added SPIFFS for webserver.
// 19-04-2016, ES: Added ringbuffer.
// 19-04-2016, ES: Added ringbuffer.
// 20-04-2016, ES: WiFi Passwords through SPIFFS files, enable OTA.
// 20-04-2016, ES: WiFi Passwords through SPIFFS files, enable OTA.
// 21-04-2016, ES: Switch to Async Webserver.
// 21-04-2016, ES: Switch to Async Webserver.
// 27-04-2016, ES: Save settings, so same volume and preset will be used after restart.
// 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).
// 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".
// 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.
// 06-05-2016, ES: Allow hiddens WiFi station if this is the only .pw file.
// 07-05-2016, ES: Added preset selection in webserver.
// 07-05-2016, ES: Added preset selection in webserver.
// 12-05-2016, ES: Added support for Ogg-encoder.
// 12-05-2016, ES: Added support for Ogg-encoder.
// 13-05-2016, ES: Better Ogg detection.
// 13-05-2016, ES: Better Ogg detection.
// 17-05-2016, ES: Analog input for commands, extra buttons if no TFT required.
// 17-05-2016, ES: Analog input for commands, extra buttons if no TFT required.
// 26-05-2016, ES: Fixed BUTTON3 bug (no TFT).
// 26-05-2016, ES: Fixed BUTTON3 bug (no TFT).
// 27-05-2016, ES: Fixed restore station at restart.
// 27-05-2016, ES: Fixed restore station at restart.
// 04-07-2016, ES: WiFi.disconnect clears old connection now (thanks to Juppit).
// 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.
// 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.
// 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.
// 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
// 14-10-2016, ES: Updated for async-mqtt-client-master 0.5.0
// 22-10-2016, ES: Correction mute/unmute.
// 22-10-2016, ES: Correction mute/unmute.
// 15-11-2016, ES: Support for .m3u playlists.
// 15-11-2016, ES: Support for .m3u playlists.
// 22-12-2016, ES: Support for localhost (play from SPIFFS).
// 22-12-2016, ES: Support for localhost (play from SPIFFS).
// 28-12-2016, ES: Implement "Resume" request.
// 28-12-2016, ES: Implement "Resume" request.
// 31-12-2016, ES: Allow ContentType "text/css".
// 31-12-2016, ES: Allow ContentType "text/css".
// 02-01-2017, ES: Webinterface in PROGMEM.
// 02-01-2017, ES: Webinterface in PROGMEM.
// 16-01-2017, ES: Correction playlists.
// 16-01-2017, ES: Correction playlists.
// 17-01-2017, ES: Bugfix config page and playlist.
// 17-01-2017, ES: Bugfix config page and playlist.
// 23-01-2017, ES: Bugfix playlist.
// 23-01-2017, ES: Bugfix playlist.
// 26-01-2017, ES: Check on wrong icy-metaint.
// 26-01-2017, ES: Check on wrong icy-metaint.
// 30-01-2017, ES: Allow chunked transfer encoding.
// 30-01-2017, ES: Allow chunked transfer encoding.
// 01-02-2017, ES: Bugfix file upload.
// 01-02-2017, ES: Bugfix file upload.
// 26-04-2017, ES: Better output webinterface on preset change.
// 26-04-2017, ES: Better output webinterface on preset change.
//
//
// Define the version number, also used for webserver as Last-Modified header:
// Define the version number, also used for webserver as Last-Modified header:
#define VERSION "Wed, 26 Apr 2017 08:45:00 GMT"
#define VERSION "Wed, 26 Apr 2017 08:45:00 GMT"
// TFT. Define USETFT if required.
// TFT. Define USETFT if required.
#define USETFT
#define USETFT
#include <ESP8266WiFi.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncWebServer.h>
#include <AsyncMqttClient.h>
#include <AsyncMqttClient.h>
#include <SPI.h>
#include <SPI.h>
#if defined ( USETFT )
#if defined ( USETFT )
#include <Adafruit_GFX.h>
#include <Adafruit_GFX.h>
#include <TFT_ILI9163C.h>
#include <TFT_ILI9163C.h>
#endif
#endif
#include <Ticker.h>
#include <Ticker.h>
#include <stdio.h>
#include <stdio.h>
#include <string.h>
#include <string.h>
#include <FS.h>
#include <FS.h>
#include <ArduinoOTA.h>
#include <ArduinoOTA.h>
Copier
Copié
Copier
Copié
#include <TinyXML.h>
extern "C"
extern "C"
{
{
#include "user_interface.h"
#include "user_interface.h"
}
}
// Definitions for 3 control switches on analog input
// Definitions for 3 control switches on analog input
// You can test the analog input values by holding down the switch and select /?analog=1
// 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.
// in the web interface. See schematics in the documentation.
// Switches are programmed as "Goto station 1", "Next station" and "Previous station" respectively.
// 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.
// Set these values to 2000 if not used or tie analog input to ground.
#define NUMANA 3
#define NUMANA 3
//#define asw1 252
//#define asw1 252
//#define asw2 334
//#define asw2 334
//#define asw3 499
//#define asw3 499
#define asw1 2000
#define asw1 2000
#define asw2 2000
#define asw2 2000
#define asw3 2000
#define asw3 2000
//
//
// Color definitions for the TFT screen (if used)
// Color definitions for the TFT screen (if used)
#define BLACK 0x0000
#define BLACK 0x0000
#define BLUE 0xF800
#define BLUE 0xF800
#define RED 0x001F
#define RED 0x001F
#define GREEN 0x07E0
#define GREEN 0x07E0
#define CYAN GREEN | BLUE
#define CYAN GREEN | BLUE
#define MAGENTA RED | BLUE
#define MAGENTA RED | BLUE
#define YELLOW RED | GREEN
#define YELLOW RED | GREEN
#define WHITE BLUE | RED | GREEN
#define WHITE BLUE | RED | GREEN
// Digital I/O used
// Digital I/O used
// Pins for VS1053 module
// Pins for VS1053 module
#define VS1053_CS 5
#define VS1053_CS 5
#define VS1053_DCS 16
#define VS1053_DCS 16
#define VS1053_DREQ 4
#define VS1053_DREQ 4
// Pins CS and DC for TFT module (if used, see definition of "USETFT")
// Pins CS and DC for TFT module (if used, see definition of "USETFT")
#define TFT_CS 15
#define TFT_CS 15
#define TFT_DC 2
#define TFT_DC 2
// Control button (GPIO) for controlling station
// Control button (GPIO) for controlling station
#define BUTTON1 2
#define BUTTON1 2
#define BUTTON2 0
#define BUTTON2 0
#define BUTTON3 15
#define BUTTON3 15
// Ringbuffer for smooth playing. 20000 bytes is 160 Kbits, about 1.5 seconds at 128kb bitrate.
// Ringbuffer for smooth playing. 20000 bytes is 160 Kbits, about 1.5 seconds at 128kb bitrate.
#define RINGBFSIZ 20000
#define RINGBFSIZ 20000
// Debug buffer size
// Debug buffer size
#define DEBUG_BUFFER_SIZE 100
#define DEBUG_BUFFER_SIZE 100
// Name of the ini file
// Name of the ini file
#define INIFILENAME "/radio.ini"
#define INIFILENAME "/radio.ini"
// Access point name if connection to WiFi network fails. Also the hostname for WiFi and OTA.
// 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.
// Not that the password of an AP must be at least as long as 8 characters.
// Also used for other naming.
// Also used for other naming.
#define NAME "Esp-radio"
#define NAME "Esp-radio"
// Maximum number of MQTT reconnects before give-up
// Maximum number of MQTT reconnects before give-up
#define MAXMQTTCONNECTS 20
#define MAXMQTTCONNECTS 20
//
//
//******************************************************************************************
//******************************************************************************************
// Forward declaration of various functions *
// Forward declaration of various functions *
//******************************************************************************************
//******************************************************************************************
void displayinfo ( const char* str, uint16_t pos, uint16_t height, uint16_t color ) ;
void displayinfo ( const char* str, uint16_t pos, uint16_t height, uint16_t color ) ;
void showstreamtitle ( const char* ml, bool full = false ) ;
void showstreamtitle ( const char* ml, bool full = false ) ;
void handlebyte ( uint8_t b, bool force = false ) ;
void handlebyte ( uint8_t b, bool force = false ) ;
void handlebyte_ch ( uint8_t b, bool force = false ) ;
void handlebyte_ch ( uint8_t b, bool force = false ) ;
void handleFS ( AsyncWebServerRequest* request ) ;
void handleFS ( AsyncWebServerRequest* request ) ;
void handleFSf ( AsyncWebServerRequest* request, const String& filename ) ;
void handleFSf ( AsyncWebServerRequest* request, const String& filename ) ;
void handleCmd ( AsyncWebServerRequest* request ) ;
void handleCmd ( AsyncWebServerRequest* request ) ;
void handleFileUpload ( AsyncWebServerRequest* request, String filename,
void handleFileUpload ( AsyncWebServerRequest* request, String filename,
size_t index, uint8_t* data, size_t len, bool final ) ;
size_t index, uint8_t* data, size_t len, bool final ) ;
char* dbgprint( const char* format, ... ) ;
char* dbgprint( const char* format, ... ) ;
char* analyzeCmd ( const char* str ) ;
char* analyzeCmd ( const char* str ) ;
char* analyzeCmd ( const char* par, const char* val ) ;
char* analyzeCmd ( const char* par, const char* val ) ;
String chomp ( String str ) ;
String chomp ( String str ) ;
void publishIP() ;
void publishIP() ;
//
//
//******************************************************************************************
//******************************************************************************************
// Global data section. *
// Global data section. *
//******************************************************************************************
//******************************************************************************************
// There is a block ini-data that contains some configuration. Configuration data is *
// 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 *
// saved in the SPIFFS file radio.ini by the webinterface. On restart the new data will *
// de read from this file. *
// de read from this file. *
// Items in ini_block can be changed by commands from webserver/MQTT/Serial. *
// Items in ini_block can be changed by commands from webserver/MQTT/Serial. *
//******************************************************************************************
//******************************************************************************************
struct ini_struct
struct ini_struct
{
{
String mqttbroker ; // The name of the MQTT broker server
String mqttbroker ; // The name of the MQTT broker server
uint16_t mqttport ; // Port, default 1883
uint16_t mqttport ; // Port, default 1883
String mqttuser ; // User for MQTT authentication
String mqttuser ; // User for MQTT authentication
String mqttpasswd ; // Password for MQTT authentication
String mqttpasswd ; // Password for MQTT authentication
String mqtttopic ; // Topic to suscribe to
String mqtttopic ; // Topic to suscribe to
String mqttpubtopic ; // Topic to pubtop (IP will be published)
String mqttpubtopic ; // Topic to pubtop (IP will be published)
uint8_t reqvol ; // Requested volume
uint8_t reqvol ; // Requested volume
uint8_t rtone[4] ; // Requested bass/treble settings
uint8_t rtone[4] ; // Requested bass/treble settings
int8_t newpreset ; // Requested preset
int8_t newpreset ; // Requested preset
String ssid ; // SSID of WiFi network to connect to
String ssid ; // SSID of WiFi network to connect to
String passwd ; // Password for WiFi network
String passwd ; // Password for WiFi network
} ;
} ;
enum datamode_t { INIT = 1, HEADER = 2, DATA = 4,
enum datamode_t { INIT = 1, HEADER = 2, DATA = 4,
METADATA = 8, PLAYLISTINIT = 16,
METADATA = 8, PLAYLISTINIT = 16,
PLAYLISTHEADER = 32, PLAYLISTDATA = 64,
PLAYLISTHEADER = 32, PLAYLISTDATA = 64,
STOPREQD = 128, STOPPED = 256
STOPREQD = 128, STOPPED = 256
} ; // State for datastream
} ; // State for datastream
// Global variables
// Global variables
int DEBUG = 1 ;
int DEBUG = 1 ;
ini_struct ini_block ; // Holds configurable data
ini_struct ini_block ; // Holds configurable data
WiFiClient mp3client ; // An instance of the mp3 client
WiFiClient mp3client ; // An instance of the mp3 client
AsyncWebServer cmdserver ( 80 ) ; // Instance of embedded webserver on port 80
AsyncWebServer cmdserver ( 80 ) ; // Instance of embedded webserver on port 80
AsyncMqttClient mqttclient ; // Client for MQTT subscriber
AsyncMqttClient mqttclient ; // Client for MQTT subscriber
IPAddress mqtt_server_IP ; // IP address of MQTT broker
IPAddress mqtt_server_IP ; // IP address of MQTT broker
char cmd[130] ; // Command from MQTT or Serial
char cmd[130] ; // Command from MQTT or Serial
#if defined ( USETFT )
#if defined ( USETFT )
TFT_ILI9163C tft = TFT_ILI9163C ( TFT_CS, TFT_DC ) ;
TFT_ILI9163C tft = TFT_ILI9163C ( TFT_CS, TFT_DC ) ;
#endif
#endif
Ticker tckr ; // For timing 100 msec
Ticker tckr ; // For timing 100 msec
uint32_t totalcount = 0 ; // Counter mp3 data
uint32_t totalcount = 0 ; // Counter mp3 data
datamode_t datamode ; // State of datastream
datamode_t datamode ; // State of datastream
int metacount ; // Number of bytes in metadata
int metacount ; // Number of bytes in metadata
int datacount ; // Counter databytes before metadata
int datacount ; // Counter databytes before metadata
String metaline ; // Readable line in metadata
String metaline ; // Readable line in metadata
String icystreamtitle ; // Streamtitle from metadata
String icystreamtitle ; // Streamtitle from metadata
String icyname ; // Icecast station name
String icyname ; // Icecast station name
int bitrate ; // Bitrate in kb/sec
int bitrate ; // Bitrate in kb/sec
int metaint = 0 ; // Number of databytes between metadata
int metaint = 0 ; // Number of databytes between metadata
int8_t currentpreset = -1 ; // Preset station playing
int8_t currentpreset = -1 ; // Preset station playing
String host ; // The URL to connect to or file to play
String host ; // The URL to connect to or file to play
String playlist ; // The URL of the specified playlist
String playlist ; // The URL of the specified playlist
bool hostreq = false ; // Request for new host
bool hostreq = false ; // Request for new host
bool reqtone = false ; // New tone setting requested
bool reqtone = false ; // New tone setting requested
bool muteflag = false ; // Mute output
bool muteflag = false ; // Mute output
uint8_t* ringbuf ; // Ringbuffer for VS1053
uint8_t* ringbuf ; // Ringbuffer for VS1053
uint16_t rbwindex = 0 ; // Fill pointer in ringbuffer
uint16_t rbwindex = 0 ; // Fill pointer in ringbuffer
uint16_t rbrindex = RINGBFSIZ - 1 ; // Emptypointer in ringbuffer
uint16_t rbrindex = RINGBFSIZ - 1 ; // Emptypointer in ringbuffer
uint16_t rcount = 0 ; // Number of bytes 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 analogsw[NUMANA] = { asw1, asw2, asw3 } ; // 3 levels of analog input
uint16_t analogrest ; // Rest value of analog input
uint16_t analogrest ; // Rest value of analog input
bool resetreq = false ; // Request to reset the ESP8266
bool resetreq = false ; // Request to reset the ESP8266
bool NetworkFound ; // True if WiFi network connected
bool NetworkFound ; // True if WiFi network connected
String networks ; // Found networks
String networks ; // Found networks
String anetworks ; // Aceptable networks (present in .ini file)
String anetworks ; // Aceptable networks (present in .ini file)
String presetlist ; // List for webserver
String presetlist ; // List for webserver
uint8_t num_an ; // Number of acceptable networks in .ini file
uint8_t num_an ; // Number of acceptable networks in .ini file
String testfilename = "" ; // File to test (SPIFFS speed)
String testfilename = "" ; // File to test (SPIFFS speed)
uint16_t mqttcount = 0 ; // Counter MAXMQTTCONNECTS
uint16_t mqttcount = 0 ; // Counter MAXMQTTCONNECTS
int8_t playlist_num = 0 ; // Nonzero for selection from playlist
int8_t playlist_num = 0 ; // Nonzero for selection from playlist
File mp3file ; // File containing mp3 on SPIFFS
File mp3file ; // File containing mp3 on SPIFFS
bool localfile = false ; // Play from local mp3-file or not
bool localfile = false ; // Play from local mp3-file or not
bool chunked = false ; // Station provides chunked transfer
bool chunked = false ; // Station provides chunked transfer
int chunkcount = 0 ; // Counter for chunked transfer
int chunkcount = 0 ; // Counter for chunked transfer
Copier
Copié
Copier
Copié
// XML IHeartRadio Globals.
const char* ihrhost = "playerservices.streamtheworld.com"; // XML data source.
const char* apiVersion = "1.5"; // API Version of IHeartRadio.
const char* mountPoint = "CIMXFMAAC"; // Testing MountPoint (Station Callsign).
const char* lang = "en"; // Language.
const 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 xmlDataLast; // Prevents duplicated tag data.
String stationServer(""); // Radio stream server.
String stationPort(""); // Radio stream port.
String stationMount(""); // Radio stream Callsign.
//******************************************************************************************
//******************************************************************************************
// End of global data section. *
// End of global data section. *
//******************************************************************************************
//******************************************************************************************
//******************************************************************************************
//******************************************************************************************
// Pages and CSS for the webinterface. *
// Pages and CSS for the webinterface. *
//******************************************************************************************
//******************************************************************************************
#include "about_html.h"
#include "about_html.h"
#include "config_html.h"
#include "config_html.h"
#include "index_html.h"
#include "index_html.h"
#include "radio_css.h"
#include "radio_css.h"
#include "favicon_ico.h"
#include "favicon_ico.h"
//
//
//******************************************************************************************
//******************************************************************************************
// VS1053 stuff. Based on maniacbug library. *
// VS1053 stuff. Based on maniacbug library. *
//******************************************************************************************
//******************************************************************************************
// VS1053 class definition. *
// VS1053 class definition. *
//******************************************************************************************
//******************************************************************************************
class VS1053
class VS1053
{
{
private:
private:
uint8_t cs_pin ; // Pin where CS line is connected
uint8_t cs_pin ; // Pin where CS line is connected
uint8_t dcs_pin ; // Pin where DCS 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 dreq_pin ; // Pin where DREQ line is connected
uint8_t curvol ; // Current volume setting 0..100%
uint8_t curvol ; // Current volume setting 0..100%
const uint8_t vs1053_chunk_size = 32 ;
const uint8_t vs1053_chunk_size = 32 ;
// SCI Register
// SCI Register
const uint8_t SCI_MODE = 0x0 ;
const uint8_t SCI_MODE = 0x0 ;
const uint8_t SCI_BASS = 0x2 ;
const uint8_t SCI_BASS = 0x2 ;
const uint8_t SCI_CLOCKF = 0x3 ;
const uint8_t SCI_CLOCKF = 0x3 ;
const uint8_t SCI_AUDATA = 0x5 ;
const uint8_t SCI_AUDATA = 0x5 ;
const uint8_t SCI_WRAM = 0x6 ;
const uint8_t SCI_WRAM = 0x6 ;
const uint8_t SCI_WRAMADDR = 0x7 ;
const uint8_t SCI_WRAMADDR = 0x7 ;
const uint8_t SCI_AIADDR = 0xA ;
const uint8_t SCI_AIADDR = 0xA ;
const uint8_t SCI_VOL = 0xB ;
const uint8_t SCI_VOL = 0xB ;
const uint8_t SCI_AICTRL0 = 0xC ;
const uint8_t SCI_AICTRL0 = 0xC ;
const uint8_t SCI_AICTRL1 = 0xD ;
const uint8_t SCI_AICTRL1 = 0xD ;
const uint8_t SCI_num_registers = 0xF ;
const uint8_t SCI_num_registers = 0xF ;
// SCI_MODE bits
// SCI_MODE bits
const uint8_t SM_SDINEW = 11 ; // Bitnumber in SCI_MODE always on
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_RESET = 2 ; // Bitnumber in SCI_MODE soft reset
const uint8_t SM_CANCEL = 3 ; // Bitnumber in SCI_MODE cancel song
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_TESTS = 5 ; // Bitnumber in SCI_MODE for tests
const uint8_t SM_LINE1 = 14 ; // Bitnumber in SCI_MODE for Line input
const uint8_t SM_LINE1 = 14 ; // Bitnumber in SCI_MODE for Line input
SPISettings VS1053_SPI ; // SPI settings for this slave
SPISettings VS1053_SPI ; // SPI settings for this slave
uint8_t endFillByte ; // Byte to send when stopping song
uint8_t endFillByte ; // Byte to send when stopping song
protected:
protected:
inline void await_data_request() const
inline void await_data_request() const
{
{
while ( !digitalRead ( dreq_pin ) )
while ( !digitalRead ( dreq_pin ) )
{
{
yield() ; // Very short delay
yield() ; // Very short delay
}
}
}
}
inline void control_mode_on() const
inline void control_mode_on() const
{
{
SPI.beginTransaction ( VS1053_SPI ) ; // Prevent other SPI users
SPI.beginTransaction ( VS1053_SPI ) ; // Prevent other SPI users
digitalWrite ( dcs_pin, HIGH ) ; // Bring slave in control mode
digitalWrite ( dcs_pin, HIGH ) ; // Bring slave in control mode
digitalWrite ( cs_pin, LOW ) ;
digitalWrite ( cs_pin, LOW ) ;
}
}
inline void control_mode_off() const
inline void control_mode_off() const
{
{
digitalWrite ( cs_pin, HIGH ) ; // End control mode
digitalWrite ( cs_pin, HIGH ) ; // End control mode
SPI.endTransaction() ; // Allow other SPI users
SPI.endTransaction() ; // Allow other SPI users
}
}
inline void data_mode_on() const
inline void data_mode_on() const
{
{
SPI.beginTransaction ( VS1053_SPI ) ; // Prevent other SPI users
SPI.beginTransaction ( VS1053_SPI ) ; // Prevent other SPI users
digitalWrite ( cs_pin, HIGH ) ; // Bring slave in data mode
digitalWrite ( cs_pin, HIGH ) ; // Bring slave in data mode
digitalWrite ( dcs_pin, LOW ) ;
digitalWrite ( dcs_pin, LOW ) ;
}
}
inline void data_mode_off() const
inline void data_mode_off() const
{
{
digitalWrite ( dcs_pin, HIGH ) ; // End data mode
digitalWrite ( dcs_pin, HIGH ) ; // End data mode
SPI.endTransaction() ; // Allow other SPI users
SPI.endTransaction() ; // Allow other SPI users
}
}
uint16_t read_register ( uint8_t _reg ) const ;
uint16_t read_register ( uint8_t _reg ) const ;
void write_register ( uint8_t _reg, uint16_t _value ) const ;
void write_register ( uint8_t _reg, uint16_t _value ) const ;
void sdi_send_buffer ( uint8_t* data, size_t len ) ;
void sdi_send_buffer ( uint8_t* data, size_t len ) ;
void sdi_send_fillers ( size_t length ) ;
void sdi_send_fillers ( size_t length ) ;
void wram_write ( uint16_t address, uint16_t data ) ;
void wram_write ( uint16_t address, uint16_t data ) ;
uint16_t wram_read ( uint16_t address ) ;
uint16_t wram_read ( uint16_t address ) ;
public:
public:
// Constructor. Only sets pin values. Doesn't touch the chip. Be sure to call begin()!
// 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 ) ;
VS1053 ( uint8_t _cs_pin, uint8_t _dcs_pin, uint8_t _dreq_pin ) ;
void begin() ; // Begin operation. Sets pins correctly,
void begin() ; // Begin operation. Sets pins correctly,
// and prepares SPI bus.
// and prepares SPI bus.
void startSong() ; // Prepare to start playing. Call this each
void startSong() ; // Prepare to start playing. Call this each
// time a new song starts.
// time a new song starts.
void playChunk ( uint8_t* data, size_t len ) ; // Play a chunk of data. Copies the data to
void playChunk ( uint8_t* data, size_t len ) ; // Play a chunk of data. Copies the data to
// the chip. Blocks until complete.
// the chip. Blocks until complete.
void stopSong() ; // Finish playing a song. Call this after
void stopSong() ; // Finish playing a song. Call this after
// the last playChunk call.
// the last playChunk call.
void setVolume ( uint8_t vol ) ; // Set the player volume.Level from 0-100,
void setVolume ( uint8_t vol ) ; // Set the player volume.Level from 0-100,
// higher is louder.
// higher is louder.
void setTone ( uint8_t* rtone ) ; // Set the player baas/treble, 4 nibbles for
void setTone ( uint8_t* rtone ) ; // Set the player baas/treble, 4 nibbles for
// treble gain/freq and bass gain/freq
// treble gain/freq and bass gain/freq
uint8_t getVolume() ; // Get the currenet volume setting.
uint8_t getVolume() ; // Get the currenet volume setting.
// higher is louder.
// higher is louder.
void printDetails ( const char *header ) ; // Print configuration details to serial output.
void printDetails ( const char *header ) ; // Print configuration details to serial output.
void softReset() ; // Do a soft reset
void softReset() ; // Do a soft reset
bool testComm ( const char *header ) ; // Test communication with module
bool testComm ( const char *header ) ; // Test communication with module
inline bool data_request() const
inline bool data_request() const
{
{
return ( digitalRead ( dreq_pin ) == HIGH ) ;
return ( digitalRead ( dreq_pin ) == HIGH ) ;
}
}
} ;
} ;
//******************************************************************************************
//******************************************************************************************
// VS1053 class implementation. *
// VS1053 class implementation. *
//******************************************************************************************
//******************************************************************************************
VS1053::VS1053 ( uint8_t _cs_pin, uint8_t _dcs_pin, uint8_t _dreq_pin ) :
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)
cs_pin(_cs_pin), dcs_pin(_dcs_pin), dreq_pin(_dreq_pin)
{
{
}
}
uint16_t VS1053::read_register ( uint8_t _reg ) const
uint16_t VS1053::read_register ( uint8_t _reg ) const
{
{
uint16_t result ;
uint16_t result ;
control_mode_on() ;
control_mode_on() ;
SPI.write ( 3 ) ; // Read operation
SPI.write ( 3 ) ; // Read operation
SPI.write ( _reg ) ; // Register to write (0..0xF)
SPI.write ( _reg ) ; // Register to write (0..0xF)
// Note: transfer16 does not seem to work
// Note: transfer16 does not seem to work
result = ( SPI.transfer ( 0xFF ) << 8 ) | // Read 16 bits data
result = ( SPI.transfer ( 0xFF ) << 8 ) | // Read 16 bits data
( SPI.transfer ( 0xFF ) ) ;
( SPI.transfer ( 0xFF ) ) ;
await_data_request() ; // Wait for DREQ to be HIGH again
await_data_request() ; // Wait for DREQ to be HIGH again
control_mode_off() ;
control_mode_off() ;
return result ;
return result ;
}
}
void VS1053::write_register ( uint8_t _reg, uint16_t _value ) const
void VS1053::write_register ( uint8_t _reg, uint16_t _value ) const
{
{
control_mode_on( );
control_mode_on( );
SPI.write ( 2 ) ; // Write operation
SPI.write ( 2 ) ; // Write operation
SPI.write ( _reg ) ; // Register to write (0..0xF)
SPI.write ( _reg ) ; // Register to write (0..0xF)
SPI.write16 ( _value ) ; // Send 16 bits data
SPI.write16 ( _value ) ; // Send 16 bits data
await_data_request() ;
await_data_request() ;
control_mode_off() ;
control_mode_off() ;
}
}
void VS1053::sdi_send_buffer ( uint8_t* data, size_t len )
void VS1053::sdi_send_buffer ( uint8_t* data, size_t len )
{
{
size_t chunk_length ; // Length of chunk 32 byte or shorter
size_t chunk_length ; // Length of chunk 32 byte or shorter
data_mode_on() ;
data_mode_on() ;
while ( len ) // More to do?
while ( len ) // More to do?
{
{
await_data_request() ; // Wait for space available
await_data_request() ; // Wait for space available
chunk_length = len ;
chunk_length = len ;
if ( len > vs1053_chunk_size )
if ( len > vs1053_chunk_size )
{
{
chunk_length = vs1053_chunk_size ;
chunk_length = vs1053_chunk_size ;
}
}
len -= chunk_length ;
len -= chunk_length ;
SPI.writeBytes ( data, chunk_length ) ;
SPI.writeBytes ( data, chunk_length ) ;
data += chunk_length ;
data += chunk_length ;
}
}
data_mode_off() ;
data_mode_off() ;
}
}
void VS1053::sdi_send_fillers ( size_t len )
void VS1053::sdi_send_fillers ( size_t len )
{
{
Copier
Copié
Copier
Copié
size_t chunk_length ;
// Length of chunk 32 byte or shorter
size_t chunk_length ;
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 ;
Différences enregistrées
Texte d'origine
Ouvrir un fichier
//****************************************************************************************** //* Esp_radio -- Webradio receiver for ESP8266, 1.8 color display and VS1053 MP3 module. * //* 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 // - SPI // - Adafruit_GFX // - TFT_ILI9163C // - ESPAsyncTCP // - ESPAsyncWebServer // - FS // - ArduinoOTA // - AsyncMqttClient // // 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. // // Define the version number, also used for webserver as Last-Modified header: #define VERSION "Wed, 26 Apr 2017 08:45: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> 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 //#define asw1 252 //#define asw2 334 //#define asw3 499 #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 "Esp-radio" // Maximum number of MQTT reconnects before give-up #define MAXMQTTCONNECTS 20 // //****************************************************************************************** // 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 publishIP() ; // //****************************************************************************************** // 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 uint16_t mqttport ; // Port, default 1883 String mqttuser ; // User for MQTT authentication String mqttpasswd ; // Password for MQTT authentication String mqtttopic ; // Topic to suscribe to String mqttpubtopic ; // Topic to pubtop (IP will be published) 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 } ; 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 ; 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 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 int bitrate ; // Bitrate in kb/sec int metaint = 0 ; // Number of databytes between metadata int8_t currentpreset = -1 ; // Preset station playing String host ; // The URL to connect to or file to play String playlist ; // The URL of the specified playlist 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 ; // True if WiFi network connected 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 //****************************************************************************************** // 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 currenet 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. * //****************************************************************************************** //****************************************************************************************** // 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 } //****************************************************************************************** // 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 ; } //****************************************************************************************** // D B G P R I N T * //****************************************************************************************** // Send a line of info to serial output. Works like vsprintf(), but checks the BEDUg 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 * //*********************4******************************************************************** 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() ; 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 static uint8_t t600 = 0 ; // Counter for 10 minutes 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 } if ( t600++ == 60 ) // 10 minutes over? { t600 = 0 ; // Yes, reset counter dbgprint ( "10 minutes over" ) ; publishIP() ; // Re-publish IP } } } //****************************************************************************************** // A N A G E T S W * //****************************************************************************************** // Translate analog input to switch number. 0 is inactive. * //****************************************************************************************** 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 = abs ( analogsw[i] - v ) ; // Compute difference 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 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 ) 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 ( str ) ; // 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 ; 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 } //****************************************************************************************** // 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, 68, YELLOW ) ; // Show info at position 60 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" ) ) ; 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 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 ; } 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 ) ; } } //****************************************************************************************** // P U B L I S H I P * //****************************************************************************************** // Publish IP to MQTT broker. * //****************************************************************************************** void publishIP() { IPAddress ip ; char ipstr[20] ; // Hold IP as string if ( ini_block.mqttpubtopic.length() ) // Topic to publish? { ip = WiFi.localIP() ; // Publish IP-adress. qos=1, retain=true sprintf ( ipstr, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3] ) ; mqttclient.publish ( ini_block.mqttpubtopic.c_str(), 1, true, ipstr ) ; dbgprint ( "Publishing IP %s to topic %s", ipstr, ini_block.mqttpubtopic.c_str() ) ; } } //****************************************************************************************** // 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(), 2 ) ; dbgprint ( "Subscribing to %s at QoS 2, packetId = %d ", ini_block.mqtttopic.c_str(), packetIdSub ) ; publishIP() ; // Topic to publish: IP } //****************************************************************************************** // 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 } } //****************************************************************************************** // 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'. * //****************************************************************************************** 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], lenght = %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 froma 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 al 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 oif 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 Serial.begin ( 115200 ) ; // For debug Serial.println() ; system_update_cpu_freq ( 160 ) ; // Set to 80/160 MHz ringbuf = (uint8_t *) malloc ( RINGBFSIZ ) ; // Create ring buffer 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 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 { ArduinoOTA.setHostname ( NAME ) ; // Set the hostname ArduinoOTA.onStart ( otastart ) ; ArduinoOTA.begin() ; // Allow update over the air if ( ini_block.mqttbroker.length() ) // Broker specified? { // 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(); } } delay ( 1000 ) ; // Show IP for a wile analogrest = ( analogRead ( A0 ) + asw1 ) / 2 ; // Assumed inactive analog input } //****************************************************************************************** // L O O P * //****************************************************************************************** // Main loop of the program. Minimal time is 20 usec. Will take about 4 msec if VS1053 * // needs data. * // Sometimes the loop is called after an interval of more than 100 msec. * // In that case we will not be able to fill the internal VS1053-fifo in time (especially * // at high bitrate). * // 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 loop() { uint32_t maxfilechunk ; // Max number of bytes to read from // stream or file // 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 loop() { 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 #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 loop() ini_block.newpreset = 0 ; // Wrap to first station } } } if ( hostreq ) // New preset or station? { hostreq = false ; currentpreset = ini_block.newpreset ; // Remember current preset // Find out if this URL is on localhost localfile = ( host.indexOf ( "localhost/" ) >= 0 ) ; if ( localfile ) // Play file from localhost? { if ( connecttofile() ) // Yes, open mp3-file { datamode = INIT ; // Start in INIT mode } } else { connecttohost() ; // Switch to new host } } 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 } 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 } //****************************************************************************************** // 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 } } //****************************************************************************************** // 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 int inx ; // Pointer in metaline int i ; // Loop control if ( datamode == INIT ) // Initialize for header receive { metaint = 0 ; // No metaint found LFcount = 0 ; // For detection end of header bitrate = 0 ; // Bitrate still unknown datamode = HEADER ; // Handle header totalcount = 0 ; // Reset totalcount metaline = "" ; // No metadata 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? { dbgprint ( metaline.c_str() ) ; // Yes, Show it if ( metaline.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 } } else if ( metaline.startsWith ("icy-metaint:" ) ) { metaint = metaline.substring(12).toInt() ; // Found metaint tag, read the value } else if ( metaline.startsWith ( "icy-name:" ) ) { icyname = metaline.substring(9) ; // Get station name icyname.trim() ; // Remove leading and trailing spaces displayinfo ( icyname.c_str(), 60, 68, YELLOW ) ; // Show station name at position 60 } else if ( metaline.startsWith ( "Transfer-Encoding:" ) ) { // Station provides chunked transfer if ( metaline.endsWith ( "chunked" ) ) { chunked = true ; // Remember chunked transfer mode chunkcount = 0 ; // Expect chunkcount in DATA } } } metaline = "" ; // Reset this line if ( LFcount == 2 ) { dbgprint ( "Switch to DATA, bitrate is %d", // Show bitrate bitrate ) ; 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 } 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 ) ; } } } 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 *) * // 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) * // 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 *) * // 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 * //****************************************************************************************** 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[250] ; // Reply to client, will be returned uint8_t oldvol ; // Current volume bool relative ; // Relative argument (+ or -) int inx ; // Index in string 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 = abs ( value.toInt() ) ; // Also as an integer 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() ) { dbgprint ( "Command: %s with parameter %s", argument.c_str(), value.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 ) ; } else if ( argument == "mute" ) // Mute request { muteflag = true ; // Request volume to zero } else if ( argument == "unmute" ) // Unmute request? { muteflag = false ; // Request normal volume } 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 ) ; playlist_num = 0 ; } } else if ( argument == "stop" ) // Stop requested? { if ( datamode & ( HEADER | DATA | METADATA | PLAYLISTINIT | PLAYLISTHEADER | PLAYLISTDATA ) ) { datamode = STOPREQD ; // Request STOP } 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 == "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 ( "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 //} }
Texte modifié
Ouvrir un fichier
//****************************************************************************************** //* Esp_radio -- Webradio receiver for ESP8266, 1.8 color display and VS1053 MP3 module. * //* 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 // - Adafruit 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. // // Define the version number, also used for webserver as Last-Modified header: #define VERSION "Wed, 26 Apr 2017 08:45: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 //#define asw1 252 //#define asw2 334 //#define asw3 499 #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 "Esp-radio" // Maximum number of MQTT reconnects before give-up #define MAXMQTTCONNECTS 20 // //****************************************************************************************** // 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 publishIP() ; // //****************************************************************************************** // 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 uint16_t mqttport ; // Port, default 1883 String mqttuser ; // User for MQTT authentication String mqttpasswd ; // Password for MQTT authentication String mqtttopic ; // Topic to suscribe to String mqttpubtopic ; // Topic to pubtop (IP will be published) 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 } ; 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 ; 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 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 int bitrate ; // Bitrate in kb/sec int metaint = 0 ; // Number of databytes between metadata int8_t currentpreset = -1 ; // Preset station playing String host ; // The URL to connect to or file to play String playlist ; // The URL of the specified playlist 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 ; // True if WiFi network connected 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 IHeartRadio Globals. const char* ihrhost = "playerservices.streamtheworld.com"; // XML data source. const char* apiVersion = "1.5"; // API Version of IHeartRadio. const char* mountPoint = "CIMXFMAAC"; // Testing MountPoint (Station Callsign). const char* lang = "en"; // Language. const 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 xmlDataLast; // Prevents duplicated tag data. 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 currenet 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. * //****************************************************************************************** //****************************************************************************************** // 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 } //****************************************************************************************** // 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 ; } //****************************************************************************************** // D B G P R I N T * //****************************************************************************************** // Send a line of info to serial output. Works like vsprintf(), but checks the BEDUg 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 * //*********************4******************************************************************** 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() ; 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 static uint8_t t600 = 0 ; // Counter for 10 minutes 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 } if ( t600++ == 60 ) // 10 minutes over? { t600 = 0 ; // Yes, reset counter dbgprint ( "10 minutes over" ) ; publishIP() ; // Re-publish IP } } } //****************************************************************************************** // A N A G E T S W * //****************************************************************************************** // Translate analog input to switch number. 0 is inactive. * //****************************************************************************************** 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 = abs ( analogsw[i] - v ) ; // Compute difference 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 } } //****************************************************************************************** // 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); //Serial.print("Start tag "); //Serial.println(tagName); } } else if (statusflags & STATUS_END_TAG) { //Serial.print("End tag "); //Serial.println(tagName); } else if (statusflags & STATUS_TAG_TEXT) { xmlTag = String(tagName); xmlData = String(data); //Serial.print("Tag:"); //Serial.print(tagName); //Serial.print(" text:"); //Serial.println(data); } else if (statusflags & STATUS_ATTR_TEXT) { //Serial.print("Attribute:"); //Serial.print(tagName); //Serial.print(" text:"); //Serial.println(data); } else if (statusflags & STATUS_ERROR) { //Serial.print("XML Parsing error Tag:"); //Serial.print(tagName); //Serial.print(" text:"); //Serial.println(data); } } //****************************************************************************************** // I H E A R T R A D I O * //****************************************************************************************** // Parses streams from IHeartRadio XML data. * //****************************************************************************************** String iheartradio( String mount ) { // Example URL for XML Data Stream: // http://playerservices.streamtheworld.com/api/livestream?version=1.5&mount=CIMXFMAAC&lang=en stop_mp3client() ; // Stop any current wificlient connections. // Create a URI for the request. // Build API URL. Serial.print("Connecting to url "); String urlin = "/api/"; urlin += "livestream"; urlin += "?version="; urlin += apiVersion; urlin += "&mount="; urlin += mount; urlin += "&lang="; urlin += lang; Serial.print("http://"); Serial.print(ihrhost); Serial.print(":"); Serial.print(xmlPort); Serial.println(urlin); // Connect to IHeartRadio. if (mp3client.connect (ihrhost, xmlPort ) ) { Serial.println("Connected"); xmlclient.print( String("GET ") + urlin + " HTTP/1.1\r\n" + "Host: " + ihrhost + "\r\n" + "Connection: close\r\n\r\n"); // Check for XML Data. while (true) { if (mp3client.available()) { char c = mp3client.read(); //xml.processChar(c); //Serial.print(c); if (c=='<') { c=mp3client.read(); if (c=='?') { xml.processChar('<'); xml.processChar('?'); break; } } } } Serial.println("Waiting for response"); // Process XML Data. while (true) { if (mp3client.available()) { char c = mp3client.read(); xml.processChar(c); if (xmlTag != "") { if (xmlTag.endsWith(F("/ip"))) { stationServer = xmlData; //Serial.println(stationServer); }else if (xmlTag.endsWith(F("/port"))) { stationPort = xmlData; //Serial.println(stationPort); }else if (xmlTag.endsWith(F("/mount"))) { stationMount = xmlData; //Serial.println(stationMount); } } //xml.reset(); //Serial.print(c); } // Check if we have all the station values stored. if (stationServer != "" && stationPort != "" && stationMount != "") { break; } } //delay(1000); //Serial.print("http://"); //Serial.print(stationServer); //Serial.print(":"); //Serial.print(stationPort); //Serial.print("/"); //Serial.print(stationMount); //Serial.println("_CS"); // Build URL for Esp-radio to stream. String urlout = "http://"; urlout += stationServer; urlout += ":"; urlout += stationPort; urlout += "/"; urlout += stationMount; urlout += "_CS"; Serial.println(urlout); xmlDataLast = xmlData; } else { Serial.println ("Not Connected to IHR"); } //if (Serial.available()) { //xml.processChar(Serial.read()); //} Serial.println("closing connection"); return urlout; // Return final streaming URL. } //****************************************************************************************** // 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 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 ) 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 ( str ) ; // 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 ; 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 } //****************************************************************************************** // 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, 68, YELLOW ) ; // Show info at position 60 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" ) ) ; 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 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 ; } 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 ) ; } } //****************************************************************************************** // P U B L I S H I P * //****************************************************************************************** // Publish IP to MQTT broker. * //****************************************************************************************** void publishIP() { IPAddress ip ; char ipstr[20] ; // Hold IP as string if ( ini_block.mqttpubtopic.length() ) // Topic to publish? { ip = WiFi.localIP() ; // Publish IP-adress. qos=1, retain=true sprintf ( ipstr, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3] ) ; mqttclient.publish ( ini_block.mqttpubtopic.c_str(), 1, true, ipstr ) ; dbgprint ( "Publishing IP %s to topic %s", ipstr, ini_block.mqttpubtopic.c_str() ) ; } } //****************************************************************************************** // 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(), 2 ) ; dbgprint ( "Subscribing to %s at QoS 2, packetId = %d ", ini_block.mqtttopic.c_str(), packetIdSub ) ; publishIP() ; // Topic to publish: IP } //****************************************************************************************** // 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 } } //****************************************************************************************** // 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'. * //****************************************************************************************** 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], lenght = %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 froma 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 al 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 oif 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 Serial.begin ( 115200 ) ; // For debug Serial.println() ; system_update_cpu_freq ( 160 ) ; // Set to 80/160 MHz ringbuf = (uint8_t *) malloc ( RINGBFSIZ ) ; // Create ring buffer xml.init((uint8_t *)xmlbuffer, sizeof(xmlbuffer), &XML_callback); // Initilize XML stream. 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 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 { ArduinoOTA.setHostname ( NAME ) ; // Set the hostname ArduinoOTA.onStart ( otastart ) ; ArduinoOTA.begin() ; // Allow update over the air if ( ini_block.mqttbroker.length() ) // Broker specified? { // 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(); } } delay ( 1000 ) ; // Show IP for a wile analogrest = ( analogRead ( A0 ) + asw1 ) / 2 ; // Assumed inactive analog input } //****************************************************************************************** // L O O P * //****************************************************************************************** // Main loop of the program. Minimal time is 20 usec. Will take about 4 msec if VS1053 * // needs data. * // Sometimes the loop is called after an interval of more than 100 msec. * // In that case we will not be able to fill the internal VS1053-fifo in time (especially * // at high bitrate). * // 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 loop() { uint32_t maxfilechunk ; // Max number of bytes to read from // stream or file // 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 loop() { 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 #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 loop() ini_block.newpreset = 0 ; // Wrap to first station } } } if ( hostreq ) // New preset or station? { hostreq = false ; currentpreset = ini_block.newpreset ; // Remember current preset // Find out if this URL is on localhost localfile = ( host.indexOf ( "localhost/" ) >= 0 ) ; if ( localfile ) // Play file from localhost? { if ( connecttofile() ) // Yes, open mp3-file { datamode = INIT ; // Start in INIT mode } } else { connecttohost() ; // Switch to new host } } 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 } 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 } //****************************************************************************************** // 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 } } //****************************************************************************************** // 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 int inx ; // Pointer in metaline int i ; // Loop control if ( datamode == INIT ) // Initialize for header receive { metaint = 0 ; // No metaint found LFcount = 0 ; // For detection end of header bitrate = 0 ; // Bitrate still unknown datamode = HEADER ; // Handle header totalcount = 0 ; // Reset totalcount metaline = "" ; // No metadata 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? { dbgprint ( metaline.c_str() ) ; // Yes, Show it if ( metaline.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 } } else if ( metaline.startsWith ("icy-metaint:" ) ) { metaint = metaline.substring(12).toInt() ; // Found metaint tag, read the value } else if ( metaline.startsWith ( "icy-name:" ) ) { icyname = metaline.substring(9) ; // Get station name icyname.trim() ; // Remove leading and trailing spaces displayinfo ( icyname.c_str(), 60, 68, YELLOW ) ; // Show station name at position 60 } else if ( metaline.startsWith ( "Transfer-Encoding:" ) ) { // Station provides chunked transfer if ( metaline.endsWith ( "chunked" ) ) { chunked = true ; // Remember chunked transfer mode chunkcount = 0 ; // Expect chunkcount in DATA } } } metaline = "" ; // Reset this line if ( LFcount == 2 ) { dbgprint ( "Switch to DATA, bitrate is %d", // Show bitrate bitrate ) ; 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 } 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 ) ; } } } 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 *) * // 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) * // 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 *) * // 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 * //****************************************************************************************** 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[250] ; // Reply to client, will be returned uint8_t oldvol ; // Current volume bool relative ; // Relative argument (+ or -) int inx ; // Index in string 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 = abs ( value.toInt() ) ; // Also as an integer 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() ) { dbgprint ( "Command: %s with parameter %s", argument.c_str(), value.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 ) ; } else if ( argument == "mute" ) // Mute request { muteflag = true ; // Request volume to zero } else if ( argument == "unmute" ) // Unmute request? { muteflag = false ; // Request normal volume } 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 ) ; playlist_num = 0 ; } } else if ( argument == "stop" ) // Stop requested? { if ( datamode & ( HEADER | DATA | METADATA | PLAYLISTINIT | PLAYLISTHEADER | PLAYLISTDATA ) ) { datamode = STOPREQD ; // Request STOP } 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 = iheartradio(value);//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 == "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 ( "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 //} }
Trouver la différence