Edzelf / ESP32-Radio

Internet radio based on ESP32, VS1053 and a TFT screen.
GNU General Public License v3.0
964 stars 227 forks source link

Spectrum analyzer #86

Open MirkoDalmonte opened 6 years ago

MirkoDalmonte commented 6 years ago

Spectrum analyzer is not essential but it is beautiful :)

https://m.youtube.com/watch?v=mSrDDmvLRfI

locki-cz commented 6 years ago

Very nice!!! How you added it? How is possible edit custom display positions and text ? Can you kick me little bit?

I want to my project add when you connect it to power it boot, connect wifi etc and stay stopped and just show date, time, day, after button press it start play radio...

MirkoDalmonte commented 6 years ago

Hi, add spectrum analyzer is a bit complex.. I have loaded in the vs1053 the last patch and the dedicated plugin. Every 100ms I read the values ​​directly in vs1053 and display them. This process is quite fast so the radio works well even at 320. Customize the display is very simple, there is a struct (tftdata) to modify existing sectors or add others... so in the displayinfo() function you can customize each sector by comparing the index of this struct. Calling tftset (index, char*) or tftset (index, string) you can display in a specific sector. Spectrum analyzer not use these functions, it uses the display directly, so its space is reserved in the struct (90-128). To leave stopped the radio at the start you must set currentpreset = ini_block.newpreset in setup ().

koskee commented 6 years ago

Any chance you'd post your code for loading the patch/plugin? I've been toying with the idea of doing this for a while, but haven't had a chance to dive in. Also, I have a question about how you display the output... When writing the bars to the screen, how did you manage to avoid overwriting the text the display? Does it automatically keep track of the text or do you have it buffered somewhere or does the screen differentiate between text/fill commands.. ? To me it seems like the text would be covered up unless you replace it every frame, which would be a lot of updating...

MirkoDalmonte commented 6 years ago

Hi koskee, yes.... the code and the answer to your questions as soon as I have time.... soon...

koskee commented 6 years ago

Awesome. I'll keep my eyes open for an update in that case. I'm also not in any rush, but thanks in advance.

koskee commented 5 years ago

@MirkoDalmonte Did you ever get around to posting code for this? Still very much interested..

MirkoDalmonte commented 5 years ago

Hi @koskee, sorry for the delay...

// // ESP32_Radio -- Webradio receiver for ESP32, 1.8 color display and VS1053 MP3 module. // By Ed Smallenburg. // // ESP32 libraries used: // - WiFiMulti // - nvs // - TFT_ILI9163C Sumotoy Version 0.9 // - ArduinoOTA // - PubSubClient // - SD // - FS // A library for the VS1053 (for ESP32) 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 preferences in either Esp32_radio_init.ino sketch or through the webinterface. // // 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 an input button causes the player to execute a programmable command. // // 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. // The SD card interface of the module may be used to play mp3-tracks on the SD card. // // For configuration of the WiFi network(s): see the global data section further on. // // The VSPI interface is used for VS1053, TFT and SD. // // Wiring. Note that this is just an example. Pins (except 18,19 and 23 of the SPI interface) // can be configured in the config page of the web interface. // ESP32dev Signal Wired to LCD Wired to VS1053 SDCARD Wired to the rest // -------- ------ -------------- ------------------- ------ --------------- // GPIO16 - pin 1 XDCS - - // GPIO5 - pin 2 XCS - - // GPIO4 - pin 4 DREQ - - // GPIO2 pin 3 D/C - - - // GPIO18 SCK pin 5 CLK pin 5 SCK CLK - // GPIO19 MISO - pin 7 MISO MISO - // GPIO23 MOSI pin 4 DIN pin 6 MOSI MOSI - // GPIO21 - - CS - // GPIO15 pin 2 CS - - - // GPI03 RXD0 - - - Reserved serial input // GPIO1 TXD0 - - - Reserved serial output // GPIO34 - - - - Optional pull-up resistor // GPIO35 - - - - Infrared receiver VS1838B // GPIO25 - - - - Rotary encoder CLK // GPIO26 - - - - Rotary encoder DT // GPIO27 - - - - Rotary encoder SW // ------- ------ --------------- ------------------- ------ ---------------- // GND - pin 8 GND pin 8 GND Power supply GND // VCC 5 V - pin 7 BL - Power supply // VCC 5 V - pin 6 VCC pin 9 5V Power supply // EN - pin 1 RST pin 3 XRST - // // 26-04-2017, ES: First set-up, derived from ESP8266 version. // 08-05-2017, ES: Handling of preferences. // 20-05-2017, ES: Handling input buttons. // 22-05-2017, ES: Save preset, volume and tone settings. // 23-05-2017, ES: No more calls of non-iram functions on interrupts. // 24-05-2017, ES: Support for featherboard. // 26-05-2017, ES: Correction playing from .m3u playlist. Allow single hidden SSID. // 30-05-2017, ES: Add SD card support (FAT format), volume indicator. // 26-06-2017, ES: Correction: start in AP-mode if no WiFi networks configured. // 28-06-2017, ES: Added IR interface. // 30-06-2017, ES: Improved functions for SD card play. // 03-07-2017, ES: Webinterface control page shows current settings. // 08-07-2017, ES: More space for streamtitle on TFT. // 18-07-2017, ES: Time Of Day on TFT. // 19-07-2017, ES: Minor corrections. // 26-07-2017, ES: Flexible pin assignment. Add rotary encoder switch. // 27-07-2017, ES: Removed tinyXML library. // 18-08-2017, Es: Minor corrections // 28-08-2017, ES: Preferences for pins used for SPI bus, // Corrected bug in handling programmable pins, // Introduced touch pins. // 11-11-2017, ES: Increased ringbuffer. Measure bit rate. // 13-11-2017, ES: Forward declarations. // 16-11-2017, ES: Replaced ringbuffer by FreeRTOS queue, play function on second CPU, // Included improved rotary switch routines supplied by fenyvesi, // Better IR sensitivity. // 30-11-2017, ES: Hide passwords in config page. // 01-12-2017, ES: Better handling of playlist. // 07-12-2017, ES: Faster handling of config screen. // 13-12-2017, ES: Correction clear LCD. // 15-12-2017, ES: Correction defaultprefs.h. // 18-12-2017, ES: Stop playing during config. // 02-01-2018, ES: Stop/resume is same command. // 22-01-2018, ES: Read ADC (GPIO36) and display as a battery capacity percentage. // // // Define the version number, also used for webserver as Last-Modified header:

define VERSION "Mon, 22 Jan 2018 14:40:00 GMT"

include

include

include

include

include

include

include

include

include

include

include

include

include <freertos/queue.h>

include <freertos/task.h>

include

include <driver/adc.h>

// Color definitions for the TFT screen (if used) // TFT has bits 6 bits (0..5) for RED, 6 bits (6..11) for GREEN and 4 bits (12..15) for BLUE.

define BLACK 0x0000

define BLUE 0xF800

define RED 0x001F

define GREEN 0x07E0

define CYAN GREEN | BLUE

define MAGENTA RED | BLUE

define YELLOW RED | GREEN

// Digital I/O used // Default pins for VS1053 module. Better specify these in the preferences

if defined ( ARDUINO_FEATHER_ESP32 )

define VS1053_CS 32

define VS1053_DCS 33

define VS1053_DREQ 15

else

define VS1053_CS 5

define VS1053_DCS 16

define VS1053_DREQ 4

endif

// Number of entries in the queue

define QSIZ 2000

// Debug buffer size

define DEBUG_BUFFER_SIZE 130

// 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 "ESP32Radio"

// Adjust size of buffer to the longest expected string for nvsgetstr

define NVSBUFSIZE 150

// Position (column) of time in topline

define TIMEPOS 108

// SPI speed for SD card

define SDSPEED 38000000

// Size of metaline buffer

define METASIZ 1024

// Max. number of NVS keys in table

define MAXKEYS 200

define ID3_HEADER_TAG 3

define ID3_HEADER 10

define TRACK_TITLE 3

define TRACK_ARTIST 33

define TRACK_ALBUM 63

define TRACK_YEAR 93

//** // Forward declaration and prototypes of various functions. * //** void displaytime ( const char str, uint16_t color = WHITE ) ; void showstreamtitle ( const char ml, bool full = false ) ; void handlebyte_ch ( uint8_t b, boolean last ) ; void handleFSf ( const String& pagename ) ; void handleCmd() ; char dbgprint( const char format, ... ) ; const char analyzeCmd ( const char str ) ; const char analyzeCmd ( const char par, const char val ) ; void chomp ( String &str ) ; String httpheader ( String contentstype ) ; bool nvssearch ( const char key ) ; void mp3loop() ; void tftlog ( const char str ) ; void playtask ( void parameter ) ; void gettime() ;

//** // Several structs. * //** //

struct scrseg_struct // For screen segments { bool update_req ; // Request update of screen uint16_t color ; // Textcolor uint16_t y ; // Begin of segment row uint16_t height ; // Height of segment String str ; // String to be displayed } ;

enum qdata_type { QDATA } ; // datatyp in qdata_struct struct qdata_struct { int datatyp ; // Identifier attribute((aligned(4))) uint8_t buf[32] ; // Buffer for chunk } ;

struct ini_struct { uint8_t reqvol ; // Requested volume uint8_t rtone[4] ; // Requested bass/treble settings int8_t newpreset ; // Requested preset String clk_server ; // Server to be used for time of day clock int8_t clk_offset ; // Offset in hours with respect to UTC int8_t clk_dst ; // Number of hours shift during DST int8_t ir_pin ; // GPIO connected to output of IR decoder int8_t enc_clk_pin ; // GPIO connected to CLK of rotary encoder int8_t enc_dt_pin ; // GPIO connected to DT of rotary encoder int8_t enc_sw_pin ; // GPIO connected to SW of rotary encoder int8_t tft_cs_pin ; // GPIO connected to CS of TFT screen int8_t tft_dc_pin ; // GPIO connected to D/C of TFT screen int8_t sd_cs_pin ; // GPIO connected to CS of SD card int8_t vs_cs_pin ; // GPIO connected to CS of VS1053 int8_t vs_dcs_pin ; // GPIO connected to DCS of VS1053 int8_t vs_dreq_pin ; // GPIO connected to DREQ of VS1053 int8_t vs_shutdown_pin ; // GPIO to shut down the amplifier int8_t spi_sck_pin ; // GPIO connected to SPI SCK pin int8_t spi_miso_pin ; // GPIO connected to SPI MISO pin int8_t spi_mosi_pin ; // GPIO connected to SPI MOSI pin uint16_t bat0 ; // ADC value for 0 percent battery charge uint16_t bat100 ; // ADC value for 100 percent battery charge } ;

struct WifiInfo_t // For list with WiFi info { uint8_t inx ; // Index as in "wifi_00" char ssid ; // SSID for an entry char passphrase ; // Passphrase for an entry } ;

struct nvs_entry { uint8_t Ns ; // Namespace ID uint8_t Type ; // Type of value uint8_t Span ; // Number of entries used for this item uint8_t Rvs ; // Reserved, should be 0xFF uint32_t CRC ; // CRC char Key[16] ; // Key in Ascii uint64_t Data ; // Data in entry } ;

struct nvs_page // For nvs entries { // 1 page is 4096 bytes uint32_t State ; uint32_t Seqnr ;

uint32_t Unused[5] ; uint32_t CRC ; uint8_t Bitmap[32] ; nvs_entry Entry[126] ; } ;

struct keyname_t // For keys in NVS { char Key[16] ; // Mac length is 15 plus delimeter } ;

//** // Global data section. * //** // There is a block ini-data that contains some configuration. Configuration data is // saved in the preferences by the webinterface. On restart the new data will // de read from these preferences. // Items in ini_block can be changed by commands from webserver/Serial. //**

enum datamode_t { INIT = 1, HEADER = 2, DATA = 4, // State for datastream METADATA = 8, PLAYLISTINIT = 16, PLAYLISTHEADER = 32, PLAYLISTDATA = 64, STOPREQD = 128, STOPPED = 256 } ;

// Global variables uint32_t lastseek = 0 ;
uint8_t menux = 0 ; char id3Buffer[100] ; boolean timeLineFileFinisced = false; uint32_t timeLineFilePlayed = 0; uint32_t pretimeLineFilePlayed = 0; uint32_t timeLineFileLength = 0; uint16_t played = 150; uint8_t morethanonc = 0 ; boolean silentreconon = false; unsigned long millisbit = 0; unsigned long millisbands = 0; unsigned long millisvol = 0; uint8_t oldvol = 0 ; int DEBUG = 0 ; // Debug on/off WiFiMulti wifiMulti ; // Possible WiFi networks ini_struct ini_block ; // Holds configurable data WiFiServer cmdserver ( 80 ) ; // Instance of embedded webserver on port 80 WiFiClient mp3client ; // An instance of the mp3 client WiFiClient cmdclient ; // An instance of the client for commands TaskHandle_t maintask ; // Taskhandle for main task TaskHandle_t xplaytask ; // Task handle for playtask SemaphoreHandle_t SPIsem = NULL ; // For exclusive SPI usage hw_timer_t timer = NULL ; // For timer char cmd[130] ; // Command from Serial TFT_ILI9163C tft = NULL ; // For instance of TFT driver QueueHandle_t dataqueue ; qdata_struct outchunk ; // Data to queue qdata_struct inchunk ; // Data from queue uint8_t outqp = outchunk.buf ; // Pointer to buffer in outchunk 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 char metalinebf[METASIZ + 1] ; // Buffer for metaline int16_t metalinebfx ; // Index for metalinebf String mp3Title ; String mp3Artist ; String mp3Album ; String mp3Year ; String icystreamtitle ; // Streamtitle from metadata String icyname ; // Icecast station name String icyurl ; // Icecast station url String ipaddress ; // Own IP-address int bitrate ; // Bitrate in kb/sec int mbitrate ; // Measured bitrate 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 bool resetreq = false ; // Request to reset the ESP32 bool NetworkFound = false ; // True if WiFi network connected String networks ; // Found networks in the surrounding int8_t playingstat = 0 ; // 1 if radio is playing int8_t playlist_num = 0 ; // Nonzero for selection from playlist File mp3file ; // File containing mp3 on SD card bool localfile = false ; // Play from local mp3-file or not bool chunked = false ; // Station provides chunked transfer int chunkcount = 0 ; // Counter for chunked transfer String http_getcmd ; // Contents of last GET command String http_rqfile ; // Requested file bool http_reponse_flag = false ; // Response required uint16_t ir_value = 0 ; // IR code struct tm timeinfo ; // Will be filled by NTP server bool time_req = false ; // Set time requested bool SD_okay = false ; // True if SD card in place and readable String SD_nodelist ; // Nodes of mp3-files on SD int SD_nodecount = 0 ; // Number of nodes in SD_nodelist String SD_currentnode = "" ; // Node ID of song currently playing ("0" if random) uint16_t adcval ; // ADC value (battery voltage) std::vector wifilist ; // List with wifi_xx info // nvs stuff nvs_page nvsbuf ; // Space for 1 page of NVS info const esp_partition_t nvs ; // Pointer to partition struct esp_err_t nvserr ; // Error code from nvs functions uint32_t nvshandle = 0 ; // Handle for nvs access uint8_t namespace_ID ; // Namespace ID found char nvskeys[MAXKEYS][16] ; // Space for NVS keys std::vector keynames ; // Keynames in NVS // Rotary encoder stuff int16_t rotationcount = 0 ; // Current position of rotary switch uint16_t enc_inactivity = 0 ; // Time inactive char timetxt[9] ; // Converted timeinfo bool singleclick = false ; // True if single click detected enum enc_menu_t { VOLUME, PRESET, TRACK, SWMUTE, MENU } ; // State for rotary encoder menu enc_menu_t enc_menu_mode = VOLUME ; // Default is VOLUME mode // Data to display. There are TFTSECS sections bool tft_update_req ; // Global update request uint8_t bands = 14; // Numero bande Spectrum Analyzer uint8_t prevbands = 0; // Numero bande Spectrum Analyzer precedente uint8_t spectrum[14][3]; // Array per Spectrum Analyzer boolean startsong = false; // Start song boolean stopsong = false; // Stop song

define TFTSECS 15

scrseg_struct tftdata[TFTSECS] = // Screen divided in 3 segments + 1 overlay { { false, WHITE, 4, 8, "" }, // Orario, segnale, stato { false, YELLOW, 18, 21, "" }, // Nome Stazione { false, WHITE, 42, 8, "" }, // Genere Stazione { false, WHITE, 58, 32, "" }, // Autore e Titolo Stazione { false, GREEN, 58, 32, "" }, // Rotary encoder { false, YELLOW, 18, 21, "" }, // Titolo Mp3 { false, WHITE, 39, 8, "" }, // id3Artist { false, WHITE, 47, 8, "" }, // id3Title { false, WHITE, 55, 8, "" }, // id3Album { false, WHITE, 63, 8, "" }, // id3Year { false, WHITE, 12, 116, "" }, // Menu clear { false, WHITE, 21, 21, "" }, // Menu line 1 { false, WHITE, 42, 21, "" }, // Menu line 2 { false, WHITE, 63, 21, "" }, // Menu line 3 { false, WHITE, 12, 78, "" } // file - webradio clear } ; // struct progpin_struct // For programmable input pins { int8_t gpio ; // Pin number bool reserved ; // Reserved for connected devices bool avail ; // Pin is available for a command String command ; // Command to execute when activated // Example: "uppreset=1" bool cur ; // Current state, true = HIGH, false = LOW } ;

progpin_struct progpin[] = // Input pins and programmed function { { 0, false, false, "", false }, //{ 1, true, false, "", false }, // Reserved for TX Serial output { 2, false, false, "", false }, //{ 3, true, false, "", false }, // Reserved for RX Serial input { 4, false, false, "", false }, { 5, false, false, "", false }, //{ 6, true, false, "", false }, // Reserved for FLASH SCK //{ 7, true, false, "", false }, // Reserved for FLASH D0 //{ 8, true, false, "", false }, // Reserved for FLASH D1 //{ 9, true, false, "", false }, // Reserved for FLASH D2 //{ 10, true, false, "", false }, // Reserved for FLASH D3 //{ 11, true, false, "", false }, // Reserved for FLASH CMD { 12, false, false, "", false }, { 13, false, false, "", false }, { 14, false, false, "", false }, { 15, false, false, "", false }, { 16, false, false, "", false }, { 17, false, false, "", false }, { 18, false, false, "", false }, // Default for SPI CLK { 19, false, false, "", false }, // Default for SPI MISO //{ 20, true, false, "", false }, // Not exposed on DEV board { 21, false, false, "", false }, // Also Wire SDA { 22, false, false, "", false }, // Also Wire SCL { 23, false, false, "", false }, // Default for SPI MOSI //{ 24, true, false, "", false }, // Not exposed on DEV board { 25, false, false, "", false }, { 26, false, false, "", false }, { 27, false, false, "", false }, //{ 28, true, false, "", false }, // Not exposed on DEV board //{ 29, true, false, "", false }, // Not exposed on DEV board //{ 30, true, false, "", false }, // Not exposed on DEV board //{ 31, true, false, "", false }, // Not exposed on DEV board { 32, false, false, "", false }, { 33, false, false, "", false }, { 34, false, false, "", false }, // Note, no internal pull-up { 35, false, false, "", false }, // Note, no internal pull-up { -1, false, false, "", false } // End of list } ;

struct touchpin_struct // For programmable input pins { int8_t gpio ; // Pin number GPIO bool reserved ; // Reserved for connected devices bool avail ; // Pin is available for a command String command ; // Command to execute when activated // Example: "uppreset=1" bool cur ; // Current state, true = HIGH, false = LOW int16_t count ; // Counter number of times low level } ; touchpin_struct touchpin[] = // Touch pins and programmed function { { 4, false, false, "", false, 0 }, // TOUCH0 //{ 0, true, false, "", false, 0 }, // TOUCH1, reserved for BOOT button { 2, false, false, "", false, 0 }, // TOUCH2 { 15, false, false, "", false, 0 }, // TOUCH3 { 13, false, false, "", false, 0 }, // TOUCH4 { 12, false, false, "", false, 0 }, // TOUCH5 { 14, false, false, "", false, 0 }, // TOUCH6 { 27, false, false, "", false, 0 }, // TOUCH7 { 33, false, false, "", false, 0 }, // TOUCH8 { 32, false, false, "", false, 0 }, // TOUCH9 { -1, false, false, "", false, 0 } // End of table } ;

//** // Pages, CSS and data for the webinterface. * //**

include "about_html.h"

include "config_html.h"

include "index_html.h"

include "mp3play_html.h"

include "radio_css.h"

include "favicon_ico.h"

include "defaultprefs.h"

include "patches.h"

//** // End of global data section. * //** // //** // 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 shutdown_pin ; // Pin where the shutdown 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_STATUS = 0x01; 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 ;

define BASE 0x1810 // Indirizzo analizer plugin

// 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 ) ) { NOP() ; // Very short delay } }

inline void control_mode_on() const
{
  SPI.beginTransaction ( VS1053_SPI ) ;       // Prevent other SPI users
  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 ;
inline bool 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, uint8_t _shutdown_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. inline bool playChunk ( uint8_t data, // Play a chunk of data. Copies the data to size_t len ) ; // the chip. Blocks until complete. // Returns true if more data can be added to fifo 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 inline uint8_t getVolume() const // Get the current volume setting. { // higher is louder. return curvol ; } 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 ) ; } void LoadUserCode( const unsigned short* plugin, uint16_t sizea); inline void getBands(); } ;

//** // VS1053 class implementation. * //**

VS1053::VS1053 ( uint8_t _cs_pin, uint8_t _dcs_pin, uint8_t _dreq_pin, uint8_t _shutdown_pin ) : cs_pin(_cs_pin), dcs_pin(_dcs_pin), dreq_pin(_dreq_pin), shutdown_pin(_shutdown_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() ; }

bool 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? { chunk_length = len ; if ( len > vs1053_chunk_size ) { chunk_length = vs1053_chunk_size ; } len -= chunk_length ; await_data_request() ; // Wait for space available SPI.writeBytes ( data, chunk_length ) ; data += chunk_length ; } data_mode_off() ; return data_request() ; // True if more data can de stored in fifo }

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

dbgprint ( header ) ; // Show a header 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 } 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 ) ; } } 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 ) ; if ( shutdown_pin >= 0 ) // Shutdown in use? { pinMode ( shutdown_pin, OUTPUT ) ; digitalWrite ( shutdown_pin, HIGH ) ; // Shut down audio output } delay ( 100 ) ; // Init SPI in slow mode ( 0.2 MHz ) VS1053_SPI = SPISettings ( 200000, MSBFIRST, SPI_MODE0 ) ; SPI.setDataMode ( SPI_MODE0 ) ; SPI.setBitOrder ( MSBFIRST ) ; //printDetails ( "Right after reset/startup" ) ; delay ( 20 ) ; //printDetails ( "20 msec after reset" ) ; if ( 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 ( 5000000, 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 ) ; LoadUserCode(patch, PATCH_SIZE); LoadUserCode(analizer, ANALIZER_SIZE); } }

void VS1053::LoadUserCode( const unsigned short iplugin, uint16_t sizea) { int i = 0; int ssize = sizea; sizea = sizeof(iplugin[0]); while (i<ssize) { unsigned short addr, n, val; addr = iplugin[i++]; n = iplugin[i++]; if (n & 0x8000U) { / RLE run, replicate n samples / n &= 0x7FFF; val = iplugin[i++]; while (n--) { write_register((uint8_t)addr, (uint16_t)val); } } else { / Copy run, copy n samples / while (n--) { val = iplugin[i++]; write_register((uint8_t)addr, (uint16_t)val); } } } }

void VS1053::getBands () { write_register(SCI_WRAMADDR, BASE+2); bands = read_register(SCI_WRAM); write_register(SCI_WRAMADDR, BASE+4); for (uint8_t i = 0; i < 14; i++) { uint8_t val = read_register(SCI_WRAM); uint8_t cur1 = val & 31; //int pek1 = (val >> 6) & 31; uint8_t cur = (cur1curvol)/100; if ( cur < 0 ) cur = 0; else if ( cur > 31 ) cur = 31; //int pek = (pek1curvol)/100; spectrum[i][0] = cur; //spectrum[i][3] = pek; } }

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 }

void VS1053::startSong() { sdi_send_fillers ( 10 ) ; if ( shutdown_pin >= 0 ) // Shutdown in use? { digitalWrite ( shutdown_pin, LOW ) ; // Enable audio output }

}

bool VS1053::playChunk ( uint8_t* data, size_t len ) { return sdi_send_buffer ( data, len ) ; // True if more data can be added to fifo }

void VS1053::stopSong() { uint16_t modereg ; // Read from mode register int i ; // Loop control

sdi_send_fillers ( 2052 ) ; if ( shutdown_pin >= 0 ) // Shutdown in use? { digitalWrite ( shutdown_pin, HIGH ) ; // Disable audio output } 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 ;

//** // End VS1053 stuff. * //**

//** // N V S O P E N * //** // Open Preferences with my-app namespace. Each application module, library, etc. // has to use namespace name to prevent key name collisions. We will open storage in // RW-mode (second parameter has to be false). * //** void nvsopen() { if ( ! nvshandle ) // Opened already? { nvserr = nvs_open ( NAME, NVS_READWRITE, &nvshandle ) ; // No, open nvs if ( nvserr ) { dbgprint ( "nvs_open failed!" ) ; } } }

//** // N V S C L E A R * //** // Clear all preferences. * //** esp_err_t nvsclear() { nvsopen() ; // Be sure to open nvs return nvs_erase_all ( nvshandle ) ; // Clear all keys }

//** // N V S G E T S T R * //** // Read a string from nvs. * //** String nvsgetstr ( const char* key ) { static char nvs_buf[NVSBUFSIZE] ; // Buffer for contents size_t len = NVSBUFSIZE ; // Max length of the string, later real length

nvsopen() ; // Be sure to open nvs nvs_buf[0] = '\0' ; // Return empty string on error nvserr = nvs_get_str ( nvshandle, key, nvs_buf, &len ) ; if ( nvserr ) { dbgprint ( "nvs_get_str failed %X for key %s, keylen is %d, len is %d!", nvserr, key, strlen ( key), len ) ; dbgprint ( "Contents: %s", nvs_buf ) ; } return String ( nvs_buf ) ; }

//** // N V S S E T S T R * //** // Put a key/value pair in nvs. Length is limited to allow easy read-back. // No writing if no change. //** esp_err_t nvssetstr ( const char* key, String val ) { String curcont ; // Current contents bool wflag = true ; // Assume update or new key

//dbgprint ( "Setstring for %s: %s", key, val.c_str() ) ; if ( val.length() >= NVSBUFSIZE ) // Limit length of string to store { dbgprint ( "nvssetstr length failed!" ) ; return ESP_ERR_NVS_NOT_ENOUGH_SPACE ; } if ( nvssearch ( key ) ) // Already in nvs? { curcont = nvsgetstr ( key ) ; // Read current value wflag = ( curcont != val ) ; // Value change? } if ( wflag ) // Update or new? { //dbgprint ( "nvssetstr update value" ) ; nvserr = nvs_set_str ( nvshandle, key, val.c_str() ) ; // Store key and value if ( nvserr ) // Check error { dbgprint ( "nvssetstr failed!" ) ; } } return nvserr ; }

//** // N V S C H K E Y * //** // Change a keyname in in nvs. * //** void nvschkey ( const char oldk, const char newk ) { String curcont ; // Current contents

if ( nvssearch ( oldk ) ) // Old key in nvs? { curcont = nvsgetstr ( oldk ) ; // Read current value nvs_erase_key ( nvshandle, oldk ) ; // Remove key nvssetstr ( newk, curcont ) ; // Insert new } }

//** // C L A I M S P I * //** // Claim the SPI bus. Uses FreeRTOS semaphores. * //** void claimSPI ( const char* p ) { const TickType_t ctry = 10 ; // Time to wait for semaphore uint32_t count = 0 ; // Wait time in ticks

while ( xSemaphoreTake ( SPIsem, ctry ) != pdTRUE ) ; // Claim SPI bus { if ( count++ > 10 ) { dbgprint ( "SPI semaphore not taken within %d ticks by CPU %d, id %s", count * ctry, xPortGetCoreID(), p ) ; } } }

//** // R E L E A S E S P I * //** // Free the the SPI bus. Uses FreeRTOS semaphores. * //** void releaseSPI() { xSemaphoreGive ( SPIsem ) ; // Release SPI bus }

//** // Q U E U E F U N C * //** // Queue a special function for the play task. * //** void queuefunc ( int func ) { qdata_struct specchunk ; // Special function to queue

specchunk.datatyp = func ; // Put function in datatyp xQueueSend ( dataqueue, &specchunk, 200 ) ; // Send to queue }

//** // N V S S E A R C H * //** // Check if key exists in nvs. * //** bool nvssearch ( const char* key ) { size_t len = NVSBUFSIZE ; // Length of the string

nvsopen() ; // Be sure to open nvs nvserr = nvs_get_str ( nvshandle, key, NULL, &len ) ; // Get length of contents return ( nvserr == ESP_OK ) ; // Return true if found }

//** // T F T S E T * //** // Request to display a segment on TFT. Version for char and String parameter. //** void tftset ( uint16_t inx, const char *str, int color = -1) { if ( str ) // String specified? { tftdata[inx].str = String ( str ) ; // Yes, set string } if (color != -1) { tftdata[inx].color = color ; } tftdata[inx].update_req = true ; // and request flag tft_update_req = true ; // and global flag }

void tftset ( uint16_t inx, String& str, int color = -1) { tftdata[inx].str = str ; // Set string if (color != -1) { tftdata[inx].color = color ; } tftdata[inx].update_req = true ; // and request flag tft_update_req = true ; // and global flag }

//** // U T F 8 A S C I I * //** // UTF8-Decoder: convert UTF8-string to extended ASCII. // Convert a single Character from UTF8 to Extended ASCII. // Return "0" if a byte has to be ignored. * //** byte utf8ascii ( byte ascii ) { static const byte lut_C3[] = { "AAAAAAACEEEEIIIIDNOOOOO#0UUUU###aaaaaaaceeeeiiiidnooooo##uuuuyyy" } ; static byte c1 ; // Last character buffer byte res = 0 ; // Result, default 0

if ( ascii <= 0x7F ) // Standard ASCII-set 0..0x7F handling { c1 = 0 ; res = ascii ; // Return unmodified } else { switch ( c1 ) // Conversion depending on first UTF8-character { case 0xC2: res = '~' ; break ; case 0xC3: res = lut_C3[ascii - 128] ; break ; case 0x82: if ( ascii == 0xAC ) { res = 'E' ; // Special case Euro-symbol } } c1 = ascii ; // Remember actual character } return res ; // Otherwise: return zero, if character has to be ignored }

//** // U T F 8 A S C I I * //** // In Place conversion UTF8-string to Extended ASCII (ASCII is shorter!). * //** void utf8ascii ( char* s ) { int i, k = 0 ; // Indexes for in en out string char c ;

for ( i = 0 ; s[i] ; i++ ) // For every input character { c = utf8ascii ( s[i] ) ; // Translate if necessary if ( c ) // Good translation? { s[k++] = c ; // Yes, put in output string } } s[k] = 0 ; // Take care of delimeter }

//** // U T F 8 A S C I I * //** // Conversion UTF8-String to Extended ASCII String. * //** String utf8ascii ( const char* s ) { int i ; // Index for input string char c ; String res = "" ; // Result string

for ( i = 0 ; s[i] ; i++ ) // For every input character { c = utf8ascii ( s[i] ) ; // Translate if necessary if ( c ) // Good translation? { res += String ( c ) ; // Yes, put in output string } } return res ; }

//** // D B G P R I N T * //** // Send a line of info to serial output. Works like vsprintf(), but checks the DEBUG flag. // Print only if DEBUG flag is true. Always returns the the formatted string. //** char dbgprint ( const char format, ... ) { static char sbuf[DEBUG_BUFFER_SIZE] ; // For debug lines va_list varArgs ; // For variable number of params

va_start ( varArgs, format ) ; // Prepare parameters vsnprintf ( sbuf, sizeof(sbuf), format, varArgs ) ; // Format the message va_end ( varArgs ) ; // End of using parameters if ( DEBUG ) // DEBUG on? { Serial.print ( "D: " ) ; // Yes, print prefix Serial.println ( sbuf ) ; // and the info } return sbuf ; // Return stored string }

//** // S E L E C T N E X T S D N O D E * //** // Select the next or previous mp3 file from SD. If the last selected song was random, the next // track is a random one too. Otherwise the next/previous node is choosen. // If nodeID is "0" choose a random nodeID. // Delta is +1 or -1 for next or previous track. // The nodeID will be returned to the caller. * //** String selectnextSDnode ( String curnod, int16_t delta ) { int16_t inx, inx2 ; // Position in nodelist

if ( hostreq ) // Host request already set? { return "" ; // Yes, no action } if ( SD_currentnode == "0" ) // Random playing? { return SD_currentnode ; // Yes, return random nodeID } else { inx = SD_nodelist.indexOf ( curnod ) ; // Get position of current nodeID in list if ( delta > 0 ) // Next track? { inx += curnod.length() + 1 ; // Get position of next nodeID in list if ( inx >= SD_nodelist.length() ) // End of list? { inx = 0 ; // Yes, wrap around } } else { if ( inx == 0 ) // At the begin of the list? { inx = SD_nodelist.length() ; // Yes, goto end of list } inx-- ; // Index of delimeter of previous node ID while ( ( inx > 0 ) && ( SD_nodelist[inx - 1] != '\n' ) ) { inx-- ; } } inx2 = SD_nodelist.indexOf ( "\n", inx ) ; // Find end of node ID } return SD_nodelist.substring ( inx, inx2 ) ; // Return nodeID }

//** // G E T S D F I L E N A M E * //** // Translate the nodeID of a track to the full filename that can be used as a station. // If nodeID is "0" choose a random nodeID. //** String getSDfilename ( String nodeID ) { String res ; // Function result File root, file ; // Handle to root and directory entry uint16_t n, i ; // Current sequence number and counter in directory int16_t inx ; // Position in nodeID const char* p = "/" ; // Points to directory/file uint16_t rndnum ; // Random index in SD_nodelist int nodeinx = 0 ; // Points to node ID in SD_nodecount int nodeinx2 ; // Points to end of node ID in SD_nodecount

SD_currentnode = nodeID ; // Save current node if ( nodeID == "0" ) // Empty parameter? { rndnum = random ( SD_nodecount ) ; // Yes, choose a random node for ( i = 0 ; i < rndnum ; i++ ) // Find the node ID { // Search to begin of the random node by skipping lines nodeinx = SD_nodelist.indexOf ( "\n", nodeinx ) + 1 ; } nodeinx2 = SD_nodelist.indexOf ( "\n", nodeinx ) ; // Find end of node ID nodeID = SD_nodelist.substring ( nodeinx, nodeinx2 ) ; // Get node ID } dbgprint ( "getSDfilename requested node ID is %s", // Show requeste node ID nodeID.c_str() ) ; while ( ( n = nodeID.toInt() ) ) // Next sequence in current level { inx = nodeID.indexOf ( "," ) ; // Find position of comma if ( inx >= 0 ) { nodeID = nodeID.substring ( inx + 1 ) ; // Remove sequence in this level from nodeID } claimSPI ( "sdopen" ) ; // Claim SPI bus root = SD.open ( p ) ; // Open the directory (this level) releaseSPI() ; // Release SPI bus for ( i = 1 ; i <= n ; i++ ) { claimSPI ( "sdopenxt" ) ; // Claim SPI bus file = root.openNextFile() ; // Get next directory entry releaseSPI() ; // Release SPI bus delay ( 10 ) ; // Allow playtask //dbgprint ( "file nr %d/%d is %s", i, n, file.name() ) ; } p = file.name() ; // Points to directory- or file name } res = String ( "localhost" ) + String ( p ) ; // Format result return res ; // Return full station spec }

//** // L I S T S D T R A C K S * //** // Search all MP3 files on directory of SD card. Return the number of files found. // A "node" of max. 4 levels ( the subdirectory level) will be generated for every file. // The numbers within the node-array is the sequence number of the file/directory in that // subdirectory. // A node ID is a string like "2,1,4,0", which means the 4th file in the first directory // of the second directory. // The list will be send to the webinterface if parameter "send"is true. * //** int listsdtracks ( const char * dirname, int level = 0, bool send = true ) { const uint16_t SD_MAXDEPTH = 4 ; // Maximum depts. Note: see mp3play_html. static uint16_t fcount, oldfcount ; // Total number of files static uint16_t SD_node[SD_MAXDEPTH + 1] ; // Node ISs, max levels deep static String SD_outbuf ; // Output buffer for cmdclient uint16_t ldirname ; // Length of dirname to remove from filename File root, file ; // Handle to root and directory entry String filename ; // Copy of filename for lowercase test uint16_t i ; // Loop control to compute single node id String tmpstr ; // Tijdelijke opslag node ID

if ( strcmp ( dirname, "/" ) == 0 ) // Are we at the root directory? { fcount = 0 ; // Yes, reset count memset ( SD_node, 0, sizeof(SD_node) ) ; // And sequence counters SD_outbuf = String() ; // And output buffer SD_nodelist = String() ; // And nodelist if ( !SD_okay ) // See if known card { if ( send ) { cmdclient.println ( "0/No tracks found" ) ; // No SD card, emppty list } return 0 ; } } oldfcount = fcount ; // To see if files found in this directory //dbgprint ( "SD directory is %s", dirname ) ; // Show current directory ldirname = strlen ( dirname ) ; // Length of dirname to remove from filename claimSPI ( "sdopen2" ) ; // Claim SPI bus root = SD.open ( dirname ) ; // Open the current directory level releaseSPI() ; // Release SPI bus if ( !root || !root.isDirectory() ) // Success? { dbgprint ( "%s is not a directory or not root", // No, print debug message dirname ) ; return fcount ; // and return } while ( true ) // Find all mp3 files { claimSPI ( "opennextf" ) ; // Claim SPI bus file = root.openNextFile() ; // Try to open next releaseSPI() ; // Release SPI bus if ( !file ) { break ; // End of list } SD_node[level]++ ; // Set entry sequence of current level filename = String ( file.name() ) ; // Copy filename filename.toLowerCase() ; // Force lowercase if ( !filename.startsWith ( "." ) && !filename.startsWith ( "/." )) // Skip hidden directories { if ( file.isDirectory() ) // Is it a directory? { if ( level < SD_MAXDEPTH ) // Yes, dig deeper { listsdtracks ( file.name(), level + 1, send ) ; // Note: called recursively SD_node[level + 1] = 0 ; // Forget counter for one level up } } else { if ( filename.endsWith ( ".mp3" ) ) // It is a file, but is it an MP3? { fcount++ ; // Yes, count total number of MP3 files tmpstr = String() ; // Empty for ( i = 0 ; i < SD_MAXDEPTH ; i++ ) // Add a line containing the node to SD_outbuf { if ( i ) // Need to add separating comma? { tmpstr += String ( "," ) ; // Yes, add comma } tmpstr += String ( SD_node[i] ) ; // Add sequence number } if ( send ) // Need to add to string for webinterface? { SD_outbuf += tmpstr + "/" + // Form line for mp3play_html page utf8ascii ( file.name() + // Filename starts after directoryname ldirname ) + String ( "\n" ) ; } SD_nodelist += tmpstr + String ( "\n" ) ; // Add to nodelist //dbgprint ( "Track: %s", // Show debug info // file.name() + ldirname ) ; if ( SD_outbuf.length() > 1000 ) // Buffer full? { cmdclient.print ( SD_outbuf ) ; // Yes, send it SD_outbuf = String() ; // Clear buffer } } } if ( send ) { mp3loop() ; // Keep playing } } } if ( fcount != oldfcount ) // Files in this directory? { SD_outbuf += String ( "-1/ \n" ) ; // Spacing in list } if ( SD_outbuf.length() ) // Flush buffer if not empty { cmdclient.print ( SD_outbuf ) ; // Filled, send it SD_outbuf = String() ; // Continue with empty buffer } return fcount ; // Return number of MP3s (sofar) }

//** // G E T E N C R Y P T I O N T Y P E * //** // Read the encryption type of the network and return as a 4 byte name * //** const char* getEncryptionType ( wifi_auth_mode_t thisType ) { switch ( thisType ) { case WIFI_AUTH_OPEN: return "OPEN" ; case WIFI_AUTH_WEP: return "WEP" ; case WIFI_AUTH_WPA_PSK: return "WPA_PSK" ; case WIFI_AUTH_WPA2_PSK: return "WPA2_PSK" ; case WIFI_AUTH_WPA_WPA2_PSK: return "WPA_WPA2_PSK" ; case WIFI_AUTH_MAX: return "MAX" ; default: break ; } return "????" ; }

//** // L I S T N E T W O R K S * //** // List the available networks. // Acceptable networks are those who have an entry in the preferences. // SSIDs of available networks will be saved for use in webinterface. * //** void listNetworks() { WifiInfo_t winfo ; // Entry from wifilist wifi_auth_mode_t encryption ; // TKIP(WPA), WEP, etc. const char* acceptable ; // Netwerk is acceptable for connection int i, j ; // Loop control

dbgprint ( "Scan Networks" ) ; // Scan for nearby networks int numSsid = WiFi.scanNetworks() ; dbgprint ( "Scan completed" ) ; if ( numSsid <= 0 ) { 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 for ( i = 0 ; i < numSsid ; i++ ) { acceptable = "" ; // Assume not acceptable for ( j = 0 ; j < wifilist.size() ; j++ ) // Search in wifilist { winfo = wifilist[j] ; // Get one entry if ( WiFi.SSID(i).indexOf ( winfo.ssid ) == 0 ) // Is this SSID acceptable? { acceptable = "Acceptable" ; break ; } } 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 ( "End of list" ) ; }

//** // 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 calling timely procedures within this routine or in called functions will // cause a crash! //** void timer10sec() { static uint32_t oldtotalcount = 7321 ; // Needed for change detection static uint8_t morethanonce = 0 ; // Counter for succesive fails uint32_t bytesplayed ; // Bytes send to MP3 converter

if ( datamode & ( INIT | HEADER | DATA | // Test op playing METADATA | PLAYLISTINIT | PLAYLISTHEADER | PLAYLISTDATA ) ) { bytesplayed = totalcount - oldtotalcount ; // Nunber of bytes played in the 10 seconds oldtotalcount = totalcount ; // Save for comparison in next cycle if ( bytesplayed == 0 ) // Still playing? { if ( morethanonce > 10 ) // No! Happened too many times? { 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 } morethanonce++ ; // Count the fails } else { // // Data has been send to MP3 decoder // Bitrate in kbits/s is bytesplayed / 10 / 1000 * 8 mbitrate = ( bytesplayed + 625 ) / 1250 ; // Measured bitrate morethanonce = 0 ; // Data seen, reset failcounter } } }

//** // T I M E R 1 0 0 * //** // Called every 100 msec on interrupt level, so must be in IRAM and no lengthy operations // allowed. //** void IRAM_ATTR timer100() { static int16_t count10sec = 0 ; // Counter for activatie 10 seconds process

if ( ++count10sec == 100 ) // 10 seconds passed? { //timer10sec() ; // Yes, do 10 second procedure count10sec = 0 ; // Reset count } if ( ( count10sec % 10 ) == 0 ) // One second over? {

if ( ++timeinfo.tm_sec >= 60 )                // Yes, update number of seconds
{
  timeinfo.tm_sec = 0 ;                       // Wrap after 60 seconds
  if ( ++timeinfo.tm_min >= 60 )
  {
    timeinfo.tm_min = 0 ;                     // Wrap after 60 minutes
    if ( ++timeinfo.tm_hour >= 24 )
    {
      timeinfo.tm_hour = 0 ;                  // Wrap after 24 hours
    }
  }
}
time_req = true ;                             // Yes, show current time request

} // Handle rotary encoder. Inactivity counter will be reset by encoder interrupt if ( ++enc_inactivity == 36000 ) // Count inactivity time { enc_inactivity = 1000 ; // Prevent wrap } // Read ADC and do some filtering adcval = ( 15 * adcval + adc1_get_raw ( ADC1_CHANNEL_0 ) ) / 16 ; }

//** // I S R _ I R * //** // Interrupts received from VS1838B on every change of the signal. // Intervals are 640 or 1640 microseconds for data. syncpulses are 3400 micros or longer. // Input is complete after 65 level changes. // Only the last 32 level changes are significant and will be handed over to common data. //** void IRAM_ATTR isr_IR() { static uint32_t t0 = 0 ; // To get the interval uint32_t t1, intval ; // Current time and interval since last change static uint32_t ir_locvalue = 0 ; // IR code static int ir_loccount ; // Length of code uint32_t mask_in = 2 ; // Mask input for conversion uint16_t mask_out = 1 ; // Mask output for conversion

t1 = micros() ; // Get current time intval = t1 - t0 ; // Compute interval t0 = t1 ; // Save for next compare if ( ( intval > 300 ) && ( intval < 800 ) ) // Short pulse? { ir_locvalue = ir_locvalue << 1 ; // Shift in a "zero" bit ir_loccount++ ; // Count number of received bits } else if ( ( intval > 1500 ) && ( intval < 1800 ) ) // Long pulse? { ir_locvalue = ( ir_locvalue << 1 ) + 1 ; // Shift in a "one" bit ir_loccount++ ; // Count number of received bits } else if ( ir_loccount == 65 ) // Value is correct after 65 level changes { while ( mask_in ) // Convert 32 bits to 16 bits { if ( ir_locvalue & mask_in ) // Bit set in pattern? { ir_value |= mask_out ; // Set set bit in result } mask_in <<= 2 ; // Shift input mask 2 positions mask_out <<= 1 ; // Shift output mask 1 position } ir_loccount = 0 ; // Ready for next input } else { ir_locvalue = 0 ; // Reset decoding ir_loccount = 0 ; } }

//** // I S R E N C S W I T C H * //** // Interrupts received from rotary encoder switch. * //** void IRAM_ATTR isr_enc_switch() { static uint32_t debouns = 0; if (millis() >= debouns + 250) { debouns = millis() ; singleclick = true ; } }

//** // I S R E N C T U R N * //**

void IRAM_ATTR isr_enc_turn() { static uint32_t debount = 0; static uint8_t old_state = 0x0001 ; // Previous state uint8_t act_state ; // The current state of the 2 PINs uint8_t inx ; // Index in enc_state static const int8_t enc_states [] = { 0, // 00 -> 00 -1, // 00 -> 01 1, // 00 -> 10 0, // 00 -> 11 1, // 01 -> 00 0, // 01 -> 01 0, // 01 -> 10 -1, // 01 -> 11 -1, // 10 -> 00 0, // 10 -> 01 0, // 10 -> 10 1, // 10 -> 11 0, // 11 -> 00 1, // 11 -> 01 -1, // 11 -> 10 0 // 11 -> 11 } ; // Read current state of CLK, DT pin. Result is a 2 bit binairy number: 00, 01, 10 or 11. act_state = ( digitalRead ( ini_block.enc_clk_pin ) << 1 ) + digitalRead ( ini_block.enc_dt_pin ) ; if (millis() >= debount + 100) { debount = millis(); inx = ( old_state << 2 ) + act_state ; // Form index in enc_states rotationcount = enc_states[inx] ; // Get delta: 0, +1 or -1 } old_state = act_state ; // Remember current status enc_inactivity = 0 ; }

//** // 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 ceckstreamtitle ( const char ml ) { String streamtitle1 = ""; uint16_t lengta = 108; if (streamtitle1.indexOf("StreamTitle=") ) { lengta += 12; } streamtitle1 = String(ml); streamtitle1.replace(""", "\""); streamtitle1.replace("&", "&"); streamtitle1.replace("'", "\'"); streamtitle1.replace("   ", " "); streamtitle1.replace("  ", " "); streamtitle1.replace(" ", " "); streamtitle1.replace("˜˜˜", " "); streamtitle1.replace("˜˜", " "); streamtitle1.replace("˜", " "); streamtitle1.replace("~~~", " "); streamtitle1.replace("~~", " "); streamtitle1.replace("~", " "); streamtitle1.trim(); if (streamtitle1.length() > lengta) { streamtitle1.substring(0, lengta - 1); } streamtitle1.toCharArray((char)ml, streamtitle1.length() + 1); } void showstreamtitle ( const char ml, bool full ) { char p1 ; char* p2 ; char streamtitle[121] ; // Streamtitle from metadata

ceckstreamtitle((char*)ml); 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
{
    p1++ ;
    *p2 = '\0' ;                              // Strip the rest of the line
} else if ( ( p2 = strstr ( ml, ";" ) ) )          // Search for end of title
{
   *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 if (String(streamtitle) != icystreamtitle) { icystreamtitle = streamtitle ; tftset ( 3, streamtitle ) ; // Set screen segment text middle part } }

//** // S T O P _ M P 3 C L I E N T * //** // Disconnect from the server. * //** void stop_mp3client () { mp3client.stop() ; // Stop stream client while ( mp3client.connected() ) while ( mp3client.available() ) // Flush stream client { mp3client.read(); } }

//** // 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 uint16_t port = 80 ; // Port number for host String extension = "/" ; // May be like "/mp3" in "skonto.ls.lv:8002/mp3" String hostwoext = host ; // Host without extension and portnumber

claimSPI ( "connecttohost" ) ; stop_mp3client() ; // Disconnect if still connected dbgprint ( "Connect to new host %s", host.c_str() ) ; if (!silentreconon) { tftset ( 0, " WEB Radio" ) ; // Set screen segment text top line } //displaytime ( "" ) ; // Clear time on TFT screen 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, like noisefm.ru:8000/play.m3u&t=.m3u 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 host there may be a portnumber inx = hostwoext.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 } dbgprint ( "Connect to %s on port %d, extension %s", hostwoext.c_str(), port, extension.c_str() ) ; 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 ( "User-Agent: EspRadio\r\n" ) + String ( "Host: " ) + hostwoext + String ( "\r\n" ) + String ( "Icy-MetaData:1\r\n" ) + String ( "Connection: close\r\n\r\n" ) ) ; releaseSPI() ; return true ; } dbgprint ( "Request %s failed!", host.c_str() ) ; releaseSPI() ; return false ; }

//** // C O N N E C T T O F I L E * //** // Open the local mp3-file. // Not yet implemented. //** bool connecttofile() { String path ; // Full file spec char p ; // Pointer to filename //displaytime ( "" ) ; // Clear time on TFT screen path = host.substring ( 9 ) ; // Path, skip the "localhost" part //dbgprint ( "Open SD %s", path.c_str() ) ; claimSPI ( "sdopen3" ) ; // Claim SPI bus mp3file = SD.open ( path ) ; // Open the file timeLineFileLength = mp3file.available() ; // Get length searchId3(); mp3file.seek(lastseek); releaseSPI() ; timeLineFilePlayed = lastseek; lastseek = 0; if ( !mp3file ) { dbgprint ( "Error opening file %s", path.c_str() ) ; // No luck return false ; } //dbgprint ( "File is open now" ) ; p = (char)path.c_str() + 1 ; // Point to filename utf8ascii(p); tftset ( 0, " SD MP3 Player" ) ; // Set screen segment top line tftset ( 5, p ) ; // Set screen segment bottom part //showstreamtitle ( p, true ) ; // Show the filename as title (middle part) //tftset ( 2, "Playing from local file" ) ; // Set screen segment bottom part icyname = "" ; // No icy name yet chunked = false ; // File not chunked metaint = 0 ; // No metadata timeLineFileFinisced = false; startsong = true; return true ; }

//** // C O N N E C T W I F I * //** // Connect to WiFi using the SSID's available in wifiMulti. // If only one AP if found in preferences (i.e. wifi_00) the connection is made without // using wifiMulti. // If connection fails, an AP is created and the function returns false. //** bool connectwifi() { char pfs ; // Pointer to formatted string char pfs2 ; // Pointer to formatted string bool localAP = false ; // True if only local AP is left

WifiInfo_t winfo ; // Entry from wifilist

WiFi.disconnect() ; // After restart the router could WiFi.softAPdisconnect(true) ; // still keep the old connection if ( wifilist.size() ) // Any AP defined? { if ( wifilist.size() == 1 ) // Just one AP defined in preferences? { winfo = wifilist[0] ; // Get this entry WiFi.begin ( winfo.ssid, winfo.passphrase ) ; // Connect to single SSID found in wifi_xx dbgprint ( "Try WiFi %s", winfo.ssid ) ; // Message to show during WiFi connect } else // More AP to try { wifiMulti.run() ; // Connect to best network } if ( WiFi.waitForConnectResult() != WL_CONNECTED ) // Try to connect { localAP = true ; // Error, setup own AP } } else { localAP = true ; // Not even a single AP defined } if ( localAP ) // Must setup local AP? { 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 pfs = dbgprint ( "IP = 192.168.4.1" ) ; // Address for AP } else { ipaddress = WiFi.localIP().toString() ; // Form IP address pfs2 = dbgprint ( "Connected to %s", WiFi.SSID().c_str() ) ; tftlog ( pfs2 ) ; pfs = dbgprint ( "IP = %s", ipaddress.c_str() ) ; // String to dispay on TFT } tftlog ( pfs ) ; // Show IP delay ( 3000 ) ; // Allow user to read this return ( localAP == false ) ; // Return result of connection }

//** // O T A S T A R T * //** // Update via WiFi has been started by Arduino IDE. * //** void otastart() { char* p ;

p = dbgprint ( "OTA update Started" ) ; tftset ( 2, p ) ; // Set screen segment bottom part }

//** // R E A D H O S T F R O M P R E F * //** // Read the mp3 host from the preferences specified by the parameter. // The host will be returned. //** String readhostfrompref ( int8_t preset ) { char tkey[12] ; // Key as an array of chars

sprintf ( tkey, "preset_%02d", preset ) ; // Form the search key if ( nvssearch ( tkey ) ) // Does it exists? { // Get the contents return nvsgetstr ( tkey ) ; // Get the station (or empty sring) } else { return String ( "" ) ; // Not found } }

//** // R E A D H O S T F R O M P R E F * //** // Search for the next mp3 host in preferences specified newpreset. // The host will be returned. newpreset will be updated //** String readhostfrompref() { String contents = "" ; // Result of search int maxtry = 0 ; // Limit number of tries

while ( ( contents = readhostfrompref ( ini_block.newpreset ) ) == "" ) { if ( ++ maxtry > 99 ) { return "" ; } if ( ++ini_block.newpreset > 99 ) // Next or wrap to 0 { ini_block.newpreset = 0 ; } } // Get the contents return contents ; // Return the station }

//** // R E A D P R O G B U T T O N S * //** // Read the preferences for the programmable input pins and the touch pins. * //** void readprogbuttons() { char mykey[20] ; // For numerated key int8_t pinnr ; // GPIO pinnumber to fill int i ; // Loop control String val ; // Contents of preference entry

for ( i = 0 ; ( pinnr = progpin[i].gpio ) >= 0 ; i++ ) // Scan for all programmable pins { sprintf ( mykey, "gpio%02d", pinnr ) ; // Form key in preferences if ( nvssearch ( mykey ) ) { val = nvsgetstr ( mykey ) ; // Get the contents if ( val.length() ) // Does it exists? { if ( !progpin[i].reserved ) // Do not use reserved pins { progpin[i].avail = true ; // This one is active now progpin[i].command = val ; // Set command dbgprint ( "gpio%02d will execute %s", // Show result pinnr, val.cstr() ) ; } } } } // Now for the touch pins 0..9, identified by their GPIO pin number for ( i = 0 ; ( pinnr = touchpin[i].gpio ) >= 0 ; i++ ) // Scan for all programmable pins { sprintf ( mykey, "touch%02d", pinnr ) ; // Form key in preferences if ( nvssearch ( mykey ) ) { val = nvsgetstr ( mykey ) ; // Get the contents if ( val.length() ) // Does it exists? { if ( !touchpin[i].reserved ) // Do not use reserved pins { touchpin[i].avail = true ; // This one is active now touchpin[i].command = val ; // Set command //pinMode ( touchpin[i].gpio, INPUT ) ; // Free floating input dbgprint ( "touch_%02d will execute %s", // Show result pinnr, val.cstr() ) ; } else { dbgprint ( "touch%02d pin (GPIO%02d) is reserved for I/O!", pinnr, pinnr ) ;

    }
  }
}

} }

//** // R E S E R V E P I N * //** // Set I/O pin to "reserved". // The pin is than not available for a programmable function. //** void reservepin ( int8_t rpinnr ) { uint8_t i = 0 ; // Index in progpin/touchpin array int8_t pin ; // Pin number in progpin array

while ( ( pin = progpin[i].gpio ) >= 0 ) // Find entry for requested pin { if ( pin == rpinnr ) // Entry found? { //dbgprint ( "GPIO%02d unavailabe for 'gpio_'-command", pin ) ; progpin[i].reserved = true ; // Yes, pin is reserved now break ; // No need to continue } i++ ; // Next entry } // Also reserve touchpin numbers i = 0 ; while ( ( pin = touchpin[i].gpio ) >= 0 ) // Find entry for requested pin { if ( pin == rpinnr ) // Entry found? { //dbgprint ( "GPIO%02d unavailabe for 'touch'-command", pin ) ; touchpin[i].reserved = true ; // Yes, pin is reserved now break ; // No need to continue } i++ ; // Next entry } }

//** // R E A D I O P R E F S * //** // Scan the preferences for IO-pin definitions. * //** void readIOprefs() { struct iosetting { const char gname ; // Name in preferences int8_t gnr ; // GPIO pin number int8_t pdefault ; // Default pin }; struct iosetting klist[] = { // List of I/O related keys { "pin_ir", &ini_block.ir_pin -1 }, { "pin_enc_clk", &ini_block.enc_clk_pin, -1 }, { "pin_enc_dt", &ini_block.enc_dt_pin, -1 }, { "pin_enc_sw", &ini_block.enc_sw_pin, -1 }, { "pin_tft_cs", &ini_block.tft_cs_pin, -1 }, { "pin_tft_dc", &ini_block.tft_dc_pin, -1 }, { "pin_sd_cs", &ini_block.sd_cs_pin, -1 }, { "pin_vs_cs", &ini_block.vs_cs_pin, VS1053_CS }, { "pin_vs_dcs", &ini_block.vs_dcs_pin, VS1053_DCS }, { "pin_vs_dreq", &ini_block.vs_dreq_pin, VS1053_DREQ }, { "pin_shutdown", &ini_block.vs_shutdown_pin, -1 }, { "pin_spi_sck", &ini_block.spi_sck_pin, 18 }, { "pin_spi_miso", &ini_block.spi_miso_pin, 19 }, { "pin_spi_mosi", &ini_block.spi_mosi_pin, 23 }, { NULL, NULL, 0 } // End of list } ; int i ; // Loop control int count = 0 ; // Number of keys found String val ; // Contents of preference entry int8_t ival ; // Value converted to integer

for ( i = 0 ; klist[i].gname ; i++ ) // Loop trough all I/O related keys { ival = klist[i].pdefault ; // Assume pin number to be the default if ( nvssearch ( klist[i].gname ) ) // Does it exist? { val = nvsgetstr ( klist[i].gname ) ; // Read value of key if ( val.length() ) // Parameter in preference? { count++ ; // Yes, count number of filled keys ival = val.toInt() ; // Convert value to integer pinnumber reservepin ( ival ) ; // Set pin to "reserved" } } *klist[i].gnr = ival ; // Set pinnumber in ini_block dbgprint ( "%s pin set to %d", // Show result klist[i].gname, ival ) ; } }

//** // R E A D P R E F S * //** // Read the preferences and interpret the commands. // If output == true, the key / value pairs are returned to the caller as a String. //** String readprefs ( bool output ) { uint16_t i ; // Loop control String val ; // Contents of preference entry String cmd ; // Command for analyzCmd String outstr = "" ; // Outputstring char* key ; // Point to nvskeys[i] uint8_t winx ; // Index in wifilist uint16_t last2char = 0 ; // To detect paragraphs

i = 0 ; while ( *( key = nvskeys[i] ) ) // Loop trough all available keys { val = nvsgetstr ( key ) ; // Read value of this key cmd = String ( key ) + // Yes, form command String ( " = " ) + val ; if ( strstr ( key, "wifi_" ) ) // Is it a wifi ssid/password? { winx = atoi ( key + 5 ) ; // Get index in wifilist val = String ( wifilist[winx].ssid ) + // Yes, hide password String ( "/***" ) ; cmd = String ( "" ) ; // Do not analyze this } if ( output ) { if ( ( i > 0 ) && ( (uint16_t)key != last2char ) ) // New paragraph? { outstr += String ( "#\n" ) ; // Yes, add separator } last2char = (uint16_t)key ; // Save 2 chars for next compare outstr += String ( key ) + // Add to outstr String ( " = " ) + val + String ( "\n" ) ; // Add newline } else { analyzeCmd ( cmd.c_str() ) ; // Analyze it } i++ ; // Next key } if ( i == 0 ) { outstr = String ( "No preferences found.\n" "Use defaults or run Esp32_radio_init first.\n" ) ; } return outstr ; }

//** // 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 const 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 } } }

//** // S C A N D I G I T A L * //** // Scan digital inputs. * //** void scandigital() { static uint32_t oldmillis = 5000 ; // To compare with current time int i ; // Loop control int8_t pinnr ; // Pin number to check bool level ; // Input level const char* reply ; // Result of analyzeCmd int16_t tlevel ; // Level found by touch pin const int16_t THRESHOLD = 30 ; // Threshold or touch pins

if ( ( millis() - oldmillis ) < 100 ) // Debounce { return ; } oldmillis = millis() ; // 100 msec over for ( i = 0 ; ( pinnr = progpin[i].gpio ) >= 0 ; i++ ) // Scan all inputs { if ( !progpin[i].avail || progpin[i].reserved ) // Skip unused and reserved pins { continue ; } level = ( digitalRead ( pinnr ) == HIGH ) ; // Sample the pin if ( level != progpin[i].cur ) // Change seen? { progpin[i].cur = level ; // And the new level if ( !level ) // HIGH to LOW change? { dbgprint ( "GPIO_%02d is now LOW, execute %s", pinnr, progpin[i].command.c_str() ) ; reply = analyzeCmd ( progpin[i].command.cstr() ) ; // Analyze command and handle it dbgprint ( reply ) ; // Result for debugging } } } // Now for the touch pins for ( i = 0 ; ( pinnr = touchpin[i].gpio ) >= 0 ; i++ ) // Scan all inputs { if ( !touchpin[i].avail || touchpin[i].reserved ) // Skip unused and reserved pins { continue ; } tlevel = ( touchRead ( pinnr ) ) ; // Sample the pin level = ( tlevel >= 30 ) ; // True if below threshold if ( level ) // Level HIGH? { touchpin[i].count = 0 ; // Reset count number of times } else { if ( ++touchpin[i].count < 3 ) // Count number of times LOW { level = true ; // Not long enough: handle as HIGH } } if ( level != touchpin[i].cur ) // Change seen? { touchpin[i].cur = level ; // And the new level if ( !level ) // HIGH to LOW change? { dbgprint ( "TOUCH%02d is now %d ( < %d ), execute %s", pinnr, tlevel, THRESHOLD, touchpin[i].command.c_str() ) ; reply = analyzeCmd ( touchpin[i].command.c_str() ); // Analyze command and handle it dbgprint ( reply ) ; // Result for debugging } } } }

//** // S C A N I R * //** // See if IR input is available. Execute the programmed command. * //** void scanIR() { char mykey[20] ; // For numerated key String val ; // Contents of preference entry const char* reply ; // Result of analyzeCmd

if ( irvalue ) // Any input? { sprintf ( mykey, "ir%04X", ir_value ) ; // Form key in preferences if ( nvssearch ( mykey ) ) { val = nvsgetstr ( mykey ) ; // Get the contents dbgprint ( "IR code %04X received. Will execute %s", ir_value, val.c_str() ) ; reply = analyzeCmd ( val.c_str() ) ; // Analyze command and handle it dbgprint ( reply ) ; // Result for debugging } else { dbgprint ( "IR code %04X received, but not found in preferences!", ir_value ) ; } ir_value = 0 ; // Reset IR code received } }

//** // M K _ L S A N * //** // Make al list of acceptable networks in preferences. // Will be called only once by setup(). // The result will be stored in wifilist. // Not that the last found SSID and password are kept in common data. If only one SSID is // defined, the connect is made without using wifiMulti. In this case a connection will // be made even if de SSID is hidden. //** void mk_lsan() { uint8_t i ; // Loop control char key[10] ; // For example: "wifi_03" String buf ; // "SSID/password" String lssid, lpw ; // Last read SSID and password from nvs int inx ; // Place of "/" WifiInfo_t winfo ; // Element to store in list

for ( i = 0 ; i < 100 ; i++ ) // Examine wifi_00 .. wifi99 { sprintf ( key, "wifi%02d", i ) ; // Form key in preferences if ( nvssearch ( key ) ) // Does it exists? { buf = nvsgetstr ( key ) ; // Get the contents inx = buf.indexOf ( "/" ) ; // Find separator between ssid and password if ( inx > 0 ) // Separator found? { lpw = buf.substring ( inx + 1 ) ; // Isolate password lssid = buf.substring ( 0, inx ) ; // Holds SSID now dbgprint ( "Added %s to list of networks", lssid.c_str() ) ; winfo.inx = i ; // Create new element for wifilist ; winfo.ssid = strdup ( lssid.c_str() ) ; // Set ssid of element winfo.passphrase = strdup ( lpw.c_str() ) ; wifilist.push_back ( winfo ) ; // Add to list wifiMulti.addAP ( winfo.ssid, // Add to wifi acceptable network list winfo.passphrase ) ; } } } }

//** // G E T R A D I O S T A T U S * //** // Return preset-, tone- and volume status. // Included are the presets, the current station, the volume and the tone settings. //** String getradiostatus() { char pnr[3] ; // Preset as 2 character, i.e. "03"

sprintf ( pnr, "%02d", ini_block.newpreset ) ; // Current preset return String ( "preset=" ) + // Add preset setting String ( pnr ) + String ( "\nvolume=" ) + // Add volume setting String ( String ( ini_block.reqvol ) ) + String ( "\ntoneha=" ) + // Add tone setting HA String ( ini_block.rtone[0] ) + String ( "\ntonehf=" ) + // Add tone setting HF String ( ini_block.rtone[1] ) + String ( "\ntonela=" ) + // Add tone setting LA String ( ini_block.rtone[2] ) + String ( "\ntonelf=" ) + // Add tone setting LF String ( ini_block.rtone[3] ) ; }

//** // G E T S E T T I N G S * //** // Send some settings to the webserver. // Included are the presets, the current station, the volume and the tone settings. //** void getsettings() { String val ; // Result to send String statstr ; // Station string int inx ; // Position of search char in line int i ; // Loop control, preset number char tkey[12] ; // Key for preset preference

for ( i = 0 ; i < 100 ; i++ ) // Max 99 presets { sprintf ( tkey, "preset_%02d", i ) ; // Preset plus number if ( nvssearch ( tkey ) ) // Does it exists? { // Get the contents statstr = nvsgetstr ( tkey ) ; // Get the station // Show just comment if available. Otherwise the preset itself. inx = statstr.indexOf ( "#" ) ; // Get position of "#" if ( inx > 0 ) // Hash sign present? { statstr.remove ( 0, inx + 1 ) ; // Yes, remove non-comment part } chomp ( statstr ) ; // Remove garbage from description val += String ( tkey ) + String ( "=" ) + statstr + String ( "\n" ) ; // Add delimeter if ( val.length() > 1000 ) // Time to flush? { cmdclient.print ( val ) ; // Yes, send val = "" ; // Start new string } } } val += getradiostatus() + // Add radio setting String ( "\n\n" ) ; // End of reply cmdclient.print ( val ) ; // And send }

//** // T F T L O G * //** // Log to TFT if enabled. * //** void tftlog ( const char *str ) { if ( tft ) // TFT configured? { tft->println ( str ) ; // Yes, show error on TFT } }

//** // T R P I N S * //** // Change keynames of pin definitions. Only necessary if old names are being used in NVS. // This function will be removed in future versions. //** void trpins() { uint8_t i = 0 ; // Loop control const char* tr[][2] = { // Translation table old->new key name { "ir_pin", "pin_ir" }, { "enc_clk", "pin_enc_clk" }, { "enc_dt", "pin_enc_dt" }, { "enc_sw", "pin_enc_sw" }, { "tft_cs", "pin_tft_cs" }, { "tft_dc", "pin_tft_dc" }, { "sd_cs", "pin_sd_cs" }, { "vs_cs", "pin_vs_cs" }, { "vs_dcs", "pin_vs_dcs" }, { "vs_dreq", "pin_vs_dreq" }, { "spi_sck", "pin_spi_sck" }, { "spi_miso", "pin_spi_miso" }, { "spi_mosi", "pin_spi_mosi" }, { NULL, NULL } // End of list } ;

while ( tr[i][0] ) // Loop trough keys to be translated { nvschkey ( tr[i][0], tr[i][1] ) ; // Change if existing i++ ; } }

//** // F I N D N S I D * //** // Find the namespace ID for the namespace passed as parameter. * //** uint8_t FindNsID ( const char* ns ) { esp_err_t result = ESP_OK ; // Result of reading partition uint32_t offset = 0 ; // Offset in nvs partition uint8_t i ; // Index in Entry 0..125 uint8_t bm ; // Bitmap for an entry uint8_t res = 0xFF ; // Function result

while ( offset < nvs->size ) { result = esp_partition_read ( nvs, offset, // Read 1 page in nvs partition &nvsbuf, sizeof(nvsbuf) ) ; if ( result != ESP_OK ) { dbgprint ( "Error reading NVS!" ) ; break ; } i = 0 ; while ( i < 126 ) {

  bm = ( nvsbuf.Bitmap[i / 4] >> ( ( i % 4 ) * 2 ) ) ;    // Get bitmap for this entry,
  bm &= 0x03 ;                                            // 2 bits for one entry
  if ( ( bm == 2 ) &&
       ( nvsbuf.Entry[i].Ns == 0 ) &&
       ( strcmp ( ns, nvsbuf.Entry[i].Key ) == 0 ) )
  {
    res = nvsbuf.Entry[i].Data & 0xFF ;                   // Return the ID
    offset = nvs->size ;                                  // Stop outer loop as well
    break ;
  }
  else
  {
    if ( bm == 2 )
    {
      i += nvsbuf.Entry[i].Span ;                         // Next entry
    }
    else
    {
      i++ ;
    }
  }
}
offset += sizeof(nvs_page) ;                              // Prepare to read next page in nvs

} return res ; }

//** // B U B B L E S O R T K E Y S * //** // Bubblesort the nvskeys. * //** void bubbleSortKeys ( uint16_t n ) { uint16_t i, j ; // Indexes in nvskeys char tmpstr[16] ; // Temp. storage for a key

for ( i = 0 ; i < n - 1 ; i++ ) // Examine all keys { for ( j = 0 ; j < n - i - 1 ; j++ ) // Compare to following keys { if ( strcmp ( nvskeys[j], nvskeys[j + 1] ) > 0 ) // Next key out of order? { strcpy ( tmpstr, nvskeys[j] ) ; // Save current key a while strcpy ( nvskeys[j], nvskeys[j + 1] ) ; // Replace current with next key strcpy ( nvskeys[j + 1], tmpstr ) ; // Replace next with saved current } } } }

//** // F I L L K E Y L I S T * //** // File the list of all relevant keys in NVS. // The keys will be sorted. //** void fillkeylist() { esp_err_t result = ESP_OK ; // Result of reading partition uint32_t offset = 0 ; // Offset in nvs partition uint16_t i ; // Index in Entry 0..125. uint8_t bm ; // Bitmap for an entry uint16_t nvsinx = 0 ; // Index in nvskey table

keynames.clear() ; // Clear the list while ( offset < nvs->size ) { result = esp_partition_read ( nvs, offset, // Read 1 page in nvs partition &nvsbuf, sizeof(nvsbuf) ) ; if ( result != ESP_OK ) { dbgprint ( "Error reading NVS!" ) ; break ; } i = 0 ; while ( i < 126 ) {

  bm = ( nvsbuf.Bitmap[i / 4] >> ( ( i % 4 ) * 2 ) ) ;      // Get bitmap for this entry, 2 bit for one entry
  bm &= 0x03 ;
  if ( bm == 2 )                                            // Entry is active?
  {
    if ( nvsbuf.Entry[i].Ns == namespace_ID )               // Namespace right?
    {
      strcpy ( nvskeys[nvsinx], nvsbuf.Entry[i].Key ) ;     // Yes, save in table
      if ( ++nvsinx == MAXKEYS )
      {
        nvsinx-- ;                                          // Prevent excessive index
      }
    }
    i += nvsbuf.Entry[i].Span ;                             // Next entry
  }
  else
  {
    i++ ;
  }
}
offset += sizeof(nvs_page) ;                                // Prepare to read next page in nvs

} nvskeys[nvsinx][0] = '\0' ; // Empty key at the end dbgprint ( "Read %d keys from NVS", nvsinx ) ; bubbleSortKeys ( nvsinx ) ; // Sort the keys }

//** // S E T U P * //** // Setup for the program. * //** void setup() { int i ; // Loop control int pinnr ; // Input pinnumber const char p ; byte mac[6] ; // WiFi mac address char tmpstr[20] ; // For version and Mac address const char partname = "nvs" ; // Partition with NVS info esp_partition_iterator_t pi ; // Iterator for find const char* wvn = "Include file %s_html has the wrong version number! Replace header file." ;

Serial.begin ( 115200 ) ; // For debug Serial.println() ; // Version tests for some vital include files if ( about_html_version < 170626 ) dbgprint ( wvn, "about" ) ; if ( config_html_version < 171207 ) dbgprint ( wvn, "config" ) ; if ( index_html_version < 180102 ) dbgprint ( wvn, "index" ) ; if ( mp3play_html_version < 170626 ) dbgprint ( wvn, "mp3play" ) ; if ( defaultprefs_version < 171215 ) dbgprint ( wvn, "defaultprefs" ) ; // Print some memory and sketch info dbgprint ( "Starting ESP32-radio running on CPU %d at %d MHz. Version %s. Free memory %d", xPortGetCoreID(), ESP.getCpuFreqMHz(), VERSION, ESP.getFreeHeap() ) ; // Normally about 199 kB maintask = xTaskGetCurrentTaskHandle() ; // My taskhandle SPIsem = xSemaphoreCreateMutex(); ; // Semaphore for SPI bus pi = esp_partition_find ( ESP_PARTITION_TYPE_DATA, // Get partition iterator for ESP_PARTITION_SUBTYPE_ANY, // the NVS partition partname ) ; if ( pi ) { nvs = esp_partition_get ( pi ) ; // Get partition struct esp_partition_iterator_release ( pi ) ; // Release the iterator dbgprint ( "Partition %s found, %d bytes", partname, nvs->size ) ; } else { dbgprint ( "Partition %s not found!", partname ) ; // Very unlikely... while ( true ) ; // Impossible to continue } trpins() ; // Translate keys in NVS to new names namespace_ID = FindNsID ( NAME ) ; // Find ID of our namespace in NVS fillkeylist() ; // Fill keynames with all keys memset ( &ini_block, 0, sizeof(ini_block) ) ; // Init ini_block ini_block.clk_server = "pool.ntp.org" ; // Default server for NTP ini_block.clk_offset = 1 ; // Default Amsterdam time zone ini_block.clk_dst = 1 ; // DST is +1 hour ini_block.bat0 = 0 ; // Battery ADC levels not yet defined ini_block.bat100 = 0 ; readIOprefs() ; // Read pins used for SPI, TFT, VS1053, IR, Rotary encoder for ( i = 0 ; (pinnr = progpin[i].gpio) >= 0 ; i++ ) // Check programmable input pins { pinMode ( pinnr, INPUT_PULLUP ) ; // Input for control button delay ( 10 ) ; // Check if pull-up active if ( ( progpin[i].cur = digitalRead ( pinnr ) ) == HIGH ) { p = "HIGH" ; } else { p = "LOW, probably no PULL-UP" ; // No Pull-up } dbgprint ( "GPIO%d is %s", pinnr, p ) ; } readprogbuttons() ; // Program the free input pins SPI.begin ( ini_block.spi_sck_pin, // Init VSPI bus with default or modified pins ini_block.spi_miso_pin, ini_block.spi_mosi_pin ) ; vs1053player = new VS1053 ( ini_block.vs_cs_pin, // Make instance of player ini_block.vs_dcs_pin, ini_block.vs_dreq_pin, ini_block.vs_shutdown_pin ) ; pinMode ( ini_block.ir_pin, INPUT ) ; // Pin for IR receiver VS1838B attachInterrupt ( ini_block.ir_pin, isr_IR, CHANGE ) ; // Interrupts will be handle by isr_IR if ( ini_block.tft_cs_pin >= 0 ) { dbgprint ( "Start TFT" ) ; tft = new TFT_ILI9163C ( ini_block.tft_cs_pin, ini_block.tft_dc_pin ) ; // Create an instant for TFT tft->begin() ; // Init TFT interface tft->setBitrate ( 16000000 ) ; // High speed tft->setRotation ( 1 ) ; // Use landscape format (1 for upside down) tft->fillRect ( 0, 0, 160, 128, BLACK ) ; // Clear screen does not work when rotated tft->clearScreen() ; // Clear screen tft->setTextSize ( 1 ) ; // Small character font tft->setTextColor ( WHITE ) ; // Info in white tft->print ( "\n" "Starting..." "\n" "Version:" ) ; strncpy ( tmpstr, VERSION, 16 ) ; // Limit version length tft->println ( tmpstr ) ; tft->println ( "By Ed Smallenburg" ) ; } if ( ini_block.sd_cs_pin >= 0 ) // SD configured? { if ( !SD.begin ( ini_block.sd_cs_pin, SPI, // Yes, SDSPEED ) ) // try to init SD card driver { p = dbgprint ( "SD Card Mount Failed!" ) ; // No success, check formatting (FAT) if ( tft ) { tftlog ( p ) ; // Show error on TFT as well } } else { SD_okay = ( SD.cardType() != CARD_NONE ) ; // See if known card if ( !SD_okay ) { p = dbgprint ( "No SD card attached" ) ; // Card not readable tftlog ( p ) ; // Show error on TFT as well } else { dbgprint ( "Locate mp3 files on SD, may take a while..." ) ; tftlog ( "Read SD card" ) ; SD_nodecount = listsdtracks ( "/", 0, false ) ; // Build nodelist p = dbgprint ( "%d tracks on SD", SD_nodecount ) ; tftlog ( p ) ; // Show number of tracks on TFT } } } mk_lsan() ; // Make al list of acceptable networks in preferences. WiFi.mode ( WIFI_STA ) ; // This ESP is a station //WiFi.persistent ( false ) ; // Do not save SSID and password WiFi.disconnect() ; // After restart the router could still keep the old connection delay ( 100 ) ; listNetworks() ; // Search for WiFi networks readprefs ( false ) ; // Read preferences tcpip_adapter_set_hostname ( TCPIP_ADAPTER_IF_STA, NAME ) ; vs1053player->begin() ; // Initialize VS1053 player delay(10); p = dbgprint ( "Connect to WiFi" ) ; // Show progress tftlog ( p ) ; // On TFT too NetworkFound = connectwifi() ; // Connect to WiFi network dbgprint ( "Start server for commands" ) ; cmdserver.begin() ; // Start http server if ( NetworkFound ) // OTA only if Wifi network found { dbgprint ( "Network found. Starting OTA" ) ; ArduinoOTA.setHostname ( NAME ) ; // Set the hostname ArduinoOTA.onStart ( otastart ) ; ArduinoOTA.begin() ; // Allow update over the air } else { currentpreset = ini_block.newpreset ; // No network: do not start radio } timer = timerBegin ( 0, 80, true ) ; // User 1st timer with prescaler 80 timerAttachInterrupt ( timer, &timer100, true ) ; // Call timer100() on timer alarm timerAlarmWrite ( timer, 100000, true ) ; // Alarm every 100 msec timerAlarmEnable ( timer ) ; // Enable the timer delay ( 1000 ) ; // Show IP for a while configTime ( ini_block.clk_offset 3600, ini_block.clk_dst 3600, ini_block.clk_server.c_str() ) ; // GMT offset, daylight offset in seconds timeinfo.tm_year = 0 ; // Set TOD to illegal // Init settings for rotary switch (if existing). if ( ( ini_block.enc_clk_pin + ini_block.enc_dt_pin + ini_block.enc_sw_pin ) > 2 ) { attachInterrupt ( ini_block.enc_clk_pin, isr_enc_turn, CHANGE) ; attachInterrupt ( ini_block.enc_dt_pin, isr_enc_turn, CHANGE) ; attachInterrupt ( ini_block.enc_sw_pin, isr_enc_switch, FALLING ) ; dbgprint ( "Rotary encoder is enabled" ) ; } else { dbgprint ( "Rotary encoder is disabled (%d/%d/%d)", ini_block.enc_clk_pin, ini_block.enc_dt_pin, ini_block.enc_sw_pin) ; } if ( NetworkFound ) { gettime() ; // Sync time } if ( tft ) { tft->fillRect ( 0, 8, 160, 118, BLACK ) ; // Clear most of the screen } outchunk.datatyp = QDATA ; // This chunk dedicated to QDATA adc1_config_width ( ADC_WIDTH_12Bit ) ; adc1_config_channel_atten ( ADC1_CHANNEL_0, ADC_ATTEN_0db ) ; dataqueue = xQueueCreate ( QSIZ, // Create queue for communication sizeof ( qdata_struct ) ) ; xTaskCreatePinnedToCore ( playtask, // Task function. "Playtask", // name of task. 2048, // Stack size of task NULL, // parameter of the task 1, // priority of the task &xplaytask, // Task handle to keep track of created task 0 ) ; // Pin task to core 0 for(int i = 0; i < 14; i++) { spectrum[i][1] = 0; // dato precedente spectrum[i][2] = 0; // picco grafico (non da Vs1053) } }

//** // R I N B Y T * //** // Read next byte from http inputbuffer. Buffered for speed reasons. * //** uint8_t rinbyt ( bool forcestart ) { static uint8_t buf[1024] ; // Inputbuffer static uint16_t i ; // Pointer in inputbuffer static uint16_t len ; // Number of bytes in buf uint16_t tlen ; // Number of available bytes uint16_t trycount = 0 ; // Limit max. time to read

if ( forcestart || ( i == len ) ) // Time to read new buffer { while ( cmdclient.connected() ) // Loop while the client's connected { tlen = cmdclient.available() ; // Number of bytes to read from the client len = tlen ; // Try to read whole input if ( len == 0 ) // Any input available? { if ( ++trycount > 3 ) // Not for a long time? { dbgprint ( "HTTP input shorter then expected" ) ; return '\n' ; // Error! No input } delay ( 10 ) ; // Give communication some time continue ; // Next loop of no input yet } if ( len > sizeof(buf) ) // Limit number of bytes { len = sizeof(buf) ; } len = cmdclient.read ( buf, len ) ; // Read a number of bytes from the stream i = 0 ; // Pointer to begin of buffer break ; } } return buf[i++] ; }

//** // W R I T E P R E F S * //** // Update the preferences. Called from the web interface. * //** void writeprefs() { int inx ; // Position in inputstr uint8_t winx ; // Index in wifilist char c ; // Input character String inputstr = "" ; // Input regel String key, contents ; // Pair for Preferences entry String dstr ; // Contents for debug

nvsclear() ; // Remove all preferences while ( true ) { c = rinbyt ( false ) ; // Get next inputcharacter if ( c == '\n' ) // Newline? { if ( inputstr.length() == 0 ) { dbgprint ( "End of writing preferences" ) ; break ; // End of contents } if ( !inputstr.startsWith ( "#" ) ) // Skip pure comment lines { inx = inputstr.indexOf ( "=" ) ; if ( inx >= 0 ) // Line with "="? { key = inputstr.substring ( 0, inx ) ; // Yes, isolate the key key.trim() ; contents = inputstr.substring ( inx + 1 ) ; // and contents contents.trim() ; dstr = contents ; // Copy for debug if ( ( key.indexOf ( "wifi_" ) == 0 ) ) // Sensitive info? { winx = key.substring(5).toInt() ; // Get index in wifilist if ( ( winx < wifilist.size() ) && // Existing wifi spec in wifilist? ( contents.indexOf ( "/**" ) > 0 ) ) // Hidden password? { contents = String ( wifilist[winx].ssid ) + // Retrieve ssid and password String ( "/" ) + String ( wifilist[winx].passphrase ) ; } dstr = String ( wifilist[winx].ssid ) + String ( "/*****" ) ; // Hide in debug line } dbgprint ( "writeprefs setstr %s = %s", key.c_str(), dstr.c_str() ) ; nvssetstr ( key.c_str(), contents ) ; // Save new pair } } inputstr = "" ; } else { if ( c != '\r' ) // Not newline. Is is a CR? { inputstr += String ( c ) ; // No, normal char, add to string } } } fillkeylist() ; // Update list with keys }

//** // H A N D L E H T T P R E P L Y * //** // Handle the output after an http request. * //** void handlehttpreply() { const char* p ; // Pointer to reply if command String sndstr = "" ; // String to send int n ; // Number of files on SD card

if ( http_reponse_flag ) { http_reponse_flag = false ; if ( cmdclient.connected() ) { if ( http_rqfile.length() == 0 && // An empty "GET"? http_getcmd.length() == 0 ) { if ( NetworkFound ) // Yes, check network { handleFSf ( String( "index.html") ) ; // Okay, send the startpage } else { handleFSf ( String( "config.html") ) ; // Or the configuration page if in AP mode } } else { if ( http_getcmd.length() ) // Command to analyze? { dbgprint ( "Send reply for %s", http_getcmd.c_str() ) ; sndstr = httpheader ( String ( "text/html" ) ) ; // Set header if ( http_getcmd.startsWith ( "getprefs" ) ) // Is it a "Get preferences"? { tftset ( 0, " CONFIG.." ); if ( datamode != STOPPED ) // Still playing? { forceStop() ; // Stop playing } sndstr += readprefs ( true ) ; // Read and send } else if ( http_getcmd.startsWith ( "getdefs" ) ) // Is it a "Get default preferences"? { sndstr += String ( defprefs_txt + 1 ) ; // Yes, read initial values } else if ( http_getcmd.startsWith ("saveprefs") ) // Is is a "Save preferences" { writeprefs() ; // Yes, handle it } else if ( http_getcmd.startsWith ( "mp3list" ) ) // Is is a "Get SD MP3 tracklist"? { tftset ( 0, " SD MP3 Player" ); if ( datamode != STOPPED ) // Still playing? { forceStop() ; // Stop playing } cmdclient.print ( sndstr ) ; // Yes, send header n = listsdtracks ( "/" ) ; // Handle it dbgprint ( "%d tracks found on SD card", n ) ; return ; // Do not send empty line } else if ( http_getcmd.startsWith ( "settings" ) ) // Is is a "Get settings" (like presets and tone)? { cmdclient.print ( sndstr ) ; // Yes, send header getsettings() ; // Handle settings request return ; // Do not send empty line } else { p = analyzeCmd ( http_getcmd.c_str() ) ; // Yes, do so sndstr += String ( p ) ; // Content of HTTP response follows the header } sndstr += String ( "\n" ) ; // The HTTP response ends with a blank line cmdclient.print ( sndstr ) ; } else if ( http_rqfile.length() ) // File requested? { dbgprint ( "Start file reply for %s", http_rqfile.c_str() ) ; handleFSf ( http_rqfile ) ; // Yes, send it } else { httpheader ( String ( "text/html" ) ) ; // Send header // the content of the HTTP response follows the header: cmdclient.println ( "Dummy response\n" ) ; // Text ending with double newline dbgprint ( "Dummy response sent" ) ; } } } } }

//** // H A N D L E H T T P * //** // Handle the input of an http request. * //** void handlehttp() { bool first = true ; // First call to rinbyt() char c ; // Next character from http input int inx0, inx ; // Pos. of search string in currenLine String currentLine = "" ; // Build up to complete line bool reqseen = false ; // No GET seen yet

if ( !cmdclient.connected() ) // Action if client is connected { return ; // No client active } dbgprint ( "handlehttp started" ) ; while ( true ) // Loop till command/file seen { c = rinbyt ( first ) ; // Get a byte first = false ; // No more first call if ( c == '\n' ) { // If the current line is blank, you got two newline characters in a row. // that's the end of the client HTTP request, so send a response: if ( currentLine.length() == 0 ) { http_reponse_flag = reqseen ; // Response required or not break ; } else { // Newline seen, remember if it is like "GET /xxx?y=2&b=9 HTTP/1.1" if ( currentLine.startsWith ( "GET /" ) ) // GET request? { inx0 = 5 ; // Start search at pos 5 } else if ( currentLine.startsWith ( "POST /" ) ) // POST request? { inx0 = 6 ; } else { inx0 = 0 ; // Not GET nor POST } if ( inx0 ) // GET or POST request? { reqseen = true ; // Request seen inx = currentLine.indexOf ( "&" ) ; // Search for 2nd parameter if ( inx < 0 ) { inx = currentLine.indexOf ( " HTTP" ) ; // Search for end of GET command } // Isolate the command http_getcmd = currentLine.substring ( inx0, inx ) ; inx = http_getcmd.indexOf ( "?" ) ; // Search for command if ( inx == 0 ) // Arguments only? { http_getcmd = http_getcmd.substring ( 1 ) ; // Yes, get rid of question mark http_rqfile = "" ; // No file } else if ( inx > 0 ) // Filename present? { http_rqfile = http_getcmd.substring ( 0, inx ) ; // Remember filename http_getcmd = http_getcmd.substring ( inx + 1 ) ; // Remove filename from GET command } else { http_rqfile = http_getcmd ; // No parameters, set filename http_getcmd = "" ; } if ( http_getcmd.length() ) { dbgprint ( "Get command is: %s", // Show result http_getcmd.c_str() ) ; } if ( http_rqfile.length() ) { dbgprint ( "Filename is: %s", // Show requested file http_rqfile.c_str() ) ; } } currentLine = "" ; } } else if ( c != '\r' ) // No LINFEED. Is it a CR? { currentLine += c ; // No, add normal char to currentLine } } //cmdclient.stop() ; }

//** // X M L P A R S E * //** // Parses line with XML data and put result in variable specified by parameter. * //** void xmlparse ( String &line, const char *selstr, String &res ) { String sel = "</" ; // Will be like "</status-code" int inx ; // Position of "</..." in line

sel += selstr ; // Form searchstring if ( line.endsWith ( sel ) ) // Is this the line we are looking for? { inx = line.indexOf ( sel ) ; // Get position of end tag res = line.substring ( 0, inx ) ; // Set result } }

//** // X M L G E T H O S T * //** // Parses streams from XML data. // Example URL for XML Data Stream: // http://playerservices.streamtheworld.com/api/livestream?version=1.5&mount=IHR_TRANAAC&lang=en * //** String xmlgethost ( String mount ) { const char xmlhost = "playerservices.streamtheworld.com" ; // XML data source const char xmlget = "GET /api/livestream" // XML get parameters "?version=1.5" // API Version of IHeartRadio "&mount=%sAAC" // MountPoint with Station Callsign "&lang=en" ; // Language

String stationServer = "" ; // Radio stream server String stationPort = "" ; // Radio stream port String stationMount = "" ; // Radio stream Callsign uint16_t timeout = 0 ; // To detect time-out String sreply = "" ; // Reply from playerservices.streamtheworld.com String statuscode = "200" ; // Assume good reply char tmpstr[200] ; // Full GET command, later stream URL String urlout ; // Result URL

stop_mp3client() ; // Stop any current wificlient connections. dbgprint ( "Connect to new iHeartRadio host: %s", mount.c_str() ) ; datamode = INIT ; // Start default in metamode chunked = false ; // Assume not chunked sprintf ( tmpstr, xmlget, mount.c_str() ) ; // Create a GET commmand for the request dbgprint ( "%s", tmpstr ) ; if ( mp3client.connect ( xmlhost, 80 ) ) // Connect to XML stream { dbgprint ( "Connected to %s", xmlhost ) ; mp3client.print ( String ( tmpstr ) + " HTTP/1.1\r\n" "Host: " + xmlhost + "\r\n" "User-Agent: Mozilla/5.0\r\n" "Connection: close\r\n\r\n" ) ; while ( mp3client.available() == 0 ) { delay ( 200 ) ; // Give server some time if ( ++timeout > 25 ) // No answer in 5 seconds? { dbgprint ( "Client Timeout !" ) ; } } dbgprint ( "XML parser processing..." ) ; while ( mp3client.available() ) { sreply = mp3client.readStringUntil ( '>' ) ; sreply.trim() ; // Search for relevant info in in reply and store in variable xmlparse ( sreply, "status-code", statuscode ) ; xmlparse ( sreply, "ip", stationServer ) ; xmlparse ( sreply, "port", stationPort ) ; xmlparse ( sreply, "mount", stationMount ) ; if ( statuscode != "200" ) // Good result sofar? { dbgprint ( "Bad xml status-code %s", // No, show and stop interpreting statuscode.c_str() ) ; tmpstr[0] = '\0' ; // Clear result break ; } } if ( ( stationServer != "" ) && // Check if all station values are stored ( stationPort != "" ) && ( stationMount != "" ) ) { sprintf ( tmpstr, "%s:%s/%s_SC", // Build URL for ESP-Radio to stream. stationServer.c_str(), stationPort.c_str(), stationMount.c_str() ) ; dbgprint ( "Found: %s", tmpstr ) ; } } else { dbgprint ( "Can't connect to XML host!" ) ; // Connection failed tmpstr[0] = '\0' ; } mp3client.stop() ; return String ( tmpstr ) ; // Return final streaming URL. }

//** // H A N D L E S A V E R E Q * //** // Handle save volume/preset/tone. This will save current settings every 10 minutes to // the preferences. On the next restart these values will be loaded. // Note that saving prefences will only take place if contents has changed. * //** void handleSaveReq() { static uint32_t savetime = 0 ; // Limit save to once per 10 minutes

if ( ( millis() - savetime ) < 600000 ) // 600 sec is 10 minutes { return ; } savetime = millis() ; // Set time of last save nvssetstr ( "preset", String ( currentpreset ) ) ; // Save current preset nvssetstr ( "volume", String ( ini_block.reqvol ) ); // Save current volue nvssetstr ( "toneha", String ( ini_block.rtone[0] ) ) ; // Save current toneha nvssetstr ( "tonehf", String ( ini_block.rtone[1] ) ) ; // Save current tonehf nvssetstr ( "tonela", String ( ini_block.rtone[2] ) ) ; // Save current tonela nvssetstr ( "tonelf", String ( ini_block.rtone[3] ) ) ; // Save current tonelf }

//** // C H K _ E N C * //** // See if rotary encoder is activated and perform its functions. * //** void chk_enc() { static int16_t waittime = 0 ; // Reduce speed for selections static int8_t enc_preset ; // Selected preset static String enc_nodeID ; // Node of selected track static String enc_filename ; // Filename of selected track String tmp ; // Temporary string int16_t inx ; // Position in string int16_t lrc ; // Local rotationcount

if ( enc_menu_mode != VOLUME && enc_menu_mode != MENU ) // In default mode? { if ( enc_inactivity > 40 ) // No, more than 4 seconds inactive { //enc_inactivity = 0 ; //enc_menu_mode = VOLUME ; // Return to VOLUME mode //tftset ( 2, (char)NULL ) ; // Restore original text at bottom } } if ( singleclick ) { singleclick = false ; switch ( enc_menu_mode ) // Which mode (VOLUME, SWMUTE, PRESET, TRACK)? { case MENU : if (menux == 0) { tftset (10, ""); hostreq = true ; // Request this host enc_menu_mode = VOLUME ; menux = 0; delay(200); } break ; case VOLUME : if (datamode != STOPPED) { if (localfile) { claimSPI ( "sdmenureadseek" ) ; // Claim SPI bus if ( timeLineFileLength - mp3file.available() - (uxQueueMessagesWaiting ( dataqueue ) 32) - 2048 >= 0 ) {
lastseek = timeLineFileLength - mp3file.available() - (uxQueueMessagesWaiting ( dataqueue ) 32) - 2048 ; } else if (timeLineFileLength - mp3file.available() - 2048 >= 0) { lastseek = timeLineFileLength - mp3file.available() - 2048 ;
} else { lastseek = 0;
} releaseSPI() ; } datamode = STOPREQD; } enc_menu_mode = MENU ; // Will go back to VOLUME after timeout menux = 0; tftset (10, ""); tftset ( 0, "
MENU*" ) ; // Set screen segment text top line displaymenu(0); break ; case SWMUTE : break ; case PRESET : currentpreset = -1 ; // Make sure current is different ini_block.newpreset = enc_preset ; // Make a definite choice enc_menu_mode = VOLUME ; // Back to default mode break ; case TRACK : host = getSDfilename ( enc_nodeID ) ; // Select track as new host hostreq = true ; // Request this host enc_menu_mode = VOLUME ; // Back to default mode break ; } } if ( rotationcount == 0 ) // Any rotation? { return ; // No, return } if ( rotationcount > 0 ) // Make local { lrc = 1 ; // One step at the time rotationcount = 0; } else { lrc = -1 ; rotationcount = 0; } switch ( enc_menu_mode ) // Which mode (VOLUME, PRESET, TRACK)? { case MENU : if (lrc == 1) { if (menux < 2) { menux++; displaymenu(menux); } } else if (lrc == -1) { if (menux > 0) { menux--; displaymenu(menux); } } break ; case VOLUME : if (lrc == 1) { if ( ini_block.reqvol <= 98 ) { ini_block.reqvol += 2; muteflag = false ; // Mute off } } else if (lrc == -1) { if ( ini_block.reqvol >= 2 ) { ini_block.reqvol -= 2; } } break ; case PRESET : waittime = 10 ; // Wait some time after this enc_preset += lrc ; // Next preset if ( enc_preset < 0 ) // Negative not allowed { enc_preset = 0 ; // Stay at 0 } tmp = readhostfrompref ( enc_preset ) ; // Get host spec and possible comment if ( tmp == "" ) // End of presets? { enc_preset = 0 ; // Yes, wrap tmp = readhostfrompref ( enc_preset ) ; // Get host spec and possible comment } dbgprint ( "Preset is %d", enc_preset ) ; // Show just comment if available. Otherwise the preset itself. inx = tmp.indexOf ( "#" ) ; // Get position of "#" if ( inx > 0 ) // Hash sign present? { tmp.remove ( 0, inx + 1 ) ; // Yes, remove non-comment part } chomp ( tmp ) ; // Remove garbage from description //tftset ( 4, tmp ) ; // Set screen segment bottom part break ; case TRACK : waittime = 5 ; // Wait some time after this enc_nodeID = selectnextSDnode ( enc_nodeID, lrc ) ; // Select the next file on SD enc_filename = getSDfilename ( enc_nodeID ) ; // Set new filename dbgprint ( "Select %s", enc_filename.c_str() ) ; while ( ( inx = enc_filename.indexOf ( "/" ) ) >= 0 ) // Search for last slash { enc_filename.remove ( 0, inx + 1 ) ; // Remove before the slash } dbgprint ( "Simplified %s", enc_filename.c_str() ) ; //tftset ( 4, enc_filename ) ; // Set screen segment bottom part default : break ; } rotationcount = 0 ; // Reset }

//** // M P 3 L O O P * //** // Called from the mail loop() for the mp3 functions. // A connection to an MP3 server is active and we are ready to receive data. // Normally there is about 2 to 4 kB available in the data stream. This depends on the sender. * //** void mp3loop() { static uint8_t tmpbuff[2048] ; // Input buffer for mp3 stream uint32_t maxchunk ; // Max number of bytes to read int res = 0 ; // Result reading from mp3 stream uint32_t av = 0 ; // Available in stream String nodeID ; // Next nodeID of track on SD

// Try to keep the Queue to playtask filled up by adding as much bytes as possible if ( datamode & ( INIT | HEADER | DATA | // Test op playing METADATA | PLAYLISTINIT | PLAYLISTHEADER | PLAYLISTDATA ) ) { maxchunk = sizeof(tmpbuff) ; // Reduce byte count for this mp3loop() if ( localfile ) // Playing file from SD card? { av = mp3file.available() ; // Bytes left in file if ( av < maxchunk ) // Reduce byte count for this mp3loop() { maxchunk = av ; } if ( maxchunk ) // Anything to read? { claimSPI ( "sdread" ) ; // Claim SPI bus res = mp3file.read ( tmpbuff, maxchunk ) ; // Read a block of data releaseSPI() ; // Release SPI bus } } else { av = mp3client.available() ; // Available from stream if ( av < maxchunk ) // Limit read size { maxchunk = av ; } if ( maxchunk ) // Anything to read? { res = mp3client.read ( tmpbuff, maxchunk ) ; // Read a number of bytes from the stream } else { if ( datamode == PLAYLISTDATA ) // End of playlist { playlist_num = 0 ; // Yes, reset dbgprint ( "End of playlist seen" ) ; datamode = STOPPED ; ini_block.newpreset++ ; // Go to next preset } } } for ( int i = 0 ; i < res ; i++ ) { if (datamode != STOPREQD && datamode != STOPPED) { if (localfile && maxchunk < sizeof(tmpbuff) && i == res - 1) { handlebyte_ch ( tmpbuff[i], true ) ; // Handle one byte } else { handlebyte_ch ( tmpbuff[i], false ) ; // Handle one byte } totalcount++; } else { outqp = outchunk.buf ; // and pointer } } } if (timeLineFileFinisced) { timeLineFileFinisced = false; datamode = STOPREQD ; // End of local mp3-file detected nodeID = selectnextSDnode ( SD_currentnode, +1 ) ; // Select the next file on SD host = getSDfilename ( nodeID ) ; hostreq = true ; // Request this host } if ( ini_block.newpreset != currentpreset ) // New station or next from playlist requested? { if ( datamode != STOPPED ) // Yes, still busy? { datamode = STOPREQD ; // Yes, request STOP outqp = outchunk.buf ; // and pointer } 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 = readhostfrompref() ; // Lookup preset in preferences chomp ( host ) ; // Get rid of part after "#" } dbgprint ( "New preset/file requested (%d/%d) from %s", ini_block.newpreset, playlist_num, host.c_str() ) ; if ( host != "" ) // Preset in ini-file? { hostreq = true ; // Force this station as new preset } else { // This preset is not available, return to preset 0, will be handled in next mp3loop() dbgprint ( "No host for this preset" ) ; ini_block.newpreset = 0 ; // Wrap to first station } } } if ( datamode == STOPREQD ) // STOP requested? { dbgprint ( "STOP requested" ) ; if ( localfile ) { claimSPI ( "close" ) ; // Claim SPI bus mp3file.close() ; releaseSPI() ; // Release SPI bus } else { stop_mp3client() ; // Disconnect if still connected } while ( xQueueReceive ( dataqueue, &inchunk, 5 ) ); chunked = false ; // Not longer chunked datacount = 0 ; // Reset datacount outqp = outchunk.buf ; // and pointer stopsong = true; metaint = 0 ; // No metaint known now datamode = STOPPED ; // Yes, state becomes STOPPED if (enc_menu_mode != MENU ) { tftset (14, ""); } icystreamtitle = ""; delay(200); return ; } if ( hostreq ) // New preset or station? { hostreq = false ; currentpreset = ini_block.newpreset ; // Remember current preset // Find out if this URL is on localhost (SD). localfile = ( host.indexOf ( "localhost/" ) >= 0 ) ; if ( localfile ) // Play file from localhost? { if ( connecttofile() ) // Yes, open mp3-file { datamode = DATA ; // Start in DATA mode } } else { if ( host.startsWith ( "ihr/" ) ) // iHeartRadio station requested? { host = host.substring ( 4 ) ; // Yes, remove "ihr/" host = xmlgethost ( host ) ; // Parse the xml to get the host } connecttohost() ; // Switch to new host } }

}

void displaymenu(int x) { switch ( x ) // Which mode (VOLUME, SWMUTE, PRESET, TRACK)? { case 0 : tftset (11, "BACK", YELLOW); tftset (12, "WEB RADIO", WHITE); tftset (13, "SD MP3", WHITE); break ; case 1 : tftset (11, "BACK", WHITE); tftset (12, "WEB RADIO", YELLOW); tftset (13, "SD MP3", WHITE); break ; case 2 : tftset (11, "BACK", WHITE); tftset (12, "WEB RADIO", WHITE); tftset (13, "SD MP3", YELLOW); break ; default : break ; } }

//** // L O O P * //** // Main loop of the program. * //** void loop() { mp3loop() ; // Do mp3 related actions if ( resetreq ) // Reset requested? { delay ( 1000 ) ; // Yes, wait some time ESP.restart() ; // Reboot } scanserial() ; // Handle serial input scandigital() ; // Scan digital inputs scanIR() ; // See if IR input ArduinoOTA.handle() ; // Check for OTA //mp3loop() ; // Do more mp3 related actions handlehttpreply() ; cmdclient = cmdserver.available() ; // Check Input from client? if ( cmdclient ) // Client connected? { dbgprint ( "Command client available" ) ; handlehttp() ; } handleSaveReq() ; // See if time to save settings chk_enc() ; // Check rotary encoder functions }

void forceStop() { dbgprint ( "force STOP requested" ) ; if ( localfile ) { claimSPI ( "close" ) ; // Claim SPI bus mp3file.close() ; releaseSPI() ; // Release SPI bus } else { stop_mp3client() ; // Disconnect if still connected } while ( xQueueReceive ( dataqueue, &inchunk, 5 ) ); chunked = false ; // Not longer chunked datacount = 0 ; // Reset datacount outqp = outchunk.buf ; // and pointer stopsong = true; metaint = 0 ; // No metaint known now datamode = STOPPED ; // Yes, state becomes STOPPED if (enc_menu_mode != MENU ) { tftset (14, ""); } icystreamtitle = ""; delay(200); }

//** // 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, boolean last ) { static int chunksize = 0 ; // Chunkcount read from stream static uint16_t playlistcnt ; // Counter to find right entry in playlist static int LFcount ; // Detection of end of header static bool ctseen = false ; // First line of header seen or not

if (datamode == STOPREQD || datamode == STOPPED) // STOP requested? { outqp = outchunk.buf ; // Item empty now return; } if ( chunked && ( 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 ; return ; } chunkcount-- ; // Update count to next chunksize block } if ( datamode == DATA ) // Handle next byte of MP3/Ogg data { outqp++ = b ; if ( outqp == ( outchunk.buf + sizeof(outchunk.buf) ) || last) // Buffer full? { while (outqp != ( outchunk.buf + sizeof(outchunk.buf) ) ) { outqp++ = '0' ; } // Send data to playtask queue. If the buffer cannot be placed within 200 ticks, // the queue is full, while the sender tries to send more. The chunk will be dis- // carded it that case. xQueueSend ( dataqueue, &outchunk, 200 ) ; // Send to queue outqp = outchunk.buf ; // Item empty now } if ( metaint ) // No METADATA on Ogg streams or mp3 files { if ( --datacount == 0 ) // End of datablock? { datamode = METADATA ; metalinebfx = -1 ; // Expecting first metabyte (counter) } } return ; } if ( datamode == INIT ) // Initialize for header receive { ctseen = false ; // Contents type not seen yet metaint = 0 ; // No metaint found LFcount = 0 ; // For detection end of header bitrate = 0 ; // Bitrate still unknown dbgprint ( "Switch to HEADER" ) ; datamode = HEADER ; // Handle header totalcount = 0 ; // Reset totalcount metalinebfx = 0 ; // No metadata yet metalinebf[0] = '\0' ; } 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 metalinebf[metalinebfx] = '\0' ; // Take care of delimiter if ( chkhdrline ( metalinebf ) ) // Reasonable input? { dbgprint ( "Headerline: %s", // Show headerline metalinebf ) ; String metaline = String ( metalinebf ) ; // Convert to string String lcml = metaline ; // Use lower case for compare lcml.toLowerCase() ; if ( lcml.startsWith ( "location: http://" ) ) // Redirection? { host = metaline.substring ( 17 ) ; // Yes, get new URL hostreq = true ; // And request this one } if ( lcml.indexOf ( "content-type" ) >= 0) // Line with "Content-Type: xxxx/yyy" { ctseen = true ; // Yes, remember seeing this String ct = metaline.substring ( 13 ) ; // Set contentstype. Not used yet ct.trim() ; dbgprint ( "%s seen.", ct.c_str() ) ; } if ( lcml.startsWith ( "icy-br:" ) ) { bitrate = metaline.substring(7).toInt() ; // Found bitrate tag, read the bitrate if ( bitrate == 0 ) // For Ogg br is like "Quality 2" { bitrate = 87 ; // Dummy bitrate } } else if ( lcml.startsWith ("icy-metaint:" ) ) { metaint = metaline.substring(12).toInt() ; // Found metaint tag, read the value } else if ( lcml.startsWith ( "icy-name:" ) ) { if (!silentreconon) { icyname = metaline.substring(9) ; // Get station name icyname.trim() ; // Remove leading and trailing spaces tftset ( 1, icyname ) ; // Set screen segment bottom part } } else if ( lcml.startsWith ("icy-genre:" ) ) { if (!silentreconon) { icyurl = metaline.substring(10) ; // Get station url icyurl.trim() ; // Remove leading and trailing spaces tftset ( 2, icyurl ) ; // Set screen segment bottom part } } else if ( lcml.startsWith ( "transfer-encoding:" ) ) { // Station provides chunked transfer if ( lcml.endsWith ( "chunked" ) ) { chunked = true ; // Remember chunked transfer mode chunkcount = 0 ; // Expect chunkcount in DATA } } } metalinebfx = 0 ; // Reset this line if ( ( LFcount == 2 ) && ctseen ) // Content type seen and a double LF? { dbgprint ( "Switch to DATA, bitrate is %d" // Show bitrate ", metaint is %d", // and metaint bitrate, metaint ) ; datamode = DATA ; // Expecting data now datacount = metaint ; // Number of bytes before first metadata startsong = true; } } else { metalinebf[metalinebfx++] = (char)b ; // Normal character, put new char in metaline if ( metalinebfx >= METASIZ ) // Prevent overflow { metalinebfx-- ; } LFcount = 0 ; // Reset double CRLF detection } return ; } if ( datamode == METADATA ) // Handle next byte of metadata { if ( metalinebfx < 0 ) // First byte of metadata? { metalinebfx = 0 ; // Prepare to store first character 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 } } else { metalinebf[metalinebfx++] = (char)b ; // Normal character, put new char in metaline if ( metalinebfx >= METASIZ ) // Prevent overflow { metalinebfx-- ; } } if ( --metacount == 0 ) { metalinebf[metalinebfx] = '\0' ; // Make sure line is limited if ( strlen ( metalinebf ) ) // 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 ( metalinebf ) ; // Show artist and title if present in metadata } if ( metalinebfx > ( METASIZ - 10 ) ) // Unlikely metaline length? { dbgprint ( "Metadata block too long! Skipping all Metadata from now on." ) ; metaint = 0 ; // Probably no metadata } 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 // Sometimes this will only contain a single line metalinebfx = 0 ; // 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 metalinebf[metalinebfx] = '\0' ; // Take care of delimeter dbgprint ( "Playlistheader: %s", // Show playlistheader metalinebf ) ; metalinebfx = 0 ; // Ready for next line if ( LFcount == 2 ) { dbgprint ( "Switch to PLAYLISTDATA, " // For debug "search for entry %d", playlist_num ) ; datamode = PLAYLISTDATA ; // Expecting data now return ; } } else { metalinebf[metalinebfx++] = (char)b ; // Normal character, put new char in metaline if ( metalinebfx >= METASIZ ) // Prevent overflow { metalinebfx-- ; } 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 ? { int inx ; // Pointer in metaline metalinebf[metalinebfx] = '\0' ; // Take care of delimeter dbgprint ( "Playlistdata: %s", // Show playlistheader metalinebf ) ; if ( strlen ( metalinebf ) < 5 ) // Skip short lines { metalinebfx = 0 ; // Flush line metalinebf[0] = '\0' ; return ; } String metaline = String ( metalinebf ) ; // Convert to string 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? { metalinebfx = 0 ; // Yes, ignore 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, metalinebf ) ; 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 } metalinebfx = 0 ; // Prepare for next line host = playlist ; // Back to the .m3u host playlistcnt++ ; // Next entry in playlist } else { metalinebf[metalinebfx++] = (char)b ; // Normal character, add it to metaline if ( metalinebfx >= METASIZ ) // Prevent overflow { metalinebfx-- ; } } } }

//** // 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 S F * //** // Handling of requesting pages from the PROGMEM. Example: favicon.ico * //** void handleFSf ( const String& pagename ) { String ct ; // Content type const char* p ; int l ; // Size of requested page int TCPCHUNKSIZE = 1024 ; // Max number of bytes per write

dbgprint ( "FileRequest received %s", pagename.c_str() ) ; ct = getContentType ( pagename ) ; // Get content type if ( ( ct == "" ) || ( pagename == "" ) ) // Empty is illegal { cmdclient.println ( "HTTP/1.1 404 Not Found" ) ; cmdclient.println ( "" ) ; return ; } else { if ( pagename.indexOf ( "index.html" ) >= 0 ) // Index page is in PROGMEM { p = index_html ; l = sizeof ( index_html ) ; } else if ( pagename.indexOf ( "radio.css" ) >= 0 ) // CSS file is in PROGMEM { p = radio_css + 1 ; l = sizeof ( radio_css ) ; } else if ( pagename.indexOf ( "config.html" ) >= 0 ) // Config page is in PROGMEM { p = config_html ; l = sizeof ( config_html ) ; } else if ( pagename.indexOf ( "mp3play.html" ) >= 0 ) // Mp3player page is in PROGMEM { p = mp3play_html ; l = sizeof ( mp3play_html ) ; } else if ( pagename.indexOf ( "about.html" ) >= 0 ) // About page is in PROGMEM { p = about_html ; l = sizeof ( about_html ) ; } else if ( pagename.indexOf ( "favicon.ico" ) >= 0 ) // Favicon icon is in PROGMEM { p = (char)favicon_ico ; l = sizeof ( favicon_ico ) ; } else { p = index_html ; l = sizeof ( index_html ) ; } if ( p == '\n' ) // If page starts with newline: { p++ ; // Skip first character l-- ; } dbgprint ( "Length of page is %d", strlen ( p ) ) ; cmdclient.print ( httpheader ( ct ) ) ; // Send header // The content of the HTTP response follows the header: if ( l < 10 ) { cmdclient.println ( "Testline
" ) ; } else { while ( l ) // Loop through the output page { if ( l <= TCPCHUNKSIZE ) // Near the end? { cmdclient.write ( p, l ) ; // Yes, send last part l = 0 ; } else { cmdclient.write ( p, TCPCHUNKSIZE ) ; // Send part of the page p += TCPCHUNKSIZE ; // Update startpoint and rest of bytes l -= TCPCHUNKSIZE ; } } } // The HTTP response ends with another blank line: cmdclient.println() ; dbgprint ( "Response send" ) ; } }

//** // 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. * //** void 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 }

//** // A N A L Y Z E C M D * //** // Handling of the various commands from remote webclient, Serial. // Version for handling string with: = //** const char analyzeCmd ( const char str ) { char value ; // Points to value after equalsign in command const char res ; // Result of analyzeCmd

value = strstr ( str, "=" ) ; // See if command contains a "=" if ( value ) { value = '\0' ; // Separate command from value res = analyzeCmd ( str, value + 1 ) ; // Analyze command and handle it value = '=' ; // Restore equal sign } else { res = analyzeCmd ( str, "0" ) ; // No value, assume zero } return res ; }

//** // A N A L Y Z E C M D * //** // Handling of the various commands from remote webclient, serial. // 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 = // 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 = // Select new station (will not be saved) // station = .mp3 // Play standalone .mp3 file (not saved) // station = .m3u // Select playlist (will not be saved) // stop // Stop playing // resume // Resume playing // mute // Mute/unmute the music (toggle) // wifi_00 = mySSID/mypassword // Set WiFi SSID and password ) // clk_server = pool.ntp.org // Time server to be used ) // clk_offset = <-11..+14> // Offset with respect to UTC in hours ) // clk_dst = <1..2> // Offset during daylight saving time in hours ) // mp3track = // Play track from SD card, nodeID 0 = random // settings // Returns setting like presets and tone // status // Show current URL to play // test // For test purposes // debug = 0 or 1 // Switch debugging on or off // reset // Restart the ESP32 // bat0 = 2318 // ADC value for an empty battery // bat100 = 2916 // ADC value for a fully charged battery // Commands marked with ")" are sensible during initialization only //** const 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[180] ; // Reply to client, will be returned uint8_t oldvol ; // Current volume bool relative ; // Relative argument (+ or -) String tmpstr ; // Temporary for value uint32_t av ; // Available in stream/file

strcpy ( reply, "Command accepted" ) ; // Default reply argument = String ( par ) ; // Get the argument chomp ( argument ) ; // Remove comment and useless spaces if ( argument.length() == 0 ) // Lege commandline (comment)? { return reply ; // Ignore } argument.toLowerCase() ; // Force to lower case value = String ( val ) ; // Get the specified value chomp ( value ) ; // Remove comment and extra spaces ivalue = value.toInt() ; // Also as an integer ivalue = abs ( ivalue ) ; // Make positive relative = argument.indexOf ( "up" ) == 0 ; // + relative setting? if ( argument.indexOf ( "down" ) == 0 ) // - relative setting? { relative = true ; // It's relative ivalue = - ivalue ; // But with negative value } if ( value.startsWith ( "http://" ) ) // Does (possible) URL contain "http://"? { value.remove ( 0, 7 ) ; // Yes, remove it } if ( value.length() ) { tmpstr = value ; // Make local copy of value if ( argument.indexOf ( "passw" ) >= 0 ) // Password in value? { tmpstr = String ( "***" ) ; // Yes, hide it } dbgprint ( "Command: %s with parameter %s", argument.c_str(), tmpstr.c_str() ) ; } else { dbgprint ( "Command: %s (without parameter)", argument.c_str() ) ; } if ( argument.indexOf ( "volume" ) >= 0 ) // Volume setting? { // Volume may be of the form "upvolume", "downvolume" or "volume" for relative or absolute setting oldvol = vs1053player->getVolume() ; // Get current volume if ( relative ) // + relative setting? { ini_block.reqvol = oldvol + ivalue ; // Up/down by 0.5 or more dB } else { ini_block.reqvol = ivalue ; // Absolue setting } if ( ini_block.reqvol > 127 ) // Wrapped around? { ini_block.reqvol = 0 ; // Yes, keep at zero } if ( ini_block.reqvol > 100 ) { ini_block.reqvol = 100 ; // Limit to normal values } muteflag = false ; // Stop possibly muting sprintf ( reply, "Volume is now %d", // Reply new volume iniblock.reqvol ) ; } else if ( argument == "mute" ) // Mute/unmute request { muteflag = !muteflag ; // Request volume to zero/normal } else if ( argument.indexOf ( "ir" ) >= 0 ) // Ir setting? { // Do not handle here } else if ( argument.indexOf ( "preset_" ) >= 0 ) // Enumerated preset? { // Do not handle here } else if ( argument.indexOf ( "preset" ) >= 0 ) // (UP/DOWN)Preset station? { if ( relative ) // Relative argument? { ini_block.newpreset += ivalue ; // Yes, adjust currentpreset } else { ini_block.newpreset = ivalue ; // Otherwise set station playlist_num = 0 ; // Absolute, reset playlist } sprintf ( reply, "Preset is now %d", // Reply new preset ini_block.newpreset ) ; } else if ( argument == "stop" ) // (un)Stop requested? { if ( datamode & ( HEADER | DATA | METADATA | PLAYLISTINIT | PLAYLISTHEADER | PLAYLISTDATA ) )

{
  datamode = STOPREQD ;                           // Request STOP
}
else
{
  hostreq = true ;                                // Request UNSTOP
}

} else if ( ( value.length() > 0 ) && ( ( argument == "mp3track" ) || // Select a track from SD card? ( argument == "station" ) ) ) // Station in the form address:port { if ( argument.startsWith ( "mp3" ) ) // MP3 track to search for { if ( !SD_okay ) // SD card present? { strcpy ( reply, "Command not accepted!" ) ; // Error reply return reply ; } value = getSDfilename ( value ) ; // like "localhost/........" } 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, "Playing %s", // Format reply host.c_str() ) ; utf8ascii ( reply ) ; // Remove possible strange characters } 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 == "test" ) // Test command { if ( localfile ) { av = timeLineFileLength ; // Available bytes in file } else { av = mp3client.available() ; // Available in stream } sprintf ( reply, "Free memory is %d, chunks in queue %d, stream %d, bitrate %d kbps", ESP.getFreeHeap(), uxQueueMessagesWaiting ( dataqueue ), av, mbitrate ) ; dbgprint ( "Stack CPU0 is %d", uxTaskGetStackHighWaterMark ( xplaytask ) ) ; dbgprint ( "Stack CPU1 is %d", uxTaskGetStackHighWaterMark ( maintask ) ) ; dbgprint ( "ADC reading is %d", adcval ) ; } // 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 == "debug" ) // debug on/off request? { DEBUG = ivalue ; // Yes, set flag accordingly } else if ( argument == "getnetworks" ) // List all WiFi networks? { sprintf ( reply, networks.cstr() ) ; // Reply is SSIDs } else if ( argument.startsWith ( "clk" ) ) // TOD parameter? { if ( argument.indexOf ( "server" ) > 0 ) // Yes, NTP server spec? { ini_block.clk_server = value ; // Yes, set server } if ( argument.indexOf ( "offset" ) > 0 ) // Offset with respect to UTC spec? { ini_block.clk_offset = ivalue ; // Yes, set offset } if ( argument.indexOf ( "dst" ) > 0 ) // Offset duringe DST spec? { ini_block.clk_dst = ivalue ; // Yes, set DST offset } } else if ( argument.startsWith ( "bat" ) ) // Battery ADC value? { if ( argument.indexOf ( "100" ) == 3 ) // 100 percent value? { ini_block.bat100 = ivalue ; // Yes, set it } else if ( argument.indexOf ( "0" ) == 3 ) // 0 percent value? { ini_block.bat0 = ivalue ; // Yes, set it } } else { sprintf ( reply, "%s called with illegal parameter: %s", NAME, argument.c_str() ) ; } return reply ; // Return reply to the caller }

//** // H T T P H E A D E R * //** // Set http headers to a string. * //** String httpheader ( String contentstype ) { return String ( "HTTP/1.1 200 OK\nContent-type:" ) + contentstype + String ( "\n" "Server: " NAME "\n" "Cache-Control: " "max-age=3600\n" "Last-Modified: " VERSION "\n\n" ) ; }

//** // Function that are called from playtask. //**

//** // D I S P L A Y B A T T E R Y * //** // Show the current battery charge level on the screen. // It will overwrite the top divider. // No action if bat0/bat100 not defined in the preferences. * //** void displaybattery() { if ( tft ) { if ( ini_block.bat0 < ini_block.bat100 ) // Levels set in preferences? { static uint16_t oldpos = 0 ; // Previous charge level uint16_t ypos ; // Position on screen uint16_t v ; // Constarinted ADC value uint16_t newpos ; // Current setting

  v = constrain ( adcval, ini_block.bat0,             // Prevent out of scale
                  ini_block.bat100 ) ;
  newpos = map ( v, ini_block.bat0,                   // Compute length of green bar
                 ini_block.bat100, 0, 160 ) ;
  if ( newpos != oldpos )                             // Value changed?
  {
    oldpos = newpos ;                                 // Remember for next compare
    ypos = tftdata[1].y - 5 ;                         // Just before 1st divider
    tft->fillRect ( 0, ypos, newpos, 2, GREEN ) ;     // Paint green part
    tft->fillRect ( newpos, ypos,
                    160 - newpos, 2, RED ) ;          // Paint red part
  }
}

} }

//** // D I S P L A Y V O L U M E * //** // Show the current volume as an indicator on the screen. * //** void displayvolume() { if ( tft ) { uint8_t newvol ; // Current setting uint8_t pos ; // Positon of volume indicator

newvol = vs1053player->getVolume() ;                // Get current volume setting
if ( newvol != oldvol )                             // Volume changed?
{
  oldvol = newvol ;                                 // Remember for next compare
  pos = map ( newvol, 0, 100, 0, 160 ) ;            // Compute position on TFT
  tft->fillRect ( 0, 126, pos, 2, RED ) ;           // Paint red part
  tft->fillRect ( pos, 126, 160 - pos, 2, GREEN ) ; // Paint green part
}

} }

void displayvolumestop() { if ( tft ) { uint8_t newvol ; // Current setting uint8_t pos ; // Positon of volume indicator

  newvol = ini_block.reqvol;                        // Get current volume setting
  if ( newvol != oldvol )                             // Volume changed?
{
  oldvol = newvol ;                                 // Remember for next compare
  pos = map ( newvol, 0, 100, 0, 160 ) ;            // Compute position on TFT
  tft->fillRect ( 0, 126, pos, 2, RED ) ;           // Paint red part
  tft->fillRect ( pos, 126, 160 - pos, 2, GREEN ) ; // Paint green part
}

} }

//** // D I S P L A Y T I M E * //** // Show the time on the LCD at a fixed position in a specified color // To prevent flickering, only the changed part of the timestring is displayed. // An empty string will force a refresh on next call. // A character on the screen is 8 pixels high and 6 pixels wide. //** void displaytime ( const char* str, uint16_t color ) { static char oldstr[9] = "........" ; // For compare uint8_t i ; // Index in strings uint8_t pos = TIMEPOS ; // X-position of character

if ( str[0] == '\0' ) // Empty string? { for ( i = 0 ; i < 8 ; i++ ) // Set oldstr to dots { oldstr[i] = '.' ; } return ; // No actual display yet } if ( tft ) // TFT active? { tft->setTextColor ( color ) ; // Set the requested color tft->setTextSize ( 1 ) ; for ( i = 0 ; i < 8 ; i++ ) // Compare old and new { if ( str[i] != oldstr[i] ) // Difference? { tft->fillRect ( pos, 3, 6, 8, BLACK ) ; // Clear the space for new character tft->setCursor ( pos, 3 ) ; // Prepare to show the info tft->print ( str[i] ) ; // Show the character oldstr[i] = str[i] ; // Remember for next compare } pos += 6 ; // Next position } } }

//** // D I S P L A Y I N F O * //** // Show a string on the LCD at a specified y-position (0..2) in a specified color. // The parametr is the index in tftdata[]. //** void displayinfo ( uint16_t inx ) { uint16_t width = 160 ; // Normal number of colums uint8_t space = 0 ; // Normal number of colums uint8_t vspace = 0 ; boolean cleara = true; scrseg_struct* p = &tftdata[inx] ;

if ( inx == 0 ) // Topline is shorter { width = 90 ; // Leave space for time } if ( tft ) // TFT active? { claimSPI ( "displayinfo" ) ; uint16_t len = p->str.length() + 1 ; // Required length of buffer char buf [ len ] ; // Need some buffer space if ( inx == 1 ) { if ( p->str.length() <= 8 ) { tft->setTextSize ( 3 ) ; uint8_t totalpix = (p->str.length()15) + ((p->str.length()-1)3); if (totalpix < 160) { space = (160-totalpix)/2; } } else if ( p->str.length() <= 13 ) { tft->setTextSize ( 2 ) ; uint8_t totalpix = (p->str.length()10) + ((p->str.length()-1)2); if (totalpix < 160) { space = (160-totalpix)/2; } vspace = 4; } else { tft->setTextSize ( 1 ) ; uint8_t totalpix = (p->str.length()5) + ((p->str.length()-1)1); if (p->str.length() > 26) { p->str = p->str.substring(0, 25); } if (totalpix < 160) { space = (160-totalpix)/2; } vspace = 6; } } else if ( inx == 2 ) { tft->setTextSize ( 1 ) ; uint8_t totalpix = (p->str.length()5) + ((p->str.length()-1)1); if (p->str.length() > 26) { p->str = p->str.substring(0, 25); } if (totalpix < 160) { space = (160-totalpix)/2; }
} else if ( inx == 5 ) { if ( p->str.length() <= 13 ) { tft->setTextSize ( 2 ) ; uint8_t totalpix = (p->str.length()10) + ((p->str.length()-1)2); if (totalpix < 160) { space = (160-totalpix)/2; } vspace = 4; } else if (p->str.length() <= 26) { tft->setTextSize ( 1 ) ; uint8_t totalpix = (p->str.length()5) + ((p->str.length()-1)1); if (totalpix < 160) { space = (160-totalpix)/2; } vspace = 6; } else { tft->setTextSize ( 1 ) ; if (p->str.length() > 52) { p->str = p->str.substring(0, 51); } } } else if ( inx > 10 && inx < 14 ) { tft->setTextSize ( 2 ) ; uint8_t totalpix = (p->str.length()10) + ((p->str.length()-1)2); if (totalpix < 160) { space = (160-totalpix)/2; } cleara = false; } else { tft->setTextSize ( 1 ) ; }

p->str.toCharArray ( buf, len ) ;                    // Make a local copy of the string
utf8ascii ( buf ) ;                                  // Convert possible UTF8
if (cleara)
{
tft->fillRect ( 0, p->y, width, p->height, BLACK ) ; // Clear the space for new info
}
tft->setTextColor ( p->color ) ;                     // Set the requested color
tft->setCursor ( space, p->y + vspace ) ;                         // Prepare to show the info
tft->println ( buf ) ;                               // Show the string
releaseSPI() ;

} }

//** // G E T T I M E * //** // Retrieve the local time from NTP server and convert to string. // Will be called every second. //** void gettime() { static int16_t delaycount = 0 ; // To reduce number of NTP requests static int16_t retrycount = 100 ;

if ( tft ) // TFT used? { if ( timeinfo.tm_year ) // Legal time found? { sprintf ( timetxt, "%02d:%02d:%02d", // Yes, format to a string timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec ) ; } if ( --delaycount <= 0 ) // Sync every few hours { delaycount = 7200 ; // Reset counter if ( timeinfo.tm_year ) // Legal time found? { dbgprint ( "Sync TOD, old value is %s", timetxt ) ; } dbgprint ( "Sync TOD" ) ; if ( !getLocalTime ( &timeinfo ) ) // Read from NTP server { dbgprint ( "Failed to obtain time!" ) ; // Error Serial.println("Orario NON trovato"); timeinfo.tm_year = 0 ; // Set current time to illegal if ( retrycount ) // Give up syncing? { retrycount-- ; // No try again delaycount = 5 ; // Retry after 5 seconds } } else { sprintf ( timetxt, "%02d:%02d:%02d", // Format new time to a string timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec ) ; dbgprint ( "Sync TOD, new value is %s", timetxt ) ; Serial.println("OK orario trovato"); } } } }

//** // H A N D L E T F T T X T * //** // Check if tft refresh is requested. * //** bool handle_tft_txt() { for ( uint16_t i = 0 ; i < TFTSECS ; i++ ) // Handle all sections { if ( tftdata[i].update_req ) // Refresh requested? { displayinfo ( i ) ; // Yes, do the refresh tftdata[i].update_req = false ; // Reset request return true ; // Just handle 1 request } } return false ; // Not a single request }

//** // DISPLAY TIME LINE * //** // * //** void displayTimeLine() { uint16_t played1 = 0;

if (timeLineFileLength > 2048 ) { if (timeLineFilePlayed > 2048 ) { played1 = (150*(timeLineFilePlayed - 2048))/(timeLineFileLength - 2048); if (played1 < played) { tft->fillRect ( 0, 80, 160, 9, BLACK ); tft->fillRect ( 5, 85, 150, 1, WHITE ); played = 0; } else if (played1 > played) { played = played1; tft->fillRect ( 5, 84, played, 3, WHITE ); } } } if ( timeLineFilePlayed != 0 && timeLineFilePlayed == pretimeLineFilePlayed ) { timeLineFileFinisced = true; tft->fillRect ( 5, 84, 150, 3, WHITE ); pretimeLineFilePlayed = timeLineFilePlayed = 0; } pretimeLineFilePlayed = timeLineFilePlayed; }

//** // DISPLAY SPECTRUM * //** // * //** void displaySpectrum() { uint8_t larg = (160-bands+1)/bands; uint8_t posi = (160-(bands*larg)-bands+1)/2; boolean visual = true; if (enc_menu_mode == MENU) { visual = false; } if (bands != prevbands) { prevbands = bands; if (visual) { tft->fillRect ( 0, 90, 160, 35, BLACK ) ; } } for ( uint8_t i = 0 ; i < bands ; i++ ) // Handle all sections { if (visual) { if (spectrum[i][0] > spectrum[i][1]) { tft->fillRect ( posi + 1, 90 + 34 - spectrum[i][0], larg, spectrum[i][0], GREEN ) ; tft->fillRect ( posi + 1, 90, larg, 34 - spectrum[i][0], BLACK ) ; } else if (spectrum[i][0] <= spectrum[i][1]) { tft->fillRect ( posi + 1, 90, larg, 34 - spectrum[i][0], BLACK ) ; } } if (spectrum[i][2] > 0) { spectrum[i][2]--; } if (spectrum[i][0] > spectrum[i][2]) { spectrum[i][2] = spectrum[i][0];
}

if (visual) { tft->fillRect ( posi + 1, 90 + 34 - spectrum[i][2] -3, larg, 2, RED ) ; }
spectrum[i][1] = spectrum[i][0];
posi += larg+1;   

} }

//** // DISPLAY SIGNAL * //** // * //**

void displaysignal() { uint8_t posx = 90; uint8_t posy = 1; if ( NetworkFound && tft) { uint8_t mini = 90; uint8_t maxi = 50; static uint8_t preclev = -2; uint8_t lev = 0; if ( WiFi.status() == WL_CONNECTED ) { uint8_t rssii = (uint8_t)WiFi.RSSI() * -1; if (rssii >= 80) { lev = 0; } else if (rssii >= 70) { lev = 1; } else if (rssii >= 60) { lev = 2; } else if (rssii >= 50) { lev = 3; } if (preclev == -2) { preclev = -1; tft->fillRect ( posx, posy, 14, 8, BLACK ) ; // Clear the space for new character drawdot(posx, posy, RED); draw1(posx, posy, RED, 1); draw1(posx, posy, RED, 2); draw1(posx, posy, RED, 3); } else if (lev != preclev) { tft->fillRect ( posx, posy, 14, 8, BLACK ) ; // Clear the space for new character preclev = lev; if (lev > 2) { drawdot(posx, posy, WHITE); draw1(posx, posy, WHITE, 1); draw1(posx, posy, WHITE, 2); draw1(posx, posy, WHITE, 3); } else if (lev > 1) { drawdot(posx, posy, WHITE); draw1(posx, posy, WHITE, 1); draw1(posx, posy, WHITE, 2); } else if (lev > 0) { drawdot(posx, posy, WHITE); draw1(posx, posy, WHITE, 1); } else { drawdot(posx, posy, WHITE); } } } else { drawdot(posx, posy, RED); draw1(posx, posy, RED, 1); draw1(posx, posy, RED, 2); draw1(posx, posy, RED, 3); } } else if (tft) { drawdot(posx, posy, RED); draw1(posx, posy, RED, 1); draw1(posx, posy, RED, 2); draw1(posx, posy, RED, 3);
} }

void drawdot(uint8_t posx, uint8_t posy, int color) { for (uint8_t i = 0; i < 2; i++) { tft->drawPixel(posx + 6 + i, posy + 8, color); } for (uint8_t i = 0; i < 2; i++) { tft->drawPixel(posx + 6 + i, posy + 8 - 1, color); }
} void draw1(uint8_t posx, uint8_t posy, int color, uint8_t val) { if (val == 1) { tft->drawPixel(posx + 4, posy + 6, color); tft->drawPixel(posx + 9, posy + 6, color); for (int i = 0; i < 4; i++) { tft->drawPixel(posx + 5 + i, posy + 5, color); } } else if (val == 2) { tft->drawPixel(posx + 2, posy + 5, color); tft->drawPixel(posx + 11, posy + 5, color); tft->drawPixel(posx + 3, posy + 4, color); tft->drawPixel(posx + 10, posy + 4, color); for (int i = 0; i < 6; i++) { tft->drawPixel(posx + 4 + i, posy + 3, color); }
} else if (val == 3) { tft->drawPixel(posx, posy + 4, color); tft->drawPixel(posx + 13, posy + 4, color); tft->drawPixel(posx + 1, posy + 3, color); tft->drawPixel(posx + 12, posy + 3, color); tft->drawPixel(posx + 2, posy + 2, color); tft->drawPixel(posx + 11, posy + 2, color); for (int i = 0; i < 8; i++) { tft->drawPixel(posx + 3 + i, posy + 1, color); }
} }

//** // SILENTRECON * //** // Handle non-queue functions for playtask. * //**

void silentrecon() { Serial.println("Riconnetto silent"); morethanonc++ ; // Count the fails if ( morethanonc > 2 ) // No! Happened too many times? { datamode = STOPREQD ; // Stop player ini_block.newpreset++ ; // Yes, try next channel if ( datamode & ( PLAYLISTDATA | // In playlist mode? PLAYLISTINIT | PLAYLISTHEADER ) ) { playlist_num = 0 ; // Yes, end of playlist } return; } if (!connecttohost()) { silentreconon = false; millisbit = millis(); } }

//** // H A N D L E _ S P E C * //** // Handle non-queue functions for playtask. * //** void handle_spec() { if (millis() >= millisbit + 500 && playingstat == 1 && !localfile) { millisbit = millis(); if (totalcount == 0) { if (!silentreconon) { silentreconon = true; silentrecon(); } } else { mbitrate = (totalcount * 8)/500; totalcount = 0; morethanonc = 0 ; silentreconon = false; } } if (startsong) { played = 150; startsong = false; playingstat = 1 ; millisbit = millis(); totalcount = 0; claimSPI ( "startsong" ) ; // Claim SPI bus vs1053player->startSong() ; // START, start player releaseSPI() ; // Release SPI bus pretimeLineFilePlayed = 0; } if (stopsong) { played = 150; stopsong = false; playingstat = 0 ; // Status claimSPI ( "stopsong" ) ; // Claim SPI bus vs1053player->setVolume ( 0 ) ; // Mute vs1053player->stopSong() ; // STOP, stop player releaseSPI() ; timeLineFilePlayed = pretimeLineFilePlayed = 0; //while ( xQueueReceive ( dataqueue, &inchunk, 5 ) ); //vTaskDelay ( 500 / portTICK_PERIOD_MS ) ; // Pause for a short time } if ( time_req ) { time_req = false ; // Clear request if ( NetworkFound ) // Time to refresh timetxt? { gettime() ; // Get the curret time } claimSPI ( "time" ) ; displaytime ( timetxt ) ; // Write to TFT screen displaysignal() ; releaseSPI() ; } if (millis() >= millisbands + 100) { claimSPI ( "bands" ) ; // Claim SPI bus
vs1053player->getBands () ; displaySpectrum(); if (enc_menu_mode != MENU) { if (localfile && playingstat == 1) { displayTimeLine(); } } releaseSPI() ; millisbands = millis(); }

if ( ( tft != NULL ) && tft_update_req ) // Need to update TFT? { tft_update_req = handle_tft_txt() ; // Yes, TFT refresh necessary } if ( muteflag && vs1053player->getVolume() != 0) // Mute or not? { claimSPI ( "hspec" ) ; vs1053player->setVolume ( 0 ) ; // Mute displayvolume() ; releaseSPI() ; } else if (vs1053player->getVolume() < ini_block.reqvol && !muteflag && millis() >= millisvol + 40 && playingstat == 1) { millisvol = millis(); claimSPI ( "hspec" ) ; if ( vs1053player->getVolume() + 8 <= ini_block.reqvol ) { vs1053player->setVolume ( vs1053player->getVolume() + 8 ) ; // Unmute } else { vs1053player->setVolume ( ini_block.reqvol ) ;
} displayvolume() ; releaseSPI() ; } else if (vs1053player->getVolume() > ini_block.reqvol && !muteflag && millis() >= millisvol + 40 && playingstat == 1) { millisvol = millis(); claimSPI ( "hspec" ) ; if ( vs1053player->getVolume() - 8 >= ini_block.reqvol ) { vs1053player->setVolume ( vs1053player->getVolume() - 8 ) ; // Unmute } else { vs1053player->setVolume ( ini_block.reqvol ) ;
} displayvolume() ; releaseSPI() ; } else if (vs1053player->getVolume() != ini_block.reqvol && !muteflag && playingstat == 0 ) { claimSPI ( "hspec" ) ; displayvolumestop() ; releaseSPI() ; } if ( reqtone ) // Request to change tone? { claimSPI ( "hspec" ) ; reqtone = false ; vs1053player->setTone ( ini_block.rtone ) ; // Set SCI_BASS to requested value releaseSPI() ; } }

//** // P L A Y T A S K * //** // Play stream data from input queue. // Handle all I/O to VS1053B during normal playing. // Handles display of text, time and volume on TFT as well. * //** void playtask ( void * parameter ) { while ( true ) { handle_spec() ; // Maybe some special funcs? if ( xQueueReceive ( dataqueue, &inchunk, 5 ) ) { while ( !vs1053player->data_request() ) // If FIFO is full.. { handle_spec() ; // Maybe some special funcs? if ( !vs1053player->data_request() ) // Still idle? { vTaskDelay ( 1 ) ; // Yes, take a break } } switch ( inchunk.datatyp ) // What kind of chunk? { case QDATA: if (datamode != STOPREQD && datamode != STOPPED) { if (localfile) { timeLineFilePlayed += sizeof(inchunk.buf); } claimSPI ( "chunk" ) ; // Claim SPI bus vs1053player->playChunk ( inchunk.buf, // DATA, send to player sizeof(inchunk.buf) ) ; releaseSPI() ; // Release SPI bus } break ; default: break ; } } } vTaskDelete ( NULL ) ; // Will never arrive here }

void searchId3() { readFile((char )id3Buffer, 0, ID3_HEADER); if (memcmp(id3Buffer, "ID3", 3) == 0) { searchId3V2(); } else { readFile((char )id3Buffer, timeLineFileLength-128, ID3_HEADER); if (memcmp(id3Buffer, "TAG", 3) == 0) { searchId3V1(); } }
}

void searchId3V2() { int positionbuf = 0; int bufstart = 10; char majorv; char minorv; char flags; int tagsize; int extsize = 0; int framesize; char frameid[4]; char frameflags[2]; boolean ver22 = false; readFile(id3Buffer, positionbuf, 10); majorv = id3Buffer[positionbuf += 3]; if(majorv == 1) { Serial.println("ID3v21"); ver22 = true; } else if(majorv == 2) { Serial.println("ID3v22"); ver22 = true; } else if(majorv == 3) { Serial.println("ID3v23"); } else if(majorv == 4) { Serial.println("ID3v24"); } else { Serial.println("Versione non trovata"); } minorv = id3Buffer[positionbuf += 1]; flags = id3Buffer[positionbuf += 1]; tagsize = int_decode(btoint(id3Buffer, 4, positionbuf += 1)); if(flags&(1<<6)==(1<<6)) { extsize = int_decode(btoint(id3Buffer, 4, positionbuf += 4)); } positionbuf = 10; if(extsize > 0) { positionbuf += extsize+4; } if (!ver22) { while(positionbuf < tagsize) { readFile(id3Buffer, positionbuf, 4); positionbuf += 4; memcpy(frameid, id3Buffer, 4); if(memcmp(frameid, "\0\0\0\0", 4) != 0) { readFile(id3Buffer, positionbuf, 4); positionbuf += 4; framesize = btoint(id3Buffer, 4, 0); if(majorv == 4) { framesize = int_decode(framesize); } readFile(id3Buffer, positionbuf, 2); positionbuf += 2; memcpy(frameflags, id3Buffer, 2); int fsiz = 98; if (framesize - 1 < 98) { fsiz = framesize -1; } if(strncmp(frameid, "TIT2", 4) == 0) // Titolo { readFile(id3Buffer, positionbuf + 1, fsiz); mp3Title = storeId3(fsiz); tftset ( 7, mp3Title ) ; positionbuf += framesize; } else if (strncmp(frameid, "TPE1", 4) == 0) // Artista { readFile(id3Buffer, positionbuf + 1, fsiz); mp3Artist = storeId3(fsiz); tftset ( 6, mp3Artist ) ; positionbuf += framesize; } else if (strncmp(frameid, "TALB", 4) == 0) // Album { readFile(id3Buffer, positionbuf + 1, fsiz); mp3Album = storeId3(fsiz); tftset ( 8, mp3Album ) ; positionbuf += framesize; } else if (strncmp(frameid, "TYER", 4) == 0) // Anno { readFile(id3Buffer, positionbuf + 1, fsiz); mp3Year = storeId3(fsiz); tftset ( 9, mp3Year) ; positionbuf += framesize; } else { positionbuf += framesize; } } } } else { while(positionbuf < tagsize) { readFile(id3Buffer, positionbuf, 3); positionbuf += 3; memcpy(frameid, id3Buffer, 3); if(memcmp(frameid, "\0\0\0", 3) != 0) { readFile(id3Buffer, positionbuf, 3); positionbuf += 3; framesize = btoint(id3Buffer, 3, 0) - 1; readFile(id3Buffer, positionbuf, 1); positionbuf += 1; memcpy(frameflags, id3Buffer, 1); int fsiz = 98; if (framesize < 98) { fsiz = framesize; } if(strncmp(frameid, "TT2", 3) == 0) // Titolo { readFile(id3Buffer, positionbuf, fsiz-1); mp3Title = storeId3(fsiz-1); tftset ( 7, mp3Title ) ; positionbuf += framesize; } else if (strncmp(frameid, "TP1", 3) == 0) // Artista { readFile(id3Buffer, positionbuf, fsiz-1); mp3Artist = storeId3(fsiz-1); tftset ( 6, mp3Artist ) ; positionbuf += framesize; } else if (strncmp(frameid, "TAL", 3) == 0) // Album { readFile(id3Buffer, positionbuf, fsiz-1); mp3Album = storeId3(fsiz-1); tftset ( 8, mp3Album ) ; positionbuf += framesize; } else if (strncmp(frameid, "TYE", 3) == 0) // Anno { readFile(id3Buffer, positionbuf, fsiz-1); mp3Year = storeId3(fsiz-1); tftset ( 9, mp3Year ) ; positionbuf += framesize; } else { positionbuf += framesize; } } }

} }

void searchId3V1() { readFile(id3Buffer, timeLineFileLength-128+TRACK_TITLE, 30); mp3Title = storeId3(30); tftset ( 7, mp3Title ) ; readFile(id3Buffer, timeLineFileLength-128+TRACK_ARTIST, 30); mp3Artist = storeId3(30); tftset ( 6, mp3Artist ) ; readFile(id3Buffer, timeLineFileLength-128+TRACK_ALBUM, 30); mp3Album = storeId3(30); tftset ( 8, mp3Album ) ; readFile(id3Buffer, timeLineFileLength-128+TRACK_YEAR, 4); mp3Year = storeId3(30); tftset ( 9, mp3Year ) ; }

void readFile(char * bufferid3, uint32_t adress, uint32_t lenght) { for (uint8_t i = 0; i < 100; i++) { bufferid3[i] = '\0'; } mp3file.seek(adress); for (uint32_t i = 0; i < lenght; i++) { bufferid3[i] = mp3file.read(); } }

String storeId3 (int siz) { String id3String; if (id3Buffer[0] == 0xef && id3Buffer[1] == 0xbb && id3Buffer[2] == 0xbf) { id3Buffer[0] = ' '; id3Buffer[1] = ' '; id3Buffer[2] = ' '; } else if (id3Buffer[0] == 0xff && id3Buffer[1] == 0xfe) { id3Buffer[0] = ' '; id3Buffer[1] = ' '; } else if (id3Buffer[0] == 0xfe && id3Buffer[1] == 0xff) { id3Buffer[0] = ' '; id3Buffer[1] = ' '; } for (int i=0; i < siz; i++) { id3String += isoToAscii (id3Buffer[i]); } id3String.trim(); if (id3String.length() > 26) { id3String.substring(0, 25); } return id3String; }

unsigned int btoint(char* bytes, int sizes, int offset) { unsigned int result = 0x00; int i = 0; for(i = 0; i < sizes; i++) { result = result << 8; result = result | (unsigned char) bytes[offset + i]; }

return result;

}

int int_decode(int values) { unsigned int a, b, c, d, result = 0x0; a = values & 0xFF; b = (values >> 8) & 0xFF; c = (values >> 16) & 0xFF; d = (values >> 24) & 0xFF;

result = result | a;
result = result | (b << 7);
result = result | (c << 14);
result = result | (d << 21);

return result;

}

String isoToAscii (char car) { uint8_t bcar = (uint8_t) car; String scar; if ( bcar > 0x7f ) { switch (bcar) { case 0xc8 : scar = "E'"; return scar; case 0xc9 : scar = "E'"; return scar; case 0xca : scar = "E"; return scar; case 0xf9 : scar = "u'"; return scar; default : scar = ""; return scar;
} } scar = String(car); return scar; }

MirkoDalmonte commented 5 years ago

This is the PATCHES file:

// PATCHES const unsigned short patch[3201] PROGMEM = { / Compressed plugin / 0x0007,0x0001, /copy 1/ 0x8050, 0x0006,0x0002, /copy 2/ 0x2a00,0xc000, 0x0006, 0x801e, 0x0000, /Rle(30)/ 0x0006,0x04aa, /copy 1194/ 0xf400,0x4095,0x0000,0x02c2,0x6124,0x0024,0x0000,0x0024, 0x2800,0x1ac5,0x4192,0x4542,0x0000,0x0041,0x2000,0x0015, 0x0030,0x0317,0x2000,0x0000,0x3f00,0x4024,0x2000,0x0000, 0x0000,0x0000,0x3e12,0x3800,0x3e00,0xb804,0x0030,0x0015, 0x0007,0x8257,0x3700,0x984c,0xf224,0x1444,0xf224,0x0024, 0x0008,0x0002,0x2910,0x0181,0x0000,0x1bc8,0xb428,0x1402, 0x0000,0x8004,0x2910,0x0195,0x0000,0x1bc8,0xb428,0x0024, 0x0006,0x0095,0x2800,0x2945,0x3e13,0x780e,0x3e11,0x7803, 0x3e13,0xf806,0x3e11,0xf801,0x3510,0xb808,0x003f,0xe004, 0xfec4,0x3800,0x48be,0x17c3,0xfe46,0x41c2,0x48be,0x4497, 0x4090,0x1c46,0xf06c,0x0024,0x2400,0x2580,0x6090,0x41c3, 0x6628,0x1c47,0x0000,0x0024,0x2800,0x2449,0xf07e,0x0024, 0xf400,0x4182,0x673a,0x1c46,0x0000,0x0024,0x2800,0x2589, 0xf06c,0x0024,0xf400,0x41c3,0x0000,0x0024,0x4224,0x3442, 0x2900,0xa640,0x4336,0x37c3,0x0000,0x1805,0x2900,0xa640, 0x4508,0x40c2,0x450a,0x9808,0x0000,0x0207,0xa478,0x1bc0, 0xc45a,0x1807,0x0030,0x03d5,0x3d01,0x5bc1,0x36f3,0xd806, 0x3601,0x5803,0x36f3,0x0024,0x36f3,0x580e,0x0007,0x8257, 0x0000,0x6004,0x3730,0x8024,0xb244,0x1c04,0xd428,0x3c02, 0x0006,0xc717,0x2800,0x2d05,0x4284,0x0024,0x3613,0x3c02, 0x0006,0xc357,0x2901,0x6880,0x3e11,0x5c05,0x4284,0x1bc5, 0x0000,0x0024,0x2800,0x2fc5,0x0000,0x0024,0x3613,0x0024, 0x3e10,0x3813,0x3e14,0x8024,0x3e04,0x8024,0x2900,0x4f40, 0x0006,0x02d3,0x36e3,0x0024,0x3009,0x1bd3,0x0007,0x8257, 0x3700,0x8024,0xf224,0x0024,0x0000,0x0024,0x2800,0x31d1, 0x3600,0x9844,0x2900,0x3780,0x0000,0x3248,0x2911,0xf140, 0x0000,0x0024,0x0030,0x0057,0x3700,0x0024,0xf200,0x4595, 0x0fff,0xfe02,0xa024,0x164c,0x8000,0x17cc,0x3f00,0x0024, 0x3500,0x0024,0x0021,0x6d82,0xd024,0x44c0,0x0006,0xa402, 0x2800,0x3695,0xd024,0x0024,0x0000,0x0000,0x2800,0x3695, 0x000b,0x6d57,0x3009,0x3c00,0x36f0,0x8024,0x36f2,0x1800, 0x2000,0x0000,0x0000,0x0024,0x3e14,0x7810,0x3e13,0xb80d, 0x3e13,0xf80a,0x3e10,0xb803,0x3e11,0x3805,0x3e11,0xb807, 0x3e14,0xf801,0x3e15,0x3815,0x0001,0x000a,0x0006,0xc4d7, 0xbf8e,0x9c42,0x3e01,0x9c03,0x0006,0xa017,0x0023,0xffd1, 0x0007,0x8250,0x0fff,0xfd85,0x3001,0x0024,0xa45a,0x4494, 0x0000,0x0093,0x2800,0x3dd1,0xf25a,0x104c,0x34f3,0x0024, 0x2800,0x3dd1,0x0000,0x0024,0x3413,0x084c,0x0000,0x0095, 0x3281,0xf806,0x4091,0x4d64,0x2400,0x4000,0x4efa,0x9c10, 0xf1eb,0x6061,0xfe55,0x2f66,0x5653,0x4d64,0x48b2,0xa201, 0x4efa,0xa201,0x36f3,0x3c10,0x36f5,0x1815,0x36f4,0xd801, 0x36f1,0x9807,0x36f1,0x1805,0x36f0,0x9803,0x36f3,0xd80a, 0x36f3,0x980d,0x2000,0x0000,0x36f4,0x5810,0x3e12,0xb817, 0x3e14,0xf812,0x3e01,0xb811,0x0007,0x9717,0x0020,0xffd2, 0x0030,0x11d1,0x3111,0x8024,0x3704,0xc024,0x3b81,0x8024, 0x3101,0x8024,0x3b81,0x8024,0x3f04,0xc024,0x2808,0x4800, 0x36f1,0x9811,0x36f3,0x0024,0x3009,0x3848,0x3e14,0x3811, 0x3e00,0x0024,0x0000,0x4000,0x0001,0x0010,0x2915,0x94c0, 0x0001,0xcc11,0x36f0,0x0024,0x2927,0x9e40,0x3604,0x1811, 0x3613,0x0024,0x3e14,0x3811,0x3e00,0x0024,0x0000,0x4000, 0x0001,0x0010,0x2915,0x94c0,0x0001,0xcc11,0x36f0,0x0024, 0x36f4,0x1811,0x3009,0x1808,0x2000,0x0000,0x0000,0x190d, 0x3600,0x3840,0x3e13,0x780e,0x3e13,0xf808,0x3e00,0x0024, 0x0000,0x464e,0x0027,0x9e0f,0x2922,0xb680,0x0000,0x190d, 0x36f3,0x0024,0x36f3,0xd808,0x36f3,0x580e,0x2000,0x0000, 0x3009,0x1800,0x3613,0x0024,0x3e22,0xb815,0x3e05,0xb814, 0x3615,0x0024,0x0000,0x800a,0x3e13,0x7801,0x3e10,0xb803, 0x3e11,0x3805,0x3e11,0xb807,0x3e14,0x3811,0x3e14,0xb813, 0x3e03,0xf80e,0xb488,0x44d5,0x3543,0x134c,0x34e5,0xc024, 0x3524,0x8024,0x35a4,0xc024,0x3710,0x8a0c,0x3540,0x4a0c, 0x3d44,0x8024,0x3a10,0x8024,0x3590,0x0024,0x4010,0x15c1, 0x6010,0x3400,0x3710,0x8024,0x2800,0x5b04,0x3af0,0x8024, 0x3df0,0x0024,0x3591,0x4024,0x3530,0x4024,0x4192,0x4050, 0x6100,0x1482,0x4020,0x1753,0xbf8e,0x1582,0x4294,0x4011, 0xbd86,0x408e,0x2400,0x590e,0xfe6d,0x2819,0x520e,0x0a00, 0x5207,0x2819,0x4fbe,0x0024,0xad56,0x904c,0xaf5e,0x1010, 0xf7d4,0x0024,0xf7fc,0x2042,0x6498,0x2046,0x3cf4,0x0024, 0x3400,0x170c,0x4090,0x1492,0x35a4,0xc024,0x2800,0x5395, 0x3c00,0x0024,0x4480,0x914c,0x36f3,0xd80e,0x36f4,0x9813, 0x36f4,0x1811,0x36f1,0x9807,0x36f1,0x1805,0x36f0,0x9803, 0x36f3,0x5801,0x3405,0x9014,0x36e3,0x0024,0x2000,0x0000, 0x36f2,0x9815,0x2814,0x9c91,0x0000,0x004d,0x2814,0x9940, 0x003f,0x0013,0x3e12,0xb817,0x3e12,0x3815,0x3e05,0xb814, 0x3625,0x0024,0x0000,0x800a,0x3e10,0x3801,0x3e10,0xb803, 0x3e11,0x3805,0x3e11,0xb807,0x3e14,0x3811,0x0006,0xa090, 0x2912,0x0d00,0x3e14,0xc024,0x4088,0x8000,0x4080,0x0024, 0x0007,0x90d1,0x2800,0x6605,0x0000,0x0024,0x0007,0x9051, 0x3100,0x4024,0x4100,0x0024,0x3900,0x0024,0x0007,0x90d1, 0x0004,0x0000,0x31f0,0x4024,0x6014,0x0400,0x0000,0x0024, 0x2800,0x6a51,0x4080,0x0024,0x0000,0x0000,0x2800,0x69c5, 0x0000,0x0024,0x0007,0x9053,0x3300,0x0024,0x4080,0x0024, 0x0000,0x0000,0x2800,0x6a58,0x0000,0x0024,0x0007,0x9051, 0x3900,0x0024,0x3200,0x504c,0x6410,0x0024,0x3cf0,0x0000, 0x4080,0x0024,0x0006,0xc691,0x2800,0x8305,0x3009,0x0400, 0x0007,0x9051,0x0000,0x1001,0x3100,0x0024,0x6012,0x0024, 0x0006,0xc6d0,0x2800,0x7749,0x003f,0xe000,0x0006,0xc693, 0x3900,0x0c00,0x3009,0x0001,0x6014,0x0024,0x0007,0x1ad0, 0x2800,0x7755,0x3009,0x0000,0x4080,0x0024,0x0000,0x0301, 0x2800,0x7145,0x4090,0x0024,0x0000,0x0024,0x2800,0x7255, 0x0000,0x0024,0x3009,0x0000,0xc012,0x0024,0x2800,0x7740, 0x3009,0x2001,0x3009,0x0000,0x6012,0x0024,0x0000,0x0341, 0x2800,0x7455,0x0000,0x0024,0x6190,0x0024,0x2800,0x7740, 0x3009,0x2000,0x6012,0x0024,0x0000,0x0381,0x2800,0x7615, 0x0000,0x0024,0x6190,0x0024,0x2800,0x7740,0x3009,0x2000, 0x6012,0x0024,0x0000,0x00c0,0x2800,0x7755,0x0000,0x0024, 0x3009,0x2000,0x0006,0xa090,0x3009,0x0000,0x4080,0x0024, 0x0000,0x0081,0x2800,0x7c15,0x0007,0x8c13,0x3300,0x104c, 0xb010,0x0024,0x0002,0x8001,0x2800,0x7e85,0x34f0,0x0024, 0x2800,0x7c00,0x0000,0x0024,0x0006,0xc351,0x3009,0x0000, 0x6090,0x0024,0x3009,0x2000,0x2900,0x0b80,0x3009,0x0405, 0x0006,0xc690,0x0006,0xc6d1,0x3009,0x0000,0x3009,0x0401, 0x6014,0x0024,0x0006,0xa093,0x2800,0x7a91,0xb880,0x0024, 0x2800,0x8bc0,0x3009,0x2c00,0x4040,0x0024,0x6012,0x0024, 0x0006,0xc6d0,0x2800,0x8bd8,0x0000,0x0024,0x0006,0xc693, 0x3009,0x0c00,0x3009,0x0001,0x6014,0x0024,0x0006,0xc350, 0x2800,0x8bc1,0x0000,0x0024,0x6090,0x0024,0x3009,0x2c00, 0x3009,0x0005,0x2900,0x0b80,0x0000,0x8bc8,0x3009,0x0400, 0x4080,0x0024,0x0003,0x8000,0x2800,0x8bc5,0x0000,0x0024, 0x6400,0x0024,0x0000,0x0081,0x2800,0x8bc9,0x0000,0x0024, 0x0007,0x8c13,0x3300,0x0024,0xb010,0x0024,0x0006,0xc650, 0x2800,0x8bd5,0x0000,0x0024,0x0001,0x0002,0x3413,0x0000, 0x3009,0x0401,0x4010,0x8406,0x0000,0x0281,0xa010,0x13c1, 0x4122,0x0024,0x0000,0x03c2,0x6122,0x8002,0x462c,0x0024, 0x469c,0x0024,0xfee2,0x0024,0x48be,0x0024,0x6066,0x8400, 0x0006,0xc350,0x2800,0x8bc1,0x0000,0x0024,0x4090,0x0024, 0x3009,0x2400,0x2900,0x0b80,0x3009,0x0005,0x0007,0x1b50, 0x2912,0x0d00,0x3613,0x0024,0x3a00,0x0380,0x4080,0x0024, 0x0000,0x00c1,0x2800,0x9485,0x3009,0x0000,0xb010,0x008c, 0x4192,0x0024,0x6012,0x0024,0x0006,0xf051,0x2800,0x9298, 0x3009,0x0400,0x0007,0x1fd1,0x30e3,0x0400,0x4080,0x0024, 0x0000,0x0301,0x2800,0x9485,0x3009,0x0000,0xb010,0x0024, 0x0000,0x0101,0x6012,0x0024,0x0006,0xf051,0x2800,0x9495, 0x0000,0x0024,0x3023,0x0400,0xf200,0x184c,0xb880,0xa400, 0x3009,0x2000,0x3009,0x0441,0x3e10,0x4402,0x2909,0xa9c0, 0x3e10,0x8024,0x36e3,0x0024,0x36f4,0xc024,0x36f4,0x1811, 0x36f1,0x9807,0x36f1,0x1805,0x36f0,0x9803,0x36f0,0x1801, 0x3405,0x9014,0x36f3,0x0024,0x36f2,0x1815,0x2000,0x0000, 0x36f2,0x9817,0x3613,0x0024,0x3e12,0xb817,0x3e12,0x3815, 0x3e05,0xb814,0x3615,0x0024,0x0000,0x800a,0x3e10,0xb803, 0x0012,0x5103,0x3e11,0x3805,0x3e11,0xb807,0x3e14,0x380d, 0x0030,0x0250,0x3e13,0xf80e,0xbe8b,0x83e0,0x290c,0x4840, 0x3613,0x0024,0x290c,0x4840,0x4086,0x984c,0x0000,0x00ce, 0x2400,0x9e8e,0x3009,0x1bc0,0x0000,0x01c3,0xae3a,0x184c, 0x0000,0x0043,0x3009,0x3842,0x290c,0x4840,0x3009,0x3840, 0x4084,0x9bc0,0xfe26,0x9bc2,0xceba,0x0024,0x4e8e,0x0024, 0x4e9a,0x0024,0x4f8e,0x0024,0x0000,0x0102,0x2800,0xa3c5, 0x0030,0x0010,0x0000,0x0206,0x3613,0x0024,0x290c,0x4840, 0x3009,0x3840,0x3000,0xdbc0,0xb366,0x0024,0x0000,0x0024, 0x2800,0xa3d5,0x4e8e,0x0024,0x4e9a,0x0024,0x4f8e,0x0024, 0x0030,0x0010,0x2800,0xa095,0x0000,0x0206,0x36f3,0xd80e, 0x36f4,0x180d,0x36f1,0x9807,0x36f1,0x1805,0x36f0,0x9803, 0x3405,0x9014,0x36f3,0x0024,0x36f2,0x1815,0x2000,0x0000, 0x36f2,0x9817,0xb386,0x40d7,0x4284,0x184c,0x0000,0x05c0, 0x2800,0xa7d5,0xf5d8,0x3804,0x0000,0x0984,0x6400,0xb84a, 0x3e13,0xf80d,0xa204,0x380e,0x0000,0x800a,0x0000,0x00ce, 0x2400,0xab0e,0xffa4,0x0024,0x48b6,0x0024,0x0000,0x0024, 0x2800,0xab04,0x4000,0x40c2,0x4224,0x0024,0x6090,0x0024, 0xffa4,0x0024,0x0fff,0xfe83,0xfe86,0x1bce,0x36f3,0xd80d, 0x48b6,0x0024,0x0fff,0xff03,0xa230,0x45c3,0x2000,0x0000, 0x36f1,0x180a, 0x0007,0x0001, /copy 1/ 0x8300, 0x0006,0x05e8, /copy 1512/ 0x0030,0x0055,0xb080,0x1402,0x0fdf,0xffc1,0x0007,0x9257, 0xb212,0x3c00,0x3d00,0x4024,0x0030,0x0297,0x3f00,0x0024, 0x0007,0x9017,0x3f00,0x0024,0x0007,0x81d7,0x3f10,0x0024, 0xc090,0x3c00,0x0006,0x0297,0xb080,0x3c00,0x0000,0x0401, 0x000a,0x1055,0x0006,0x0017,0x3f10,0x3401,0x000a,0x2795, 0x3f00,0x3401,0x0001,0x6857,0xf400,0x55c0,0x0000,0x0817, 0xb080,0x57c0,0x0014,0x958f,0x0000,0x5f4e,0x0030,0x0017, 0x3700,0x0024,0x0004,0x0001,0xb012,0x0024,0x0000,0x004d, 0x280f,0xe115,0x0006,0x2016,0x0006,0x01d7,0x3f00,0x0024, 0x0000,0x190d,0x000f,0xf94f,0x0000,0xcc4e,0x280f,0xe100, 0x0006,0x2016,0x0000,0x0080,0x0005,0x4f92,0x2909,0xf840, 0x3613,0x2800,0x0006,0x0197,0x0006,0xa115,0xb080,0x0024, 0x3f00,0x3400,0x0007,0x8a57,0x3700,0x0024,0x4080,0x0024, 0x0000,0x0040,0x2800,0xce15,0x0006,0xa2d7,0x3009,0x3c00, 0x0006,0xa157,0x3009,0x1c00,0x0006,0x01d7,0x0000,0x190d, 0x000a,0x708f,0x0000,0xd70e,0x290b,0x1a80,0x3f00,0x184c, 0x0030,0x0017,0x4080,0x1c01,0x0000,0x0200,0x2800,0xca55, 0xb102,0x0024,0x0000,0xcc48,0x2800,0xca55,0x0000,0xd30e, 0x0011,0x210f,0x0000,0x190d,0x280f,0xcb00,0x3613,0x0024, 0x0006,0xa115,0x0006,0x01d7,0x37f0,0x1401,0x6100,0x1c01, 0x4012,0x0024,0x0000,0x8000,0x6010,0x0024,0x34f3,0x0400, 0x2800,0xd5d8,0x0000,0x0024,0x0000,0x8001,0x6010,0x3c01, 0x0000,0x000d,0x2811,0x8259,0x0000,0x0024,0x2a11,0x2100, 0x0030,0x0257,0x3700,0x0024,0x4080,0x0024,0x0000,0x0024, 0x2800,0xd915,0x0006,0x0197,0x0006,0xa115,0x3f00,0x3400, 0x003f,0xc000,0xb600,0x41c1,0x0012,0x5103,0x000c,0xc002, 0xdcd6,0x0024,0x0019,0xd4c2,0x2800,0x9745,0x0001,0x0f48, 0x0013,0xd9c3,0x6fd6,0x0024,0x0000,0x190d,0x2800,0xded5, 0x0014,0x1b01,0x0020,0x480f,0x0000,0xdd8e,0x0000,0x190d, 0x2820,0x41c0,0x0001,0x0f48,0x0039,0x324f,0x0001,0x3c4e, 0x2820,0x4a18,0xb882,0x0024,0x2a20,0x48c0,0x003f,0xfd00, 0xb700,0x0024,0x003f,0xf901,0x6010,0x0024,0x0000,0x0024, 0x280a,0xc505,0x0000,0x190d,0x0014,0x1b01,0x0015,0x59c0, 0x6fc2,0x0024,0x0000,0x0024,0x2800,0xe915,0x0000,0x0024, 0x290c,0x4840,0x3613,0x0024,0x290c,0x4840,0x4086,0x184c, 0x0000,0x18c2,0x6234,0x0024,0x0000,0x1d02,0x2800,0xe515, 0x6234,0x0024,0x0030,0x0317,0x2800,0xe900,0x3f00,0x0024, 0x0000,0x1d82,0x2800,0xe795,0x6234,0x0024,0x2912,0x0d00, 0x4084,0x184c,0xf200,0x0024,0x6200,0x0024,0x0006,0x0017, 0x2800,0xe480,0xb080,0x3c40,0x0000,0x0202,0x2800,0xe915, 0xa024,0x0024,0xc020,0x0024,0x2800,0xe480,0x0030,0x02d7, 0x000a,0x8c8f,0x0000,0xea4e,0x000c,0x0981,0x280a,0x71c0, 0x002c,0x9d40,0x000a,0x708f,0x0000,0xd70e,0x280a,0xc0d5, 0x0012,0x5182,0x6fd6,0x0024,0x003f,0xfd81,0x280a,0x8e45, 0xb710,0x0024,0xb710,0x0024,0x003f,0xfc01,0x6012,0x0024, 0x0000,0x0101,0x2801,0x0615,0xffd2,0x0024,0x48b2,0x0024, 0x4190,0x0024,0x0000,0x190d,0x2801,0x0615,0x0030,0x0250, 0xb880,0x104c,0x3cf0,0x0024,0x0010,0x5500,0xb880,0x23c0, 0xb882,0x2000,0x0007,0x8590,0x2914,0xbec0,0x0000,0x0440, 0x0007,0x8b50,0xb880,0x0024,0x2920,0x0100,0x3800,0x0024, 0x2920,0x0000,0x0006,0x8a91,0x0000,0x0800,0xb880,0xa440, 0x003f,0xfd81,0xb710,0xa7c0,0x003f,0xfc01,0x6012,0x0024, 0x0000,0x0101,0x2801,0x0f55,0x0000,0x0024,0xffe2,0x0024, 0x48b2,0x0024,0x4190,0x0024,0x0000,0x0024,0x2801,0x0f55, 0x0000,0x0024,0x2912,0x2d80,0x0000,0x0780,0x4080,0x0024, 0x0006,0x8a90,0x2801,0x0f55,0x0000,0x01c2,0xb886,0x8040, 0x3613,0x03c1,0xbcd2,0x0024,0x0030,0x0011,0x2800,0xfbd5, 0x003f,0xff42,0xb886,0x8040,0x3009,0x03c1,0x0000,0x0020, 0xac22,0x0024,0x0000,0x0102,0x6cd2,0x0024,0x3e10,0x0024, 0x2909,0x8c80,0x3e00,0x4024,0x36f3,0x0024,0x3e11,0x8024, 0x3e01,0xc024,0x2901,0x3300,0x0000,0x0201,0xf400,0x4512, 0x2900,0x0c80,0x3213,0x1b8c,0x3100,0x0024,0xb010,0x0024, 0x0000,0x0024,0x2801,0x0f55,0x0000,0x0024,0x291a,0x8a40, 0x0000,0x0100,0x2920,0x0200,0x3633,0x0024,0x2920,0x0280, 0x0000,0x0401,0x408e,0x0024,0x2920,0x0280,0x0000,0x0401, 0x003f,0xfd81,0xb710,0x4006,0x003f,0xfc01,0x6012,0x0024, 0x0000,0x0101,0x2801,0x0f55,0x0000,0x0024,0xffe2,0x0024, 0x48b2,0x0024,0x4190,0x0024,0x0000,0x0024,0x2801,0x0f55, 0x0000,0x0024,0x2912,0x2d80,0x0000,0x0780,0x4080,0x0024, 0x0000,0x01c2,0x2800,0xf7c5,0x0006,0x8a90,0x2a01,0x0f40, 0x2920,0x0100,0x0000,0x0401,0x0000,0x0180,0x2920,0x0200, 0x3613,0x0024,0x2920,0x0280,0x3613,0x0024,0x0000,0x0401, 0x2920,0x0280,0x4084,0x984c,0x0019,0x9d01,0x6212,0x0024, 0x001e,0x5c01,0x2801,0x0a95,0x6012,0x0024,0x0000,0x0024, 0x2801,0x0c85,0x0000,0x0024,0x001b,0x5bc1,0x6212,0x0024, 0x001b,0xdd81,0x2801,0x1055,0x6012,0x0024,0x0000,0x0024, 0x2801,0x1055,0x0000,0x0024,0x0000,0x004d,0x000a,0xbf4f, 0x280a,0xb880,0x0001,0x0d8e,0x0020,0xfb4f,0x0000,0x190d, 0x0001,0x148e,0x2920,0x0480,0x3009,0x2bc1,0x291a,0x8a40, 0x36e3,0x0024,0x0000,0x190d,0x000a,0x708f,0x280a,0xcac0, 0x0000,0xd70e,0x0030,0x0017,0x3700,0x4024,0x0000,0x0200, 0xb102,0x0024,0x0000,0x00c0,0x2801,0x1385,0x0005,0x4f92, 0x2909,0xf840,0x3613,0x2800,0x0006,0x0197,0x0006,0xa115, 0xb080,0x0024,0x3f00,0x3400,0x0000,0x190d,0x000a,0x708f, 0x280a,0xc0c0,0x0000,0xd70e,0x0000,0x004d,0x0020,0xfe0f, 0x2820,0xfb40,0x0001,0x158e,0x2801,0x1755,0x3009,0x1000, 0x6012,0x93cc,0x0000,0x0024,0x2801,0x3205,0x0000,0x0024, 0x3413,0x0024,0x34b0,0x0024,0x4080,0x0024,0x0000,0x0200, 0x2801,0x1a55,0xb882,0x0024,0x3453,0x0024,0x3009,0x13c0, 0x4080,0x0024,0x0000,0x0200,0x2801,0x3205,0x0000,0x0024, 0xb882,0x130c,0x0000,0x004d,0x0021,0x058f,0x2821,0x0340, 0x0001,0x1b4e,0x2801,0x2b95,0x6012,0x0024,0x0000,0x0024, 0x2801,0x2b95,0x0000,0x0024,0x34c3,0x184c,0x3e13,0xb80f, 0xf400,0x4500,0x0026,0x9dcf,0x0001,0x1f4e,0x0000,0xfa0d, 0x2926,0x8e80,0x3e10,0x110c,0x36f3,0x0024,0x2801,0x2b80, 0x36f3,0x980f,0x001c,0xdd00,0x001c,0xd901,0x6ec2,0x0024, 0x001c,0xdd00,0x2801,0x2255,0x0018,0xdbc1,0x3413,0x184c, 0xf400,0x4500,0x2926,0xc640,0x3e00,0x13cc,0x2801,0x2940, 0x36f3,0x0024,0x6ec2,0x0024,0x003f,0xc000,0x2801,0x24d5, 0x002a,0x4001,0x3413,0x184c,0xf400,0x4500,0x2926,0xafc0, 0x3e00,0x13cc,0x2801,0x2940,0x36f3,0x0024,0xb400,0x0024, 0xd100,0x0024,0x0000,0x0024,0x2801,0x2945,0x0000,0x0024, 0x3613,0x0024,0x3e11,0x4024,0x2926,0x8540,0x3e01,0x0024, 0x4080,0x1b8c,0x0000,0x0024,0x2801,0x2945,0x0000,0x0024, 0x3413,0x184c,0xf400,0x4500,0x2926,0x8e80,0x3e10,0x13cc, 0x36f3,0x0024,0x3110,0x8024,0x31f0,0xc024,0x0000,0x4000, 0x0000,0x0021,0x6d06,0x0024,0x3110,0x8024,0x2826,0xa8c4, 0x31f0,0xc024,0x2a26,0xad00,0x34c3,0x184c,0x3410,0x8024, 0x3430,0xc024,0x0000,0x4000,0x0000,0x0021,0x6d06,0x0024, 0x0000,0x0024,0x2801,0x3214,0x4d06,0x0024,0x0000,0x0200, 0x2922,0x1885,0x0001,0x3088,0x0000,0x0200,0x3e10,0x8024, 0x2921,0xca80,0x3e00,0xc024,0x291a,0x8a40,0x0000,0x0024, 0x2922,0x1880,0x36f3,0x0024,0x0000,0x004d,0x0021,0x0ecf, 0x2821,0x0bc0,0x0001,0x318e,0x2801,0x1480,0x3c30,0x4024, 0x0000,0x190d,0x0000,0x464e,0x2821,0x0f80,0x0027,0x9e0f, 0x0020,0xcd4f,0x2820,0xc780,0x0001,0x33ce,0x0006,0xf017, 0x0000,0x0015,0xb070,0xbc15,0x0000,0x464e,0x0027,0x9e0f, 0x2820,0xcd80,0x0000,0x190d,0x3613,0x0024,0x3e10,0xb803, 0x3e14,0x3811,0x3e11,0x3805,0x3e00,0x3801,0x0007,0xc390, 0x0006,0xa011,0x3010,0x0444,0x3050,0x4405,0x6458,0x0302, 0xff94,0x4081,0x0003,0xffc5,0x48b6,0x0024,0xff82,0x0024, 0x42b2,0x0042,0xb458,0x0003,0x4cd6,0x9801,0xf248,0x1bc0, 0xb58a,0x0024,0x6de6,0x1804,0x0006,0x0010,0x3810,0x9bc5, 0x3800,0xc024,0x36f4,0x1811,0x36f0,0x9803,0x283e,0x2d80, 0x0fff,0xffc3,0x2801,0x4a00,0x0000,0x0024,0x3413,0x0024, 0x2801,0x3e05,0xf400,0x4517,0x2801,0x4200,0x6894,0x13cc, 0x37b0,0x184c,0x6090,0x1d51,0x0000,0x0910,0x3f00,0x060c, 0x3100,0x4024,0x6016,0xb812,0x000c,0x8012,0x2801,0x4091, 0xb884,0x0024,0x6894,0x3002,0x0000,0x028d,0x003a,0x5e0f, 0x0001,0x520e,0x2939,0xb0c0,0x3e10,0x93cc,0x4084,0x9bd2, 0x4282,0x0024,0x0000,0x0040,0x2801,0x4405,0x4292,0x130c, 0x3443,0x0024,0x2801,0x4545,0x000c,0x8390,0x2a01,0x48c0, 0x3444,0x0024,0x3073,0x0024,0xc090,0x014c,0x2801,0x48c0, 0x3800,0x0024,0x000c,0x4113,0xb880,0x2380,0x3304,0x4024, 0x3800,0x05cc,0xcc92,0x05cc,0x3910,0x0024,0x3910,0x4024, 0x000c,0x8110,0x3910,0x0024,0x39f0,0x4024,0x3810,0x0024, 0x38d0,0x4024,0x3810,0x0024,0x38f0,0x4024,0x34c3,0x0024, 0x3444,0x0024,0x3073,0x0024,0x3063,0x0024,0x3000,0x0024, 0x4080,0x0024,0x0000,0x0024,0x2839,0x53d5,0x4284,0x0024, 0x3613,0x0024,0x2801,0x4c05,0x6898,0xb804,0x0000,0x0084, 0x293b,0x1cc0,0x3613,0x0024,0x000c,0x8117,0x3711,0x0024, 0x37d1,0x4024,0x4e8a,0x0024,0x0000,0x0015,0x2801,0x4ec5, 0xce9a,0x0024,0x3f11,0x0024,0x3f01,0x4024,0x000c,0x8197, 0x408a,0x9bc4,0x3f15,0x4024,0x2801,0x5105,0x4284,0x3c15, 0x6590,0x0024,0x0000,0x0024,0x2839,0x53d5,0x4284,0x0024, 0x0000,0x0024,0x2801,0x3cd8,0x458a,0x0024,0x2a39,0x53c0, 0x003e,0x2d4f,0x283a,0x5ed5,0x0001,0x358e,0x000c,0x4653, 0x0000,0x0246,0xffac,0x0c01,0x48be,0x0024,0x4162,0x4546, 0x6642,0x4055,0x3501,0x8024,0x0000,0x0087,0x667c,0x4057, 0x000c,0x41d5,0x283a,0x62d5,0x3501,0x8024,0x667c,0x1c47, 0x3701,0x8024,0x283a,0x62d5,0xc67c,0x0024,0x0000,0x0024, 0x283a,0x62c5,0x0000,0x0024,0x2a3a,0x5ec0,0x3009,0x3851, 0x3e14,0xf812,0x3e12,0xb817,0x3e11,0x8024,0x0006,0x0293, 0x3301,0x8024,0x468c,0x3804,0x0006,0xa057,0x2801,0x5e04, 0x0006,0x0011,0x469c,0x0024,0x3be1,0x8024,0x2801,0x5e15, 0x0006,0xc392,0x3311,0x0024,0x33f1,0x2844,0x3009,0x2bc4, 0x0030,0x04d2,0x3311,0x0024,0x3a11,0x0024,0x3201,0x8024, 0x003f,0xfc04,0xb64c,0x0fc4,0xc648,0x0024,0x3a01,0x0024, 0x3111,0x1fd3,0x6498,0x07c6,0x868c,0x2444,0x0023,0xffd2, 0x3901,0x8e06,0x0030,0x0551,0x3911,0x8e06,0x3961,0x9c44, 0xf400,0x44c6,0xd46c,0x1bc4,0x36f1,0xbc13,0x2801,0x6795, 0x36f2,0x9817,0x002b,0xffd2,0x3383,0x188c,0x3e01,0x8c06, 0x0006,0xa097,0x3009,0x1c12,0x3213,0x0024,0x468c,0xbc12, 0x002b,0xffd2,0xf400,0x4197,0x2801,0x6484,0x3713,0x0024, 0x2801,0x64c5,0x37e3,0x0024,0x3009,0x2c17,0x3383,0x0024, 0x3009,0x0c06,0x468c,0x4197,0x0006,0xa052,0x2801,0x66c4, 0x3713,0x2813,0x2801,0x6705,0x37e3,0x0024,0x3009,0x2c17, 0x36f1,0x8024,0x36f2,0x9817,0x36f4,0xd812,0x2100,0x0000, 0x3904,0x5bd1,0x2a01,0x57ce,0x3e11,0x7804,0x0030,0x0257, 0x3701,0x0024,0x0013,0x4d05,0xd45b,0xe0e1,0x0007,0xc795, 0x2801,0x6f15,0x0fff,0xff45,0x3511,0x184c,0x4488,0xb808, 0x0006,0x8a97,0x2801,0x6ec5,0x3009,0x1c40,0x3511,0x1fc1, 0x0000,0x0020,0xac52,0x1405,0x6ce2,0x0024,0x0000,0x0024, 0x2801,0x6ec1,0x68c2,0x0024,0x291a,0x8a40,0x3e10,0x0024, 0x2921,0xca80,0x3e00,0x4024,0x36f3,0x0024,0x3009,0x1bc8, 0x36f0,0x1801,0x3601,0x5804,0x3e13,0x780f,0x3e13,0xb808, 0x0008,0x9b0f,0x0001,0x71ce,0x2908,0x9300,0x0000,0x004d, 0x36f3,0x9808,0x2000,0x0000,0x36f3,0x580f,0x0007,0x81d7, 0x3711,0x8024,0x3711,0xc024,0x3700,0x0024,0x0000,0x2001, 0xb012,0x0024,0x0034,0x0000,0x2801,0x7485,0x0000,0x01c1, 0x0014,0xc000,0x0000,0x01c1,0x4fce,0x0024,0xffea,0x0024, 0x48b6,0x0024,0x4384,0x4097,0xb886,0x45c6,0xfede,0x0024, 0x4db6,0x0024,0x466c,0x0024,0x0006,0xc610,0x8dd6,0x8007, 0x0000,0x00c6,0xff6e,0x0024,0x48b2,0x0024,0x0034,0x2406, 0xffee,0x0024,0x2914,0xaa80,0x40b2,0x0024,0xf1c6,0x0024, 0xf1d6,0x0024,0x0000,0x0201,0x8d86,0x0024,0x61de,0x0024, 0x0006,0xc612,0x2801,0x7b01,0x0006,0xc713,0x4c86,0x0024, 0x2912,0x1180,0x0006,0xc351,0x0006,0x0210,0x2912,0x0d00, 0x3810,0x984c,0xf200,0x2043,0x2808,0xa000,0x3800,0x0024, 0x0007,0x0001, /copy 1/ 0x802e, 0x0006,0x0002, /copy 2/ 0x2801,0x6880, 0x0007,0x0001, /copy 1/ 0x8030, 0x0006,0x0002, /copy 2/ 0x2800,0x1b40, 0x0007,0x0001, /copy 1/ 0x8028, 0x0006,0x0002, /copy 2/ 0x2a00,0x42ce, 0x0007,0x0001, /copy 1/ 0x8032, 0x0006,0x0002, /copy 2/ 0x2800,0x6040, 0x0007,0x0001, /copy 1/ 0x3580, 0x0006, 0x8038, 0x0000, /Rle(56)/ 0x0007,0x0001, /copy 1/ 0xfab3, 0x0006,0x01a4, /copy 420/ 0x0001,0x0001,0x0001,0x0001,0x0000,0xffff,0xfffe,0xfffb, 0xfff9,0xfff5,0xfff2,0xffed,0xffe8,0xffe3,0xffde,0xffd8, 0xffd3,0xffce,0xffca,0xffc7,0xffc4,0xffc4,0xffc5,0xffc7, 0xffcc,0xffd3,0xffdc,0xffe6,0xfff3,0x0001,0x0010,0x001f, 0x002f,0x003f,0x004e,0x005b,0x0066,0x006f,0x0074,0x0075, 0x0072,0x006b,0x005f,0x004f,0x003c,0x0024,0x0009,0xffed, 0xffcf,0xffb0,0xff93,0xff77,0xff5f,0xff4c,0xff3d,0xff35, 0xff34,0xff3b,0xff4a,0xff60,0xff7e,0xffa2,0xffcd,0xfffc, 0x002e,0x0061,0x0094,0x00c4,0x00f0,0x0114,0x0131,0x0144, 0x014b,0x0146,0x0134,0x0116,0x00eb,0x00b5,0x0075,0x002c, 0xffde,0xff8e,0xff3d,0xfeef,0xfea8,0xfe6a,0xfe39,0xfe16, 0xfe05,0xfe06,0xfe1b,0xfe43,0xfe7f,0xfecd,0xff2a,0xff95, 0x0009,0x0082,0x00fd,0x0173,0x01e1,0x0242,0x0292,0x02cc, 0x02ec,0x02f2,0x02da,0x02a5,0x0253,0x01e7,0x0162,0x00c9, 0x0021,0xff70,0xfebc,0xfe0c,0xfd68,0xfcd5,0xfc5b,0xfc00, 0xfbc9,0xfbb8,0xfbd2,0xfc16,0xfc85,0xfd1b,0xfdd6,0xfeae, 0xff9e,0x009c,0x01a0,0x02a1,0x0392,0x046c,0x0523,0x05b0, 0x060a,0x062c,0x0613,0x05bb,0x0526,0x0456,0x0351,0x021f, 0x00c9,0xff5a,0xfde1,0xfc6a,0xfb05,0xf9c0,0xf8aa,0xf7d0, 0xf73d,0xf6fa,0xf70f,0xf77e,0xf848,0xf96b,0xfadf,0xfc9a, 0xfe8f,0x00ad,0x02e3,0x051a,0x073f,0x0939,0x0af4,0x0c5a, 0x0d59,0x0de1,0x0de5,0x0d5c,0x0c44,0x0a9e,0x0870,0x05c7, 0x02b4,0xff4e,0xfbaf,0xf7f8,0xf449,0xf0c7,0xed98,0xeae0, 0xe8c4,0xe765,0xe6e3,0xe756,0xe8d2,0xeb67,0xef19,0xf3e9, 0xf9cd,0x00b5,0x088a,0x112b,0x1a72,0x2435,0x2e42,0x3866, 0x426b,0x4c1b,0x553e,0x5da2,0x6516,0x6b6f,0x7087,0x7441, 0x7686,0x774a,0x7686,0x7441,0x7087,0x6b6f,0x6516,0x5da2, 0x553e,0x4c1b,0x426b,0x3866,0x2e42,0x2435,0x1a72,0x112b, 0x088a,0x00b5,0xf9cd,0xf3e9,0xef19,0xeb67,0xe8d2,0xe756, 0xe6e3,0xe765,0xe8c4,0xeae0,0xed98,0xf0c7,0xf449,0xf7f8, 0xfbaf,0xff4e,0x02b4,0x05c7,0x0870,0x0a9e,0x0c44,0x0d5c, 0x0de5,0x0de1,0x0d59,0x0c5a,0x0af4,0x0939,0x073f,0x051a, 0x02e3,0x00ad,0xfe8f,0xfc9a,0xfadf,0xf96b,0xf848,0xf77e, 0xf70f,0xf6fa,0xf73d,0xf7d0,0xf8aa,0xf9c0,0xfb05,0xfc6a, 0xfde1,0xff5a,0x00c9,0x021f,0x0351,0x0456,0x0526,0x05bb, 0x0613,0x062c,0x060a,0x05b0,0x0523,0x046c,0x0392,0x02a1, 0x01a0,0x009c,0xff9e,0xfeae,0xfdd6,0xfd1b,0xfc85,0xfc16, 0xfbd2,0xfbb8,0xfbc9,0xfc00,0xfc5b,0xfcd5,0xfd68,0xfe0c, 0xfebc,0xff70,0x0021,0x00c9,0x0162,0x01e7,0x0253,0x02a5, 0x02da,0x02f2,0x02ec,0x02cc,0x0292,0x0242,0x01e1,0x0173, 0x00fd,0x0082,0x0009,0xff95,0xff2a,0xfecd,0xfe7f,0xfe43, 0xfe1b,0xfe06,0xfe05,0xfe16,0xfe39,0xfe6a,0xfea8,0xfeef, 0xff3d,0xff8e,0xffde,0x002c,0x0075,0x00b5,0x00eb,0x0116, 0x0134,0x0146,0x014b,0x0144,0x0131,0x0114,0x00f0,0x00c4, 0x0094,0x0061,0x002e,0xfffc,0xffcd,0xffa2,0xff7e,0xff60, 0xff4a,0xff3b,0xff34,0xff35,0xff3d,0xff4c,0xff5f,0xff77, 0xff93,0xffb0,0xffcf,0xffed,0x0009,0x0024,0x003c,0x004f, 0x005f,0x006b,0x0072,0x0075,0x0074,0x006f,0x0066,0x005b, 0x004e,0x003f,0x002f,0x001f,0x0010,0x0001,0xfff3,0xffe6, 0xffdc,0xffd3,0xffcc,0xffc7,0xffc5,0xffc4,0xffc4,0xffc7, 0xffca,0xffce,0xffd3,0xffd8,0xffde,0xffe3,0xffe8,0xffed, 0xfff2,0xfff5,0xfff9,0xfffb,0xfffe,0xffff,0x0000,0x0001, 0x0001,0x0001,0x0001,0x0000, 0x0007,0x0001, /copy 1/ 0x180b, 0x0006,0x000b, /copy 11/ 0x000f,0x0010,0x001c,0xfab3,0x3580,0x8037,0xa037,0x0001, 0x0000,0x3580,0x01a4, 0x000a,0x0001, /copy 1/ 0x0300,

define PATCH_SIZE 3201

};

const unsigned short analizer[1000] PROGMEM = { / Compressed plugin / 0x0007, 0x0001, 0x8d00, 0x0006, 0x0004, 0x2803, 0x5b40, 0x0000, / 0 / 0x0024, 0x0007, 0x0001, 0x8d02, 0x0006, 0x00d6, 0x3e12, 0xb817, / 8 / 0x3e12, 0x3815, 0x3e05, 0xb814, 0x3615, 0x0024, 0x0000, 0x800a, / 10 / 0x3e10, 0x3801, 0x0006, 0x0800, 0x3e10, 0xb803, 0x0000, 0x0303, / 18 / 0x3e11, 0x3805, 0x3e11, 0xb807, 0x3e14, 0x3812, 0xb884, 0x130c, / 20 / 0x3410, 0x4024, 0x4112, 0x10d0, 0x4010, 0x008c, 0x4010, 0x0024, / 28 / 0xf400, 0x4012, 0x3000, 0x3840, 0x3009, 0x3801, 0x0000, 0x0041, / 30 / 0xfe02, 0x0024, 0x2903, 0xb480, 0x48b2, 0x0024, 0x36f3, 0x0844, / 38 / 0x6306, 0x8845, 0xae3a, 0x8840, 0xbf8e, 0x8b41, 0xac32, 0xa846, / 40 / 0xffc8, 0xabc7, 0x3e01, 0x7800, 0xf400, 0x4480, 0x6090, 0x0024, / 48 / 0x6090, 0x0024, 0xf400, 0x4015, 0x3009, 0x3446, 0x3009, 0x37c7, / 50 / 0x3009, 0x1800, 0x3009, 0x3844, 0x48b3, 0xe1e0, 0x4882, 0x4040, / 58 / 0xfeca, 0x0024, 0x5ac2, 0x0024, 0x5a52, 0x0024, 0x4cc2, 0x0024, / 60 / 0x48ba, 0x4040, 0x4eea, 0x4801, 0x4eca, 0x9800, 0xff80, 0x1bc1, / 68 / 0xf1eb, 0xe3e2, 0xf1ea, 0x184c, 0x4c8b, 0xe5e4, 0x48be, 0x9804, / 70 / 0x488e, 0x41c6, 0xfe82, 0x0024, 0x5a8e, 0x0024, 0x525e, 0x1b85, / 78 / 0x4ffe, 0x0024, 0x48b6, 0x41c6, 0x4dd6, 0x48c7, 0x4df6, 0x0024, / 80 / 0xf1d6, 0x0024, 0xf1d6, 0x0024, 0x4eda, 0x0024, 0x0000, 0x0fc3, / 88 / 0x2903, 0xb480, 0x4e82, 0x0024, 0x4084, 0x130c, 0x0006, 0x0500, / 90 / 0x3440, 0x4024, 0x4010, 0x0024, 0xf400, 0x4012, 0x3200, 0x4024, / 98 / 0xb132, 0x0024, 0x4214, 0x0024, 0xf224, 0x0024, 0x6230, 0x0024, / a0 / 0x0001, 0x0001, 0x2803, 0x54c9, 0x0000, 0x0024, 0xf400, 0x40c2, / a8 / 0x3200, 0x0024, 0xff82, 0x0024, 0x48b2, 0x0024, 0xb130, 0x0024, / b0 / 0x6202, 0x0024, 0x003f, 0xf001, 0x2803, 0x57d1, 0x0000, 0x1046, / b8 / 0xfe64, 0x0024, 0x48be, 0x0024, 0x2803, 0x58c0, 0x3a01, 0x8024, / c0 / 0x3200, 0x0024, 0xb010, 0x0024, 0xc020, 0x0024, 0x3a00, 0x0024, / c8 / 0x36f4, 0x1812, 0x36f1, 0x9807, 0x36f1, 0x1805, 0x36f0, 0x9803, / d0 / 0x36f0, 0x1801, 0x3405, 0x9014, 0x36f3, 0x0024, 0x36f2, 0x1815, / d8 / 0x2000, 0x0000, 0x36f2, 0x9817, 0x0007, 0x0001, 0x8d6d, 0x0006, / e0 / 0x01f6, 0x3613, 0x0024, 0x3e12, 0xb817, 0x3e12, 0x3815, 0x3e05, / e8 / 0xb814, 0x3645, 0x0024, 0x0000, 0x800a, 0x3e10, 0xb803, 0x3e11, / f0 / 0x3805, 0x3e11, 0xb811, 0x3e14, 0xb813, 0x3e13, 0xf80e, 0x4182, / f8 / 0x384d, 0x0006, 0x0912, 0x2803, 0x6105, 0x0006, 0x0451, 0x0006, / 100 / 0xc352, 0x3100, 0x8803, 0x6238, 0x1bcc, 0x0000, 0x0024, 0x2803, / 108 / 0x7705, 0x4194, 0x0024, 0x0006, 0x0912, 0x3613, 0x0024, 0x0006, / 110 / 0x0411, 0x0000, 0x0302, 0x3009, 0x3850, 0x0006, 0x0410, 0x3009, / 118 / 0x3840, 0x0000, 0x1100, 0x2914, 0xbec0, 0xb882, 0xb801, 0x0000, / 120 / 0x1000, 0x0006, 0x0810, 0x2915, 0x7ac0, 0xb882, 0x0024, 0x3900, / 128 / 0x9bc1, 0x0006, 0xc351, 0x3009, 0x1bc0, 0x3009, 0x1bd0, 0x3009, / 130 / 0x0404, 0x0006, 0x0451, 0x2803, 0x66c0, 0x3901, 0x0024, 0x4448, / 138 / 0x0402, 0x4294, 0x0024, 0x6498, 0x2402, 0x001f, 0x4002, 0x6424, / 140 / 0x0024, 0x0006, 0x0411, 0x2803, 0x6611, 0x0000, 0x03ce, 0x2403, / 148 / 0x764e, 0x0000, 0x0013, 0x0006, 0x1a04, 0x0006, 0x0451, 0x3100, / 150 / 0x8024, 0xf224, 0x44c5, 0x4458, 0x0024, 0xf400, 0x4115, 0x3500, / 158 / 0xc024, 0x623c, 0x0024, 0x0000, 0x0024, 0x2803, 0x7691, 0x0000, / 160 / 0x0024, 0x4384, 0x184c, 0x3100, 0x3800, 0x2915, 0x7dc0, 0xf200, / 168 / 0x0024, 0x003f, 0xfec3, 0x4084, 0x4491, 0x3113, 0x1bc0, 0xa234, / 170 / 0x0024, 0x0000, 0x2003, 0x6236, 0x2402, 0x0000, 0x1003, 0x2803, / 178 / 0x6fc8, 0x0000, 0x0024, 0x003f, 0xf803, 0x3100, 0x8024, 0xb236, / 180 / 0x0024, 0x2803, 0x75c0, 0x3900, 0xc024, 0x6236, 0x0024, 0x0000, / 188 / 0x0803, 0x2803, 0x7208, 0x0000, 0x0024, 0x003f, 0xfe03, 0x3100, / 190 / 0x8024, 0xb236, 0x0024, 0x2803, 0x75c0, 0x3900, 0xc024, 0x6236, / 198 / 0x0024, 0x0000, 0x0403, 0x2803, 0x7448, 0x0000, 0x0024, 0x003f, / 1a0 / 0xff03, 0x3100, 0x8024, 0xb236, 0x0024, 0x2803, 0x75c0, 0x3900, / 1a8 / 0xc024, 0x6236, 0x0402, 0x003f, 0xff83, 0x2803, 0x75c8, 0x0000, / 1b0 / 0x0024, 0xb236, 0x0024, 0x3900, 0xc024, 0xb884, 0x07cc, 0x3900, / 1b8 / 0x88cc, 0x3313, 0x0024, 0x0006, 0x0491, 0x4194, 0x2413, 0x0006, / 1c0 / 0x04d1, 0x2803, 0x9755, 0x0006, 0x0902, 0x3423, 0x0024, 0x3c10, / 1c8 / 0x8024, 0x3100, 0xc024, 0x4304, 0x0024, 0x39f0, 0x8024, 0x3100, / 1d0 / 0x8024, 0x3cf0, 0x8024, 0x0006, 0x0902, 0xb884, 0x33c2, 0x3c20, / 1d8 / 0x8024, 0x34d0, 0xc024, 0x6238, 0x0024, 0x0000, 0x0024, 0x2803, / 1e0 / 0x8dd8, 0x4396, 0x0024, 0x2403, 0x8d83, 0x0000, 0x0024, 0x3423, / 1e8 / 0x0024, 0x34e4, 0x4024, 0x3123, 0x0024, 0x3100, 0xc024, 0x4304, / 1f0 / 0x0024, 0x4284, 0x2402, 0x0000, 0x2003, 0x2803, 0x8b89, 0x0000, / 1f8 / 0x0024, 0x3423, 0x184c, 0x34f4, 0x4024, 0x3004, 0x844c, 0x3100, / 200 / 0xb850, 0x6236, 0x0024, 0x0006, 0x0802, 0x2803, 0x81c8, 0x4088, / 208 / 0x1043, 0x4336, 0x1390, 0x4234, 0x0024, 0x4234, 0x0024, 0xf400, / 210 / 0x4091, 0x2903, 0xa480, 0x0003, 0x8308, 0x4336, 0x1390, 0x4234, / 218 / 0x0024, 0x4234, 0x0024, 0x2903, 0x9a00, 0xf400, 0x4091, 0x0004, / 220 / 0x0003, 0x3423, 0x1bd0, 0x3404, 0x4024, 0x3123, 0x0024, 0x3100, / 228 / 0x8024, 0x6236, 0x0024, 0x0000, 0x4003, 0x2803, 0x85c8, 0x0000, / 230 / 0x0024, 0xb884, 0x878c, 0x3900, 0x8024, 0x34e4, 0x4024, 0x3123, / 238 / 0x0024, 0x31e0, 0x8024, 0x6236, 0x0402, 0x0000, 0x0024, 0x2803, / 240 / 0x8b88, 0x4284, 0x0024, 0x0000, 0x0024, 0x2803, 0x8b95, 0x0000, / 248 / 0x0024, 0x3413, 0x184c, 0x3410, 0x8024, 0x3e10, 0x8024, 0x34e0, / 250 / 0xc024, 0x2903, 0x4080, 0x3e10, 0xc024, 0xf400, 0x40d1, 0x003f, / 258 / 0xff44, 0x36e3, 0x048c, 0x3100, 0x8024, 0xfe44, 0x0024, 0x48ba, / 260 / 0x0024, 0x3901, 0x0024, 0x0000, 0x00c3, 0x3423, 0x0024, 0xf400, / 268 / 0x4511, 0x34e0, 0x8024, 0x4234, 0x0024, 0x39f0, 0x8024, 0x3100, / 270 / 0x8024, 0x6294, 0x0024, 0x3900, 0x8024, 0x0006, 0x0411, 0x6894, / 278 / 0x04c3, 0xa234, 0x0403, 0x6238, 0x0024, 0x0000, 0x0024, 0x2803, / 280 / 0x9741, 0x0000, 0x0024, 0xb884, 0x90cc, 0x39f0, 0x8024, 0x3100, / 288 / 0x8024, 0xb884, 0x3382, 0x3c20, 0x8024, 0x34d0, 0xc024, 0x6238, / 290 / 0x0024, 0x0006, 0x0512, 0x2803, 0x9758, 0x4396, 0x0024, 0x2403, / 298 / 0x9703, 0x0000, 0x0024, 0x0003, 0xf002, 0x3201, 0x0024, 0xb424, / 2a0 / 0x0024, 0x0028, 0x0002, 0x2803, 0x9605, 0x6246, 0x0024, 0x0004, / 2a8 / 0x0003, 0x2803, 0x95c1, 0x4434, 0x0024, 0x0000, 0x1003, 0x6434, / 2b0 / 0x0024, 0x2803, 0x9600, 0x3a00, 0x8024, 0x3a00, 0x8024, 0x3213, / 2b8 / 0x104c, 0xf400, 0x4511, 0x34f0, 0x8024, 0x6294, 0x0024, 0x3900, / 2c0 / 0x8024, 0x36f3, 0x4024, 0x36f3, 0xd80e, 0x36f4, 0x9813, 0x36f1, / 2c8 / 0x9811, 0x36f1, 0x1805, 0x36f0, 0x9803, 0x3405, 0x9014, 0x36f3, / 2d0 / 0x0024, 0x36f2, 0x1815, 0x2000, 0x0000, 0x36f2, 0x9817, 0x0007, / 2d8 / 0x0001, 0x1868, 0x0006, 0x0010, 0x0032, 0x004f, 0x007e, 0x00c8, / 2e0 / 0x013d, 0x01f8, 0x0320, 0x04f6, 0x07e0, 0x0c80, 0x13d8, 0x1f7f, / 2e8 / 0x3200, 0x4f5f, 0x61a8, 0x0000, 0x0007, 0x0001, 0x8e68, 0x0006, / 2f0 / 0x0054, 0x3e12, 0xb814, 0x0000, 0x800a, 0x3e10, 0x3801, 0x3e10, / 2f8 / 0xb803, 0x3e11, 0x7806, 0x3e11, 0xf813, 0x3e13, 0xf80e, 0x3e13, / 300 / 0x4024, 0x3e04, 0x7810, 0x449a, 0x0040, 0x0001, 0x0003, 0x2803, / 308 / 0xa344, 0x4036, 0x03c1, 0x0003, 0xffc2, 0xb326, 0x0024, 0x0018, / 310 / 0x0042, 0x4326, 0x4495, 0x4024, 0x40d2, 0x0000, 0x0180, 0xa100, / 318 / 0x4090, 0x0010, 0x0fc2, 0x4204, 0x0024, 0xbc82, 0x4091, 0x459a, / 320 / 0x0024, 0x0000, 0x0054, 0x2803, 0xa244, 0xbd86, 0x4093, 0x2403, / 328 / 0xa205, 0xfe01, 0x5e0c, 0x5c43, 0x5f2d, 0x5e46, 0x020c, 0x5c56, / 330 / 0x8a0c, 0x5e53, 0x5e0c, 0x5c43, 0x5f2d, 0x5e46, 0x020c, 0x5c56, / 338 / 0x8a0c, 0x5e52, 0x0024, 0x4cb2, 0x4405, 0x0018, 0x0044, 0x654a, / 340 / 0x0024, 0x2803, 0xb040, 0x36f4, 0x5810, 0x0007, 0x0001, 0x8e92, / 348 / 0x0006, 0x0080, 0x3e12, 0xb814, 0x0000, 0x800a, 0x3e10, 0x3801, / 350 / 0x3e10, 0xb803, 0x3e11, 0x7806, 0x3e11, 0xf813, 0x3e13, 0xf80e, / 358 / 0x3e13, 0x4024, 0x3e04, 0x7810, 0x449a, 0x0040, 0x0000, 0x0803, / 360 / 0x2803, 0xaf04, 0x30f0, 0x4024, 0x0fff, 0xfec2, 0xa020, 0x0024, / 368 / 0x0fff, 0xff02, 0xa122, 0x0024, 0x4036, 0x0024, 0x0000, 0x1fc2, / 370 / 0xb326, 0x0024, 0x0010, 0x4002, 0x4326, 0x4495, 0x4024, 0x40d2, / 378 / 0x0000, 0x0180, 0xa100, 0x4090, 0x0010, 0x0042, 0x4204, 0x0024, / 380 / 0xbc82, 0x4091, 0x459a, 0x0024, 0x0000, 0x0054, 0x2803, 0xae04, / 388 / 0xbd86, 0x4093, 0x2403, 0xadc5, 0xfe01, 0x5e0c, 0x5c43, 0x5f2d, / 390 / 0x5e46, 0x0024, 0x5c56, 0x0024, 0x5e53, 0x5e0c, 0x5c43, 0x5f2d, / 398 / 0x5e46, 0x0024, 0x5c56, 0x0024, 0x5e52, 0x0024, 0x4cb2, 0x4405, / 3a0 / 0x0010, 0x4004, 0x654a, 0x9810, 0x0000, 0x0144, 0xa54a, 0x1bd1, / 3a8 / 0x0006, 0x0413, 0x3301, 0xc444, 0x687e, 0x2005, 0xad76, 0x8445, / 3b0 / 0x4ed6, 0x8784, 0x36f3, 0x64c2, 0xac72, 0x8785, 0x4ec2, 0xa443, / 3b8 / 0x3009, 0x2440, 0x3009, 0x2741, 0x36f3, 0xd80e, 0x36f1, 0xd813, / 3c0 / 0x36f1, 0x5806, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x2000, 0x0000, / 3c8 / 0x36f2, 0x9814, 0x0007, 0x0001, 0x8ed2, 0x0006, 0x000e, 0x4c82, / 3d0 / 0x0024, 0x0000, 0x0024, 0x2000, 0x0005, 0xf5c2, 0x0024, 0x0000, / 3d8 / 0x0980, 0x2000, 0x0000, 0x6010, 0x0024, 0x000a, 0x0001, 0x0d00,

define ANALIZER_SIZE 1000

};

// FINE PATCHES
fletsche commented 5 years ago

@MirkoDalmonte Hi Mirko, thank you for sharing your code with the community. However I think posting the whole source code into this discussion is maybe not the best way to do that. I also tried to add some functionality to this project and found it useful to do so by using the tools Github provides for this. Although it took me (and still takes me) some time to understand how it works, I think its worthwhile to read/watch some tutorial and then publish your code via your own (cloned) repository and probably via a pull request.

MirkoDalmonte commented 5 years ago

Hi, I don't have time to do that, sorry...

koskee commented 5 years ago

@MirkoDalmonte Ohh excellent! Looks like it was my turn getting caught with my head in the clouds, as I only now noticed your response.. So a big ( and very delayed) thank you. I will give it a try and see how it goes. Cheers mate :+1:

ememem13 commented 5 years ago

@koskee Was the spectrum analyzer uploaded? I get many error messages.

gcharles81 commented 4 years ago

Hello I would like to know if anyone managed to use the pasted spectrum analyzer code , I cant compile it Thanks

robrob73 commented 4 years ago

Super project ESP Radio&Spectrum analyzer.... It is very sad to waste such great project and don't continue with it. Im not coder, but I very appreciate man, who share his work. Spectrum analyzer very like me but I get many error too.

blotfi commented 4 years ago

I just adapted and updated the code to have Spectrum analyzer https://github.com/blotfi/ESP32-Radio-with-Spectrum-analyzer

Enjoy programming under CLion / PlateformeIO
Check my video: https://www.youtube.com/watch?v=HP0uNj6u15I that explain that, but you can also use my code with Arduino IDE.

MirkoDalmonte commented 4 years ago

Wowwww!!! Great job!!!! Thank you!

Mirko

Il gio 28 mag 2020, 13:10 Lotfi notifications@github.com ha scritto:

I just adapted and updated the code to have Spectrum analyzer https://github.com/blotfi/ESP32-Radio-with-Spectrum-analyzer Enjoy

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Edzelf/ESP32-Radio/issues/86#issuecomment-635274907, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHBIZCMST2VBQFFDFFWOOGDRTZBC3ANCNFSM4EQJ64GQ .

gcharles81 commented 4 years ago

Hello @blotfi is there a reason why you have not used all the TFT width ? or its because initial code was for a portrait TFT mode

Thanks

blotfi commented 4 years ago

better ask for this fork in : https://github.com/blotfi/ESP32-Radio-with-Spectrum-analyzer/issues

blotfi commented 4 years ago

I also added a video to explain this part: https://youtu.be/aWUMx9HL5kk

blotfi commented 2 years ago

check my remote control : https://play.google.com/store/apps/details?id=com.embesystems.esp_radio_rc&hl=en_US&gl=US