Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spectrum analyzer #86

Open
MirkoDalmonte opened this issue Feb 12, 2018 · 20 comments
Open

Spectrum analyzer #86

MirkoDalmonte opened this issue Feb 12, 2018 · 20 comments

Comments

@MirkoDalmonte
Copy link

Spectrum analyzer is not essential but it is beautiful :)

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

@locki-cz
Copy link

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
Copy link
Author

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
Copy link

koskee commented Apr 22, 2018

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
Copy link
Author

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

@koskee
Copy link

koskee commented Apr 23, 2018

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
Copy link

koskee commented Jan 2, 2019

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

@MirkoDalmonte
Copy link
Author

MirkoDalmonte commented Jan 3, 2019

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 <nvs.h>
#include <PubSubClient.h>
#include <WiFiMulti.h>
#include <ESPmDNS.h>
#include <TFT_ILI9163C.h>
#include <stdio.h>
#include <string.h>
#include <FS.h>
#include <SD.h>
#include <SPI.h>
#include <ArduinoOTA.h>
#include <time.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <esp_partition.h>
#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<WifiInfo_t> 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<keyname_t> 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 = (pek1
curvol)/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.c_str() ) ;
}
}
}
}
// 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.c_str() ) ;
}
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.c_str() ) ; // 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 ( ir_value ) // 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 .. wifi_99
{
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
ini_block.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.c_str() ) ; // 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
Copy link
Author

MirkoDalmonte commented Jan 3, 2019

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
Copy link

fletsche commented Jan 3, 2019

@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
Copy link
Author

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

@koskee
Copy link

koskee commented Feb 28, 2019

@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 👍

@ememem13
Copy link

ememem13 commented Apr 9, 2019

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

@gcharles81
Copy link

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

@robrob73
Copy link

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
Copy link

blotfi commented May 28, 2020

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
Copy link
Author

MirkoDalmonte commented May 28, 2020 via email

@gcharles81
Copy link

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
Copy link

blotfi commented May 29, 2020

@blotfi
Copy link

blotfi commented May 29, 2020

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

@blotfi
Copy link

blotfi commented Sep 1, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants