· 6 years ago · Apr 24, 2019, 05:56 PM
1//***************************************************************************************************
2//* ESP32_Radio -- Webradio receiver for ESP32, VS1053 MP3 module and optional display. *
3//* By Ed Smallenburg. *
4//***************************************************************************************************
5// ESP32 libraries used:
6// - WiFiMulti
7// - nvs
8// - Adafruit_ST7735
9// - ArduinoOTA
10// - PubSubClientenc_dt_pin
11// - SD
12// - FS
13// - update
14// A library for the VS1053 (for ESP32) is not available (or not easy to find). Therefore
15// a class for this module is derived from the maniacbug library and integrated in this sketch.
16//
17// See http://www.internet-radio.com for suitable stations. Add the stations of your choice
18// to the preferences in either Esp32_radio_init.ino sketch or through the webinterface.
19//
20// Brief description of the program:
21// First a suitable WiFi network is found and a connection is made.
22// Then a connection will be made to a shoutcast server. The server starts with some
23// info in the header in readable ascii, ending with a double CRLF, like:
24// icy-name:Classic Rock Florida - SHE Radio
25// icy-genre:Classic Rock 60s 70s 80s Oldies Miami South Florida
26// icy-url:http://www.ClassicRockFLorida.com
27// content-type:audio/mpeg
28// icy-pub:1
29// icy-metaint:32768 - Metadata after 32768 bytes of MP3-data
30// icy-br:128 - in kb/sec (for Ogg this is like "icy-br=Quality 2"
31//
32// After de double CRLF is received, the server starts sending mp3- or Ogg-data. For mp3, this
33// data may contain metadata (non mp3) after every "metaint" mp3 bytes.
34// The metadata is empty in most cases, but if any is available the content will be
35// presented on the TFT.
36// Pushing an input button causes the player to execute a programmable command.
37//
38// The display used is a Chinese 1.8 color TFT module 128 x 160 pixels.
39// Now there is room for 26 characters per line and 16 lines.
40// Software will work without installing the display.
41// Other displays are also supported. See documentation.
42// The SD card interface of the module may be used to play mp3-tracks on the SD card.
43//
44// For configuration of the WiFi network(s): see the global data section further on.
45//
46// The VSPI interface is used for VS1053, TFT and SD.
47//
48// Wiring. Note that this is just an example. Pins (except 18,19 and 23 of the SPI interface)
49// can be configured in the config page of the web interface.
50// ESP32dev Signal Wired to LCD Wired to VS1053 SDCARD Wired to the rest
51// -------- ------ -------------- ------------------- ------ ---------------
52// GPIO32 - pin 1 XDCS - -
53// GPIO5 - pin 2 XCS - -
54// GPIO4 - pin 4 DREQ - -
55// GPIO2 pin 3 D/C or A0 - - -
56// GPIO22 - - CS -
57// GPIO16 RXD2 - - - TX of NEXTION (if in use)
58// GPIO17 TXD2 - - - RX of NEXTION (if in use)
59// GPIO18 SCK pin 5 CLK or SCK pin 5 SCK CLK -
60// GPIO19 MISO - pin 7 MISO MISO -
61// GPIO23 MOSI pin 4 DIN or SDA pin 6 MOSI MOSI -
62// GPIO15 pin 2 CS - - -
63// GPI03 RXD0 - - - Reserved serial input
64// GPIO1 TXD0 - - - Reserved serial output
65// GPIO34 - - - - Optional pull-up resistor
66// GPIO35 - - - - Infrared receiver VS1838B
67// GPIO25 - - - - Rotary encoder CLK
68// GPIO26 - - - - Rotary encoder DT
69// GPIO27 - - - - Rotary encoder SW
70// ------- ------ --------------- ------------------- ------ ----------------
71// GND - pin 8 GND pin 8 GND Power supply GND
72// VCC 5 V - pin 7 BL - Power supply
73// VCC 5 V - pin 6 VCC pin 9 5V Power supply
74// EN - pin 1 RST pin 3 XRST -
75//
76// 26-04-2017, ES: First set-up, derived from ESP8266 version.
77// 08-05-2017, ES: Handling of preferences.
78// 20-05-2017, ES: Handling input buttons and MQTT.
79// 22-05-2017, ES: Save preset, volume and tone settings.
80// 23-05-2017, ES: No more calls of non-iram functions on interrupts.
81// 24-05-2017, ES: Support for featherboard.
82// 26-05-2017, ES: Correction playing from .m3u playlist. Allow single hidden SSID.
83// 30-05-2017, ES: Add SD card support (FAT format), volume indicator.
84// 26-06-2017, ES: Correction: start in AP-mode if no WiFi networks configured.
85// 28-06-2017, ES: Added IR interface.
86// 30-06-2017, ES: Improved functions for SD card play.
87// 03-07-2017, ES: Webinterface control page shows current settings.
88// 04-07-2017, ES: Correction MQTT subscription. Keep playing during long operations.
89// 08-07-2017, ES: More space for streamtitle on TFT.
90// 18-07-2017, ES: Time Of Day on TFT.
91// 19-07-2017, ES: Minor corrections.
92// 26-07-2017, ES: Flexible pin assignment. Add rotary encoder switch.
93// 27-07-2017, ES: Removed tinyXML library.
94// 18-08-2017, Es: Minor corrections
95// 28-08-2017, ES: Preferences for pins used for SPI bus,
96// Corrected bug in handling programmable pins,
97// Introduced touch pins.
98// 30-08-2017, ES: Limit number of retries for MQTT connection.
99// Added MDNS responder.
100// 11-11-2017, ES: Increased ringbuffer. Measure bit rate.
101// 13-11-2017, ES: Forward declarations.
102// 16-11-2017, ES: Replaced ringbuffer by FreeRTOS queue, play function on second CPU,
103// Included improved rotary switch routines supplied by fenyvesi,
104// Better IR sensitivity.
105// 30-11-2017, ES: Hide passwords in config page.
106// 01-12-2017, ES: Better handling of playlist.
107// 07-12-2017, ES: Faster handling of config screen.
108// 08-12-2017, ES: More MQTT items to publish, added pin_shutdown.
109// 13-12-2017, ES: Correction clear LCD.
110// 15-12-2017, ES: Correction defaultprefs.h.
111// 18-12-2017, ES: Stop playing during config.
112// 02-01-2018, ES: Stop/resume is same command.
113// 22-01-2018, ES: Read ADC (GPIO36) and display as a battery capacity percentage.
114// 13-02-2018, ES: Stop timer during NVS write.
115// 15-02-2018, ES: Correction writing wifi credentials in NVS.
116// 03-03-2018, ES: Correction bug IR pinnumber.
117// 05-03-2018, ES: Improved rotary encoder interface.
118// 10-03-2018, ES: Minor corrections.
119// 13-04-2018, ES: Guard against empty string send to TFT, thanks to Andreas Spiess.
120// 16-04-2018, ES: ID3 tags handling while playing from SD.
121// 25-04-2018, ES: Choice of several display boards.
122// 30-04-2018, ES: Bugfix: crash when no IR is configured, no display without VS1063.
123// 08-05-2018, ES: 1602 LCD display support (limited).
124// 11-05-2018, ES: Bugfix: incidental crash in isr_enc_turn().
125// 30-05-2018, ES: Bugfix: Assigned DRAM to global variables used in timer ISR.
126// 31-05-2018, ES: Bugfix: Crashed if I2C is used, but pins not defined.
127// 01-06-2018, ES: Run Playtask on CPU 0.
128// 04-06-2018, ES: Made handling of playlistdata more tolerant (NDR).
129// 09-06-2018, ES: Typo in defaultprefs.h.
130// 10-06-2018, ES: Rotary encoder, interrupts on all 3 signals.
131// 25-06-2018, ES: Timing of mp3loop. Limit read from stream to free queue space.
132// 16-07-2018, ES: Correction tftset().
133// 25-07-2018, ES: Correction touch pins.
134// 30-07-2018, ES: Added GPIO39 and inversed shutdown pin. Thanks to fletsche.
135// 31-07-2018, ES: Added TFT backlight control.
136// 01-08-2018, ES: Debug info for IR. Shutdown amplifier if volume is 0.
137// 02-08-2018, ES: Added support for ILI9341 display.
138// 03-08-2018, ES: Added playlistposition for MQTT.
139// 06-08-2018, ES: Correction negative time offset, OTA through remote host.
140// 16-08-2018, ES: Added Nextion support.
141// 18-09-2018, ES: "uppreset" and "downpreset" for MP3 player.
142// 04-10-2018, ES: Fixed compile error OLED 64x128 display.
143// 09-10-2018, ES: Bug fix xSemaphoreTake.
144// 05-01-2019, ES: Fine tune datarate.
145// 05-01-2019, ES: Basic http authentication. (just one user)
146// 11-02-2019, ES: MQTT topic and subtopic size enlarged.
147// 24-04-2019, ES: Do not lock SPI during gettime(). Calling gettime may take a long time.
148//
149//
150// Define the version number, also used for webserver as Last-Modified header and to
151// check version for update. The format must be exactly as specified by the HTTP standard!
152#define VERSION "Wed, 24 Apr 2019 08:20:00 GMT"
153// ESP32-Radio can be updated (OTA) to the latest version from a remote server.
154// The download uses the following server and files:
155#define UPDATEHOST "smallenburg.nl" // Host for software updates
156#define BINFILE "/Arduino/Esp32_radio.ino.bin" // Binary file name for update software
157#define TFTFILE "/Arduino/ESP32-Radio.tft" // Binary file name for update NEXTION image
158//
159// Define (just one) type of display. See documentation.
160#define BLUETFT // Works also for RED TFT 128x160
161//#define OLED // 64x128 I2C OLED
162//#define DUMMYTFT // Dummy display
163//#define LCD1602I2C // LCD 1602 display with I2C backpack
164//#define ILI9341 // ILI9341 240*320
165//#define NEXTION // Nextion display. Uses UART 2 (pin 16 and 17)
166//
167#include <nvs.h>
168#include <PubSubClient.h>
169#include <WiFiMulti.h>
170#include <ESPmDNS.h>
171#include <time.h>
172#include <stdio.h>
173#include <string.h>
174#include <FS.h>
175#include <SD.h>
176#include <SPI.h>
177#include <ArduinoOTA.h>
178#include <freertos/queue.h>
179#include <freertos/task.h>
180#include <esp_task_wdt.h>
181#include <esp_partition.h>
182#include <driver/adc.h>
183#include <Update.h>
184#include <base64.h>
185// Number of entries in the queue
186#define QSIZ 600
187// Debug buffer size
188#define DEBUG_BUFFER_SIZE 150
189#define NVSBUFSIZE 150
190// Access point name if connection to WiFi network fails. Also the hostname for WiFi and OTA.
191// Note that the password of an AP must be at least as long as 8 characters.
192// Also used for other naming.
193#define NAME "ESP32Radio"
194// Maximum number of MQTT reconnects before give-up
195#define MAXMQTTCONNECTS 5
196// Adjust size of buffer to the longest expected string for nvsgetstr
197#define NVSBUFSIZE 150
198// Position (column) of time in topline relative to end
199#define TIMEPOS -52
200// SPI speed for SD card
201#define SDSPEED 1000000
202// Size of metaline buffer
203#define METASIZ 1024
204// Max. number of NVS keys in table
205#define MAXKEYS 200
206// Time-out [sec] for blanking TFT display (BL pin)
207#define BL_TIME 45
208//
209// Subscription topics for MQTT. The topic will be pefixed by "PREFIX/", where PREFIX is replaced
210// by the the mqttprefix in the preferences. The next definition will yield the topic
211// "ESP32Radio/command" if mqttprefix is "ESP32Radio".
212#define MQTT_SUBTOPIC "command" // Command to receive from MQTT
213//
214#define otaclient mp3client // OTA uses mp3client for connection to host
215
216//**************************************************************************************************
217// Forward declaration and prototypes of various functions. *
218//**************************************************************************************************
219void displaytime ( const char* str, uint16_t color = 0xFFFF ) ;
220void showstreamtitle ( const char* ml, bool full = false ) ;
221void handlebyte_ch ( uint8_t b ) ;
222void handleFSf ( const String& pagename ) ;
223void handleCmd() ;
224char* dbgprint( const char* format, ... ) ;
225const char* analyzeCmd ( const char* str ) ;
226const char* analyzeCmd ( const char* par, const char* val ) ;
227void chomp ( String &str ) ;
228String httpheader ( String contentstype ) ;
229bool nvssearch ( const char* key ) ;
230void mp3loop() ;
231void tftlog ( const char *str ) ;
232void playtask ( void * parameter ) ; // Task to play the stream
233void spftask ( void * parameter ) ; // Task for special functions
234void gettime() ;
235void reservepin ( int8_t rpinnr ) ;
236
237
238//**************************************************************************************************
239// Several structs. *
240//**************************************************************************************************
241//
242
243struct scrseg_struct // For screen segments
244{
245 bool update_req ; // Request update of screen
246 uint16_t color ; // Textcolor
247 uint16_t y ; // Begin of segment row
248 uint16_t height ; // Height of segment
249 String str ; // String to be displayed
250} ;
251
252enum qdata_type { QDATA, QSTARTSONG, QSTOPSONG } ; // datatyp in qdata_struct
253struct qdata_struct
254{
255 int datatyp ; // Identifier
256 __attribute__((aligned(4))) uint8_t buf[32] ; // Buffer for chunk
257} ;
258
259struct ini_struct
260{
261 String mqttbroker ; // The name of the MQTT broker server
262 String mqttprefix ; // Prefix to use for topics
263 uint16_t mqttport ; // Port, default 1883
264 String mqttuser ; // User for MQTT authentication
265 String mqttpasswd ; // Password for MQTT authentication
266 uint8_t reqvol ; // Requested volume
267 uint8_t rtone[4] ; // Requested bass/treble settings
268 int8_t newpreset ; // Requested preset
269 String clk_server ; // Server to be used for time of day clock
270 int8_t clk_offset ; // Offset in hours with respect to UTC
271 int8_t clk_dst ; // Number of hours shift during DST
272 int8_t ir_pin ; // GPIO connected to output of IR decoder
273 int8_t enc_clk_pin ; // GPIO connected to CLK of rotary encoder
274 int8_t enc_dt_pin ; // GPIO connected to DT of rotary encoder
275 int8_t enc_sw_pin ; // GPIO connected to SW of rotary encoder
276 int8_t tft_cs_pin ; // GPIO connected to CS of TFT screen
277 int8_t tft_dc_pin ; // GPIO connected to D/C or A0 of TFT screen
278 int8_t tft_scl_pin ; // GPIO connected to SCL of i2c TFT screen
279 int8_t tft_sda_pin ; // GPIO connected to SDA of I2C TFT screen
280 int8_t tft_bl_pin ; // GPIO to activate BL of display
281 int8_t tft_blx_pin ; // GPIO to activate BL of display (inversed logic)
282 int8_t sd_cs_pin ; // GPIO connected to CS of SD card
283 int8_t vs_cs_pin ; // GPIO connected to CS of VS1053
284 int8_t vs_dcs_pin ; // GPIO connected to DCS of VS1053
285 int8_t vs_dreq_pin ; // GPIO connected to DREQ of VS1053
286 int8_t vs_shutdown_pin ; // GPIO to shut down the amplifier
287 int8_t vs_shutdownx_pin ; // GPIO to shut down the amplifier (inversed logic)
288 int8_t spi_sck_pin ; // GPIO connected to SPI SCK pin
289 int8_t spi_miso_pin ; // GPIO connected to SPI MISO pin
290 int8_t spi_mosi_pin ; // GPIO connected to SPI MOSI pin
291 uint16_t bat0 ; // ADC value for 0 percent battery charge
292 uint16_t bat100 ; // ADC value for 100 percent battery charge
293} ;
294
295struct WifiInfo_t // For list with WiFi info
296{
297 uint8_t inx ; // Index as in "wifi_00"
298 char * ssid ; // SSID for an entry
299 char * passphrase ; // Passphrase for an entry
300} ;
301
302struct nvs_entry
303{
304 uint8_t Ns ; // Namespace ID
305 uint8_t Type ; // Type of value
306 uint8_t Span ; // Number of entries used for this item
307 uint8_t Rvs ; // Reserved, should be 0xFF
308 uint32_t CRC ; // CRC
309 char Key[16] ; // Key in Ascii
310 uint64_t Data ; // Data in entry
311} ;
312
313struct nvs_page // For nvs entries
314{ // 1 page is 4096 bytes
315 uint32_t State ;
316 uint32_t Seqnr ;
317 uint32_t Unused[5] ;
318 uint32_t CRC ;
319 uint8_t Bitmap[32] ;
320 nvs_entry Entry[126] ;
321} ;
322
323struct keyname_t // For keys in NVS
324{
325 char Key[16] ; // Max length is 15 plus delimeter
326} ;
327
328//**************************************************************************************************
329// Global data section. *
330//**************************************************************************************************
331// There is a block ini-data that contains some configuration. Configuration data is *
332// saved in the preferences by the webinterface. On restart the new data will *
333// de read from these preferences. *
334// Items in ini_block can be changed by commands from webserver/MQTT/Serial. *
335//**************************************************************************************************
336
337enum display_t { T_UNDEFINED, T_BLUETFT, T_OLED, // Various types of display
338 T_DUMMYTFT, T_LCD1602I2C, T_ILI9341,
339 T_NEXTION } ;
340
341enum datamode_t { INIT = 1, HEADER = 2, DATA = 4, // State for datastream
342 METADATA = 8, PLAYLISTINIT = 16,
343 PLAYLISTHEADER = 32, PLAYLISTDATA = 64,
344 STOPREQD = 128, STOPPED = 256
345 } ;
346
347// Global variables
348int DEBUG = 1 ; // Debug on/off
349int numSsid ; // Number of available WiFi networks
350WiFiMulti wifiMulti ; // Possible WiFi networks
351ini_struct ini_block ; // Holds configurable data
352WiFiServer cmdserver ( 80 ) ; // Instance of embedded webserver, port 80
353WiFiClient mp3client ; // An instance of the mp3 client, also used for OTA
354WiFiClient cmdclient ; // An instance of the client for commands
355WiFiClient wmqttclient ; // An instance for mqtt
356PubSubClient mqttclient ( wmqttclient ) ; // Client for MQTT subscriber
357HardwareSerial* nxtserial = NULL ; // Serial port for NEXTION (if defined)
358TaskHandle_t maintask ; // Taskhandle for main task
359TaskHandle_t xplaytask ; // Task handle for playtask
360TaskHandle_t xspftask ; // Task handle for special functions
361SemaphoreHandle_t SPIsem = NULL ; // For exclusive SPI usage
362hw_timer_t* timer = NULL ; // For timer
363char timetxt[9] ; // Converted timeinfo
364char cmd[130] ; // Command from MQTT or Serial
365uint8_t tmpbuff[6000] ; // Input buffer for mp3 or data stream
366QueueHandle_t dataqueue ; // Queue for mp3 datastream
367QueueHandle_t spfqueue ; // Queue for special functions
368qdata_struct outchunk ; // Data to queue
369qdata_struct inchunk ; // Data from queue
370uint8_t* outqp = outchunk.buf ; // Pointer to buffer in outchunk
371uint32_t totalcount = 0 ; // Counter mp3 data
372datamode_t datamode ; // State of datastream
373int metacount ; // Number of bytes in metadata
374int datacount ; // Counter databytes before metadata
375char metalinebf[METASIZ + 1] ; // Buffer for metaline/ID3 tags
376int16_t metalinebfx ; // Index for metalinebf
377String icystreamtitle ; // Streamtitle from metadata
378String icyname ; // Icecast station name
379String ipaddress ; // Own IP-address
380int bitrate ; // Bitrate in kb/sec
381int mbitrate ; // Measured bitrate
382int metaint = 0 ; // Number of databytes between metadata
383int8_t currentpreset = -1 ; // Preset station playing
384String host ; // The URL to connect to or file to play
385String playlist ; // The URL of the specified playlist
386bool hostreq = false ; // Request for new host
387bool reqtone = false ; // New tone setting requested
388bool muteflag = false ; // Mute output
389bool resetreq = false ; // Request to reset the ESP32
390bool updatereq = false ; // Request to update software from remote host
391bool NetworkFound = false ; // True if WiFi network connected
392bool mqtt_on = false ; // MQTT in use
393String networks ; // Found networks in the surrounding
394uint16_t mqttcount = 0 ; // Counter MAXMQTTCONNECTS
395int8_t playingstat = 0 ; // 1 if radio is playing (for MQTT)
396int16_t playlist_num = 0 ; // Nonzero for selection from playlist
397File mp3file ; // File containing mp3 on SD card
398uint32_t mp3filelength ; // File length
399bool localfile = false ; // Play from local mp3-file or not
400bool chunked = false ; // Station provides chunked transfer
401int chunkcount = 0 ; // Counter for chunked transfer
402String http_getcmd ; // Contents of last GET command
403String http_rqfile ; // Requested file
404bool http_response_flag = false ; // Response required
405uint16_t ir_value = 0 ; // IR code
406uint32_t ir_0 = 550 ; // Average duration of an IR short pulse
407uint32_t ir_1 = 1650 ; // Average duration of an IR long pulse
408struct tm timeinfo ; // Will be filled by NTP server
409bool time_req = false ; // Set time requested
410bool SD_okay = false ; // True if SD card in place and readable
411String SD_nodelist ; // Nodes of mp3-files on SD
412int SD_nodecount = 0 ; // Number of nodes in SD_nodelist
413String SD_currentnode = "" ; // Node ID of song playing ("0" if random)
414uint16_t adcval ; // ADC value (battery voltage)
415uint32_t clength ; // Content length found in http header
416uint32_t max_mp3loop_time = 0 ; // To check max handling time in mp3loop (msec)
417int16_t scanios ; // TEST*TEST*TEST
418int16_t scaniocount ; // TEST*TEST*TEST
419uint16_t bltimer = 0 ; // Backlight time-out counter
420display_t displaytype = T_UNDEFINED ; // Display type
421std::vector<WifiInfo_t> wifilist ; // List with wifi_xx info
422// nvs stuff
423nvs_page nvsbuf ; // Space for 1 page of NVS info
424const esp_partition_t* nvs ; // Pointer to partition struct
425esp_err_t nvserr ; // Error code from nvs functions
426uint32_t nvshandle = 0 ; // Handle for nvs access
427uint8_t namespace_ID ; // Namespace ID found
428char nvskeys[MAXKEYS][16] ; // Space for NVS keys
429std::vector<keyname_t> keynames ; // Keynames in NVS
430// Rotary encoder stuff
431#define sv DRAM_ATTR static volatile
432sv uint16_t clickcount = 0 ; // Incremented per encoder click
433sv int16_t rotationcount = 0 ; // Current position of rotary switch
434sv uint16_t enc_inactivity = 0 ; // Time inactive
435sv bool singleclick = false ; // True if single click detected
436sv bool doubleclick = false ; // True if double click detected
437sv bool tripleclick = false ; // True if triple click detected
438sv bool longclick = false ; // True if longclick detected
439enum enc_menu_t { VOLUME, PRESET, TRACK } ; // State for rotary encoder menu
440enc_menu_t enc_menu_mode = VOLUME ; // Default is VOLUME mode
441
442//
443struct progpin_struct // For programmable input pins
444{
445 int8_t gpio ; // Pin number
446 bool reserved ; // Reserved for connected devices
447 bool avail ; // Pin is available for a command
448 String command ; // Command to execute when activated
449 // Example: "uppreset=1"
450 bool cur ; // Current state, true = HIGH, false = LOW
451} ;
452
453progpin_struct progpin[] = // Input pins and programmed function
454{
455 { 0, false, false, "", false },
456 //{ 1, true, false, "", false }, // Reserved for TX Serial output
457 { 2, false, false, "", false },
458 //{ 3, true, false, "", false }, // Reserved for RX Serial input
459 { 4, false, false, "", false },
460 { 5, false, false, "", false },
461 //{ 6, true, false, "", false }, // Reserved for FLASH SCK
462 //{ 7, true, false, "", false }, // Reserved for FLASH D0
463 //{ 8, true, false, "", false }, // Reserved for FLASH D1
464 //{ 9, true, false, "", false }, // Reserved for FLASH D2
465 //{ 10, true, false, "", false }, // Reserved for FLASH D3
466 //{ 11, true, false, "", false }, // Reserved for FLASH CMD
467 { 12, false, false, "", false },
468 { 13, false, false, "", false },
469 { 14, false, false, "", false },
470 { 15, false, false, "", false },
471 { 16, false, false, "", false }, // May be UART 2 RX for Nextion
472 { 17, false, false, "", false }, // May be UART 2 TX for Nextion
473 { 18, false, false, "", false }, // Default for SPI CLK
474 { 19, false, false, "", false }, // Default for SPI MISO
475 //{ 20, true, false, "", false }, // Not exposed on DEV board
476 { 21, false, false, "", false }, // Also Wire SDA
477 { 22, false, false, "", false }, // Also Wire SCL
478 { 23, false, false, "", false }, // Default for SPI MOSI
479 //{ 24, true, false, "", false }, // Not exposed on DEV board
480 { 25, false, false, "", false },
481 { 26, false, false, "", false },
482 { 27, false, false, "", false },
483 //{ 28, true, false, "", false }, // Not exposed on DEV board
484 //{ 29, true, false, "", false }, // Not exposed on DEV board
485 //{ 30, true, false, "", false }, // Not exposed on DEV board
486 //{ 31, true, false, "", false }, // Not exposed on DEV board
487 { 32, false, false, "", false },
488 { 33, false, false, "", false },
489 { 34, false, false, "", false }, // Note, no internal pull-up
490 { 35, false, false, "", false }, // Note, no internal pull-up
491 //{ 36, true, false, "", false }, // Reserved for ADC battery level
492 { 39, false, false, "", false }, // Note, no internal pull-up
493 { -1, false, false, "", false } // End of list
494} ;
495
496struct touchpin_struct // For programmable input pins
497{
498 int8_t gpio ; // Pin number GPIO
499 bool reserved ; // Reserved for connected devices
500 bool avail ; // Pin is available for a command
501 String command ; // Command to execute when activated
502 // Example: "uppreset=1"
503 bool cur ; // Current state, true = HIGH, false = LOW
504 int16_t count ; // Counter number of times low level
505} ;
506touchpin_struct touchpin[] = // Touch pins and programmed function
507{
508 { 4, false, false, "", false, 0 }, // TOUCH0
509 { 0, true, false, "", false, 0 }, // TOUCH1, reserved for BOOT button
510 { 2, false, false, "", false, 0 }, // TOUCH2
511 { 15, false, false, "", false, 0 }, // TOUCH3
512 { 13, false, false, "", false, 0 }, // TOUCH4
513 { 12, false, false, "", false, 0 }, // TOUCH5
514 { 14, false, false, "", false, 0 }, // TOUCH6
515 { 27, false, false, "", false, 0 }, // TOUCH7
516 { 33, false, false, "", false, 0 }, // TOUCH8
517 { 32, false, false, "", false, 0 }, // TOUCH9
518 { -1, false, false, "", false, 0 } // End of list
519 // End of table
520} ;
521
522
523//**************************************************************************************************
524// Pages, CSS and data for the webinterface. *
525//**************************************************************************************************
526#include "about_html.h"
527#include "config_html.h"
528#include "index_html.h"
529#include "mp3play_html.h"
530#include "radio_css.h"
531#include "favicon_ico.h"
532#include "defaultprefs.h"
533
534//**************************************************************************************************
535// End of global data section. *
536//**************************************************************************************************
537
538
539
540
541//**************************************************************************************************
542// M Q T T P U B _ C L A S S *
543//**************************************************************************************************
544// ID's for the items to publish to MQTT. Is index in amqttpub[]
545enum { MQTT_IP, MQTT_ICYNAME, MQTT_STREAMTITLE, MQTT_NOWPLAYING,
546 MQTT_PRESET, MQTT_VOLUME, MQTT_PLAYING, MQTT_PLAYLISTPOS
547 } ;
548enum { MQSTRING, MQINT8, MQINT16 } ; // Type of variable to publish
549
550class mqttpubc // For MQTT publishing
551{
552 struct mqttpub_struct
553 {
554 const char* topic ; // Topic as partial string (without prefix)
555 uint8_t type ; // Type of payload
556 void* payload ; // Payload for this topic
557 bool topictrigger ; // Set to true to trigger MQTT publish
558 } ;
559 // Publication topics for MQTT. The topic will be pefixed by "PREFIX/", where PREFIX is replaced
560 // by the the mqttprefix in the preferences.
561 protected:
562 mqttpub_struct amqttpub[9] = // Definitions of various MQTT topic to publish
563 { // Index is equal to enum above
564 { "ip", MQSTRING, &ipaddress, false }, // Definition for MQTT_IP
565 { "icy/name", MQSTRING, &icyname, false }, // Definition for MQTT_ICYNAME
566 { "icy/streamtitle", MQSTRING, &icystreamtitle, false }, // Definition for MQTT_STREAMTITLE
567 { "nowplaying", MQSTRING, &ipaddress, false }, // Definition for MQTT_NOWPLAYING
568 { "preset" , MQINT8, ¤tpreset, false }, // Definition for MQTT_PRESET
569 { "volume" , MQINT8, &ini_block.reqvol, false }, // Definition for MQTT_VOLUME
570 { "playing", MQINT8, &playingstat, false }, // Definition for MQTT_PLAYING
571 { "playlist/pos", MQINT16, &playlist_num, false }, // Definition for MQTT_PLAYLISTPOS
572 { NULL, 0, NULL, false } // End of definitions
573 } ;
574 public:
575 void trigger ( uint8_t item ) ; // Trigger publishig for one item
576 void publishtopic() ; // Publish triggerer items
577} ;
578
579
580//**************************************************************************************************
581// MQTTPUB class implementation. *
582//**************************************************************************************************
583
584//**************************************************************************************************
585// T R I G G E R *
586//**************************************************************************************************
587// Set request for an item to publish to MQTT. *
588//**************************************************************************************************
589void mqttpubc::trigger ( uint8_t item ) // Trigger publishig for one item
590{
591 amqttpub[item].topictrigger = true ; // Request re-publish for an item
592}
593
594//**************************************************************************************************
595// P U B L I S H T O P I C *
596//**************************************************************************************************
597// Publish a topic to MQTT broker. *
598//**************************************************************************************************
599void mqttpubc::publishtopic()
600{
601 int i = 0 ; // Loop control
602 char topic[80] ; // Topic to send
603 const char* payload ; // Points to payload
604 char intvar[10] ; // Space for integer parameter
605 while ( amqttpub[i].topic )
606 {
607 if ( amqttpub[i].topictrigger ) // Topic ready to send?
608 {
609 amqttpub[i].topictrigger = false ; // Success or not: clear trigger
610 sprintf ( topic, "%s/%s", ini_block.mqttprefix.c_str(),
611 amqttpub[i].topic ) ; // Add prefix to topic
612 switch ( amqttpub[i].type ) // Select conversion method
613 {
614 case MQSTRING :
615 payload = ((String*)amqttpub[i].payload)->c_str() ;
616 //payload = pstr->c_str() ; // Get pointer to payload
617 break ;
618 case MQINT8 :
619 sprintf ( intvar, "%d",
620 *(int8_t*)amqttpub[i].payload ) ; // Convert to array of char
621 payload = intvar ; // Point to this array
622 break ;
623 case MQINT16 :
624 sprintf ( intvar, "%d",
625 *(int16_t*)amqttpub[i].payload ) ; // Convert to array of char
626 payload = intvar ; // Point to this array
627 break ;
628 default :
629 continue ; // Unknown data type
630 }
631 dbgprint ( "Publish to topic %s : %s", // Show for debug
632 topic, payload ) ;
633 if ( !mqttclient.publish ( topic, payload ) ) // Publish!
634 {
635 dbgprint ( "MQTT publish failed!" ) ; // Failed
636 }
637 return ; // Do the rest later
638 }
639 i++ ; // Next entry
640 }
641}
642
643mqttpubc mqttpub ; // Instance for mqttpubc
644
645
646//
647//**************************************************************************************************
648// VS1053 stuff. Based on maniacbug library. *
649//**************************************************************************************************
650// VS1053 class definition. *
651//**************************************************************************************************
652class VS1053
653{
654 private:
655 int8_t cs_pin ; // Pin where CS line is connected
656 int8_t dcs_pin ; // Pin where DCS line is connected
657 int8_t dreq_pin ; // Pin where DREQ line is connected
658 int8_t shutdown_pin ; // Pin where the shutdown line is connected
659 int8_t shutdownx_pin ; // Pin where the shutdown (inversed) line is connected
660 uint8_t curvol ; // Current volume setting 0..100%
661 const uint8_t vs1053_chunk_size = 32 ;
662 // SCI Register
663 const uint8_t SCI_MODE = 0x0 ;
664 const uint8_t SCI_BASS = 0x2 ;
665 const uint8_t SCI_CLOCKF = 0x3 ;
666 const uint8_t SCI_AUDATA = 0x5 ;
667 const uint8_t SCI_WRAM = 0x6 ;
668 const uint8_t SCI_WRAMADDR = 0x7 ;
669 const uint8_t SCI_AIADDR = 0xA ;
670 const uint8_t SCI_VOL = 0xB ;
671 const uint8_t SCI_AICTRL0 = 0xC ;
672 const uint8_t SCI_AICTRL1 = 0xD ;
673 const uint8_t SCI_num_registers = 0xF ;
674 // SCI_MODE bits
675 const uint8_t SM_SDINEW = 11 ; // Bitnumber in SCI_MODE always on
676 const uint8_t SM_RESET = 2 ; // Bitnumber in SCI_MODE soft reset
677 const uint8_t SM_CANCEL = 3 ; // Bitnumber in SCI_MODE cancel song
678 const uint8_t SM_TESTS = 5 ; // Bitnumber in SCI_MODE for tests
679 const uint8_t SM_LINE1 = 14 ; // Bitnumber in SCI_MODE for Line input
680 SPISettings VS1053_SPI ; // SPI settings for this slave
681 uint8_t endFillByte ; // Byte to send when stopping song
682 bool okay = true ; // VS1053 is working
683 protected:
684 inline void await_data_request() const
685 {
686 while ( ( dreq_pin >= 0 ) &&
687 ( !digitalRead ( dreq_pin ) ) )
688 {
689 NOP() ; // Very short delay
690 }
691 }
692
693 inline void control_mode_on() const
694 {
695 SPI.beginTransaction ( VS1053_SPI ) ; // Prevent other SPI users
696 digitalWrite ( cs_pin, LOW ) ;
697 }
698
699 inline void control_mode_off() const
700 {
701 digitalWrite ( cs_pin, HIGH ) ; // End control mode
702 SPI.endTransaction() ; // Allow other SPI users
703 }
704
705 inline void data_mode_on() const
706 {
707 SPI.beginTransaction ( VS1053_SPI ) ; // Prevent other SPI users
708 //digitalWrite ( cs_pin, HIGH ) ; // Bring slave in data mode
709 digitalWrite ( dcs_pin, LOW ) ;
710 }
711
712 inline void data_mode_off() const
713 {
714 digitalWrite ( dcs_pin, HIGH ) ; // End data mode
715 SPI.endTransaction() ; // Allow other SPI users
716 }
717
718 uint16_t read_register ( uint8_t _reg ) const ;
719 void write_register ( uint8_t _reg, uint16_t _value ) const ;
720 inline bool sdi_send_buffer ( uint8_t* data, size_t len ) ;
721 void sdi_send_fillers ( size_t length ) ;
722 void wram_write ( uint16_t address, uint16_t data ) ;
723 uint16_t wram_read ( uint16_t address ) ;
724 void output_enable ( bool ena ) ; // Enable amplifier through shutdown pin(s)
725
726 public:
727 // Constructor. Only sets pin values. Doesn't touch the chip. Be sure to call begin()!
728 VS1053 ( int8_t _cs_pin, int8_t _dcs_pin, int8_t _dreq_pin,
729 int8_t _shutdown_pin, int8_t _shutdownx_pin ) ;
730 void begin() ; // Begin operation. Sets pins correctly,
731 // and prepares SPI bus.
732 void startSong() ; // Prepare to start playing. Call this each
733 // time a new song starts.
734 inline bool playChunk ( uint8_t* data, // Play a chunk of data. Copies the data to
735 size_t len ) ; // the chip. Blocks until complete.
736 // Returns true if more data can be added
737 // to fifo
738 void stopSong() ; // Finish playing a song. Call this after
739 // the last playChunk call.
740 void setVolume ( uint8_t vol ) ; // Set the player volume.Level from 0-100,
741 // higher is louder.
742 void setTone ( uint8_t* rtone ) ; // Set the player baas/treble, 4 nibbles for
743 // treble gain/freq and bass gain/freq
744 inline uint8_t getVolume() const // Get the current volume setting.
745 { // higher is louder.
746 return curvol ;
747 }
748 void printDetails ( const char *header ) ; // Print config details to serial output
749 void softReset() ; // Do a soft reset
750 bool testComm ( const char *header ) ; // Test communication with module
751 inline bool data_request() const
752 {
753 return ( digitalRead ( dreq_pin ) == HIGH ) ;
754 }
755 void AdjustRate ( long ppm2 ) ; // Fine tune the datarate
756
757} ;
758
759//**************************************************************************************************
760// VS1053 class implementation. *
761//**************************************************************************************************
762
763VS1053::VS1053 ( int8_t _cs_pin, int8_t _dcs_pin, int8_t _dreq_pin,
764 int8_t _shutdown_pin, int8_t _shutdownx_pin ) :
765 cs_pin(_cs_pin), dcs_pin(_dcs_pin), dreq_pin(_dreq_pin), shutdown_pin(_shutdown_pin),
766 shutdownx_pin(_shutdownx_pin)
767{
768}
769
770uint16_t VS1053::read_register ( uint8_t _reg ) const
771{
772 uint16_t result ;
773
774 control_mode_on() ;
775 SPI.write ( 3 ) ; // Read operation
776 SPI.write ( _reg ) ; // Register to write (0..0xF)
777 // Note: transfer16 does not seem to work
778 result = ( SPI.transfer ( 0xFF ) << 8 ) | // Read 16 bits data
779 ( SPI.transfer ( 0xFF ) ) ;
780 await_data_request() ; // Wait for DREQ to be HIGH again
781 control_mode_off() ;
782 return result ;
783}
784
785void VS1053::write_register ( uint8_t _reg, uint16_t _value ) const
786{
787 control_mode_on( );
788 SPI.write ( 2 ) ; // Write operation
789 SPI.write ( _reg ) ; // Register to write (0..0xF)
790 SPI.write16 ( _value ) ; // Send 16 bits data
791 await_data_request() ;
792 control_mode_off() ;
793}
794
795bool VS1053::sdi_send_buffer ( uint8_t* data, size_t len )
796{
797 size_t chunk_length ; // Length of chunk 32 byte or shorter
798
799 data_mode_on() ;
800 while ( len ) // More to do?
801 {
802 chunk_length = len ;
803 if ( len > vs1053_chunk_size )
804 {
805 chunk_length = vs1053_chunk_size ;
806 }
807 len -= chunk_length ;
808 await_data_request() ; // Wait for space available
809 SPI.writeBytes ( data, chunk_length ) ;
810 data += chunk_length ;
811 }
812 data_mode_off() ;
813 return data_request() ; // True if more data can de stored in fifo
814}
815
816void VS1053::sdi_send_fillers ( size_t len )
817{
818 size_t chunk_length ; // Length of chunk 32 byte or shorter
819
820 data_mode_on() ;
821 while ( len ) // More to do?
822 {
823 await_data_request() ; // Wait for space available
824 chunk_length = len ;
825 if ( len > vs1053_chunk_size )
826 {
827 chunk_length = vs1053_chunk_size ;
828 }
829 len -= chunk_length ;
830 while ( chunk_length-- )
831 {
832 SPI.write ( endFillByte ) ;
833 }
834 }
835 data_mode_off();
836}
837
838void VS1053::wram_write ( uint16_t address, uint16_t data )
839{
840 write_register ( SCI_WRAMADDR, address ) ;
841 write_register ( SCI_WRAM, data ) ;
842}
843
844uint16_t VS1053::wram_read ( uint16_t address )
845{
846 write_register ( SCI_WRAMADDR, address ) ; // Start reading from WRAM
847 return read_register ( SCI_WRAM ) ; // Read back result
848}
849
850bool VS1053::testComm ( const char *header )
851{
852 // Test the communication with the VS1053 module. The result wille be returned.
853 // If DREQ is low, there is problably no VS1053 connected. Pull the line HIGH
854 // in order to prevent an endless loop waiting for this signal. The rest of the
855 // software will still work, but readbacks from VS1053 will fail.
856 int i ; // Loop control
857 uint16_t r1, r2, cnt = 0 ;
858 uint16_t delta = 300 ; // 3 for fast SPI
859
860 dbgprint ( header ) ; // Show a header
861 if ( !digitalRead ( dreq_pin ) )
862 {
863 dbgprint ( "VS1053 not properly installed!" ) ;
864 // Allow testing without the VS1053 module
865 pinMode ( dreq_pin, INPUT_PULLUP ) ; // DREQ is now input with pull-up
866 return false ; // Return bad result
867 }
868 // Further TESTING. Check if SCI bus can write and read without errors.
869 // We will use the volume setting for this.
870 // Will give warnings on serial output if DEBUG is active.
871 // A maximum of 20 errors will be reported.
872 if ( strstr ( header, "Fast" ) )
873 {
874 delta = 3 ; // Fast SPI, more loops
875 }
876 for ( i = 0 ; ( i < 0xFFFF ) && ( cnt < 20 ) ; i += delta )
877 {
878 write_register ( SCI_VOL, i ) ; // Write data to SCI_VOL
879 r1 = read_register ( SCI_VOL ) ; // Read back for the first time
880 r2 = read_register ( SCI_VOL ) ; // Read back a second time
881 if ( r1 != r2 || i != r1 || i != r2 ) // Check for 2 equal reads
882 {
883 dbgprint ( "VS1053 SPI error. SB:%04X R1:%04X R2:%04X", i, r1, r2 ) ;
884 cnt++ ;
885 delay ( 10 ) ;
886 }
887 }
888 okay = ( cnt == 0 ) ; // True if working correctly
889 return ( okay ) ; // Return the result
890}
891
892void VS1053::begin()
893{
894 pinMode ( dreq_pin, INPUT ) ; // DREQ is an input
895 pinMode ( cs_pin, OUTPUT ) ; // The SCI and SDI signals
896 pinMode ( dcs_pin, OUTPUT ) ;
897 digitalWrite ( dcs_pin, HIGH ) ; // Start HIGH for SCI en SDI
898 digitalWrite ( cs_pin, HIGH ) ;
899 if ( shutdown_pin >= 0 ) // Shutdown in use?
900 {
901 pinMode ( shutdown_pin, OUTPUT ) ;
902 }
903 if ( shutdownx_pin >= 0 ) // Shutdown (inversed logic) in use?
904 {
905 pinMode ( shutdownx_pin, OUTPUT ) ;
906 }
907 output_enable ( false ) ; // Disable amplifier through shutdown pin(s)
908 delay ( 100 ) ;
909 // Init SPI in slow mode ( 0.2 MHz )
910 VS1053_SPI = SPISettings ( 200000, MSBFIRST, SPI_MODE0 ) ;
911 SPI.setDataMode ( SPI_MODE0 ) ;
912 SPI.setBitOrder ( MSBFIRST ) ;
913 //printDetails ( "Right after reset/startup" ) ;
914 delay ( 20 ) ;
915 //printDetails ( "20 msec after reset" ) ;
916 if ( testComm ( "Slow SPI, Testing VS1053 read/write registers..." ) )
917 {
918 // Most VS1053 modules will start up in midi mode. The result is that there is no audio
919 // when playing MP3. You can modify the board, but there is a more elegant way:
920 wram_write ( 0xC017, 3 ) ; // GPIO DDR = 3
921 wram_write ( 0xC019, 0 ) ; // GPIO ODATA = 0
922 delay ( 100 ) ;
923 //printDetails ( "After test loop" ) ;
924 softReset() ; // Do a soft reset
925 // Switch on the analog parts
926 write_register ( SCI_AUDATA, 44100 + 1 ) ; // 44.1kHz + stereo
927 // The next clocksetting allows SPI clocking at 5 MHz, 4 MHz is safe then.
928 write_register ( SCI_CLOCKF, 6 << 12 ) ; // Normal clock settings
929 // multiplyer 3.0 = 12.2 MHz
930 //SPI Clock to 4 MHz. Now you can set high speed SPI clock.
931 VS1053_SPI = SPISettings ( 5000000, MSBFIRST, SPI_MODE0 ) ;
932 write_register ( SCI_MODE, _BV ( SM_SDINEW ) | _BV ( SM_LINE1 ) ) ;
933 testComm ( "Fast SPI, Testing VS1053 read/write registers again..." ) ;
934 delay ( 10 ) ;
935 await_data_request() ;
936 endFillByte = wram_read ( 0x1E06 ) & 0xFF ;
937 dbgprint ( "endFillByte is %X", endFillByte ) ;
938 //printDetails ( "After last clocksetting" ) ;
939 delay ( 100 ) ;
940 }
941}
942
943void VS1053::setVolume ( uint8_t vol )
944{
945 // Set volume. Both left and right.
946 // Input value is 0..100. 100 is the loudest.
947 // Clicking reduced by using 0xf8 to 0x00 as limits.
948 uint16_t value ; // Value to send to SCI_VOL
949
950 if ( vol != curvol )
951 {
952 curvol = vol ; // Save for later use
953 value = map ( vol, 0, 100, 0xF8, 0x00 ) ; // 0..100% to one channel
954 value = ( value << 8 ) | value ;
955 write_register ( SCI_VOL, value ) ; // Volume left and right
956 output_enable ( vol != 0 ) ; // Enable/disable amplifier through shutdown pin(s)
957 }
958}
959
960void VS1053::setTone ( uint8_t *rtone ) // Set bass/treble (4 nibbles)
961{
962 // Set tone characteristics. See documentation for the 4 nibbles.
963 uint16_t value = 0 ; // Value to send to SCI_BASS
964 int i ; // Loop control
965
966 for ( i = 0 ; i < 4 ; i++ )
967 {
968 value = ( value << 4 ) | rtone[i] ; // Shift next nibble in
969 }
970 write_register ( SCI_BASS, value ) ; // Volume left and right
971}
972
973void VS1053::startSong()
974{
975 sdi_send_fillers ( 10 ) ;
976 output_enable ( true ) ; // Enable amplifier through shutdown pin(s)
977}
978
979bool VS1053::playChunk ( uint8_t* data, size_t len )
980{
981 return okay && sdi_send_buffer ( data, len ) ; // True if more data can be added to fifo
982}
983
984void VS1053::stopSong()
985{
986 uint16_t modereg ; // Read from mode register
987 int i ; // Loop control
988
989 sdi_send_fillers ( 2052 ) ;
990 output_enable ( false ) ; // Disable amplifier through shutdown pin(s)
991 delay ( 10 ) ;
992 write_register ( SCI_MODE, _BV ( SM_SDINEW ) | _BV ( SM_CANCEL ) ) ;
993 for ( i = 0 ; i < 200 ; i++ )
994 {
995 sdi_send_fillers ( 32 ) ;
996 modereg = read_register ( SCI_MODE ) ; // Read status
997 if ( ( modereg & _BV ( SM_CANCEL ) ) == 0 )
998 {
999 sdi_send_fillers ( 2052 ) ;
1000 //dbgprint ( "Song stopped correctly after %d msec", i * 10 ) ;
1001 return ;
1002 }
1003 delay ( 10 ) ;
1004 }
1005 printDetails ( "Song stopped incorrectly!" ) ;
1006}
1007
1008void VS1053::softReset()
1009{
1010 write_register ( SCI_MODE, _BV ( SM_SDINEW ) | _BV ( SM_RESET ) ) ;
1011 delay ( 10 ) ;
1012 await_data_request() ;
1013}
1014
1015void VS1053::printDetails ( const char *header )
1016{
1017 uint16_t regbuf[16] ;
1018 uint8_t i ;
1019
1020 dbgprint ( header ) ;
1021 dbgprint ( "REG Contents" ) ;
1022 dbgprint ( "--- -----" ) ;
1023 for ( i = 0 ; i <= SCI_num_registers ; i++ )
1024 {
1025 regbuf[i] = read_register ( i ) ;
1026 }
1027 for ( i = 0 ; i <= SCI_num_registers ; i++ )
1028 {
1029 delay ( 5 ) ;
1030 dbgprint ( "%3X - %5X", i, regbuf[i] ) ;
1031 }
1032}
1033
1034void VS1053::output_enable ( bool ena ) // Enable amplifier through shutdown pin(s)
1035{
1036 if ( shutdown_pin >= 0 ) // Shutdown in use?
1037 {
1038 digitalWrite ( shutdown_pin, !ena ) ; // Shut down or enable audio output
1039 }
1040 if ( shutdownx_pin >= 0 ) // Shutdown (inversed logic) in use?
1041 {
1042 digitalWrite ( shutdownx_pin, ena ) ; // Shut down or enable audio output
1043 }
1044}
1045
1046
1047void VS1053::AdjustRate ( long ppm2 ) // Fine tune the data rate
1048{
1049 write_register ( SCI_WRAMADDR, 0x1e07 ) ;
1050 write_register ( SCI_WRAM, ppm2 ) ;
1051 write_register ( SCI_WRAM, ppm2 >> 16 ) ;
1052 // oldClock4KHz = 0 forces adjustment calculation when rate checked.
1053 write_register ( SCI_WRAMADDR, 0x5b1c ) ;
1054 write_register ( SCI_WRAM, 0 ) ;
1055 // Write to AUDATA or CLOCKF checks rate and recalculates adjustment.
1056 write_register ( SCI_AUDATA, read_register ( SCI_AUDATA ) ) ;
1057}
1058
1059
1060// The object for the MP3 player
1061VS1053* vs1053player ;
1062
1063//**************************************************************************************************
1064// End VS1053 stuff. *
1065//**************************************************************************************************
1066
1067// Include software for the right display
1068#ifdef BLUETFT
1069#include "bluetft.h" // For ILI9163C or ST7735S 128x160 display
1070#endif
1071#ifdef ILI9341
1072#include "ILI9341.h" // For ILI9341 320x240 display
1073#endif
1074#ifdef OLED
1075#include "SSD1306.h" // For OLED I2C SD1306 64x128 display
1076#endif
1077#ifdef LCD1602I2C
1078#include "LCD1602.h" // For LCD 1602 display (I2C)
1079#endif
1080#ifdef DUMMYTFT
1081#include "Dummytft.h" // For Dummy display
1082#endif
1083#ifdef NEXTION
1084#include "NEXTION.h" // For NEXTION display
1085#endif
1086
1087
1088//**************************************************************************************************
1089// B L S E T *
1090//**************************************************************************************************
1091// Enable or disable the TFT backlight if configured. *
1092// May be called from interrupt level. *
1093//**************************************************************************************************
1094void IRAM_ATTR blset ( bool enable )
1095{
1096 if ( ini_block.tft_bl_pin >= 0 ) // Backlight for TFT control?
1097 {
1098 digitalWrite ( ini_block.tft_bl_pin, enable ) ; // Enable/disable backlight
1099 }
1100 if ( ini_block.tft_blx_pin >= 0 ) // Backlight for TFT (inversed logic) control?
1101 {
1102 digitalWrite ( ini_block.tft_blx_pin, !enable ) ; // Enable/disable backlight
1103 }
1104 if ( enable )
1105 {
1106 bltimer = 0 ; // Reset counter backlight time-out
1107 }
1108}
1109
1110
1111//**************************************************************************************************
1112// N V S O P E N *
1113//**************************************************************************************************
1114// Open Preferences with my-app namespace. Each application module, library, etc. *
1115// has to use namespace name to prevent key name collisions. We will open storage in *
1116// RW-mode (second parameter has to be false). *
1117//**************************************************************************************************
1118void nvsopen()
1119{
1120 if ( ! nvshandle ) // Opened already?
1121 {
1122 nvserr = nvs_open ( NAME, NVS_READWRITE, &nvshandle ) ; // No, open nvs
1123 if ( nvserr )
1124 {
1125 dbgprint ( "nvs_open failed!" ) ;
1126 }
1127 }
1128}
1129
1130
1131//**************************************************************************************************
1132// N V S C L E A R *
1133//**************************************************************************************************
1134// Clear all preferences. *
1135//**************************************************************************************************
1136esp_err_t nvsclear()
1137{
1138 nvsopen() ; // Be sure to open nvs
1139 return nvs_erase_all ( nvshandle ) ; // Clear all keys
1140}
1141
1142
1143//**************************************************************************************************
1144// N V S G E T S T R *
1145//**************************************************************************************************
1146// Read a string from nvs. *
1147//**************************************************************************************************
1148String nvsgetstr ( const char* key )
1149{
1150 static char nvs_buf[NVSBUFSIZE] ; // Buffer for contents
1151 size_t len = NVSBUFSIZE ; // Max length of the string, later real length
1152
1153 nvsopen() ; // Be sure to open nvs
1154 nvs_buf[0] = '\0' ; // Return empty string on error
1155 nvserr = nvs_get_str ( nvshandle, key, nvs_buf, &len ) ;
1156 if ( nvserr )
1157 {
1158 dbgprint ( "nvs_get_str failed %X for key %s, keylen is %d, len is %d!",
1159 nvserr, key, strlen ( key), len ) ;
1160 dbgprint ( "Contents: %s", nvs_buf ) ;
1161 }
1162 return String ( nvs_buf ) ;
1163}
1164
1165
1166//**************************************************************************************************
1167// N V S S E T S T R *
1168//**************************************************************************************************
1169// Put a key/value pair in nvs. Length is limited to allow easy read-back. *
1170// No writing if no change. *
1171//**************************************************************************************************
1172esp_err_t nvssetstr ( const char* key, String val )
1173{
1174 String curcont ; // Current contents
1175 bool wflag = true ; // Assume update or new key
1176
1177 //dbgprint ( "Setstring for %s: %s", key, val.c_str() ) ;
1178 if ( val.length() >= NVSBUFSIZE ) // Limit length of string to store
1179 {
1180 dbgprint ( "nvssetstr length failed!" ) ;
1181 return ESP_ERR_NVS_NOT_ENOUGH_SPACE ;
1182 }
1183 if ( nvssearch ( key ) ) // Already in nvs?
1184 {
1185 curcont = nvsgetstr ( key ) ; // Read current value
1186 wflag = ( curcont != val ) ; // Value change?
1187 }
1188 if ( wflag ) // Update or new?
1189 {
1190 //dbgprint ( "nvssetstr update value" ) ;
1191 nvserr = nvs_set_str ( nvshandle, key, val.c_str() ) ; // Store key and value
1192 if ( nvserr ) // Check error
1193 {
1194 dbgprint ( "nvssetstr failed!" ) ;
1195 }
1196 }
1197 return nvserr ;
1198}
1199
1200
1201//**************************************************************************************************
1202// N V S C H K E Y *
1203//**************************************************************************************************
1204// Change a keyname in in nvs. *
1205//**************************************************************************************************
1206void nvschkey ( const char* oldk, const char* newk )
1207{
1208 String curcont ; // Current contents
1209
1210 if ( nvssearch ( oldk ) ) // Old key in nvs?
1211 {
1212 curcont = nvsgetstr ( oldk ) ; // Read current value
1213 nvs_erase_key ( nvshandle, oldk ) ; // Remove key
1214 nvssetstr ( newk, curcont ) ; // Insert new
1215 }
1216}
1217
1218
1219//**************************************************************************************************
1220// C L A I M S P I *
1221//**************************************************************************************************
1222// Claim the SPI bus. Uses FreeRTOS semaphores. *
1223//**************************************************************************************************
1224void claimSPI ( const char* p )
1225{
1226 const TickType_t ctry = 10 ; // Time to wait for semaphore
1227 uint32_t count = 0 ; // Wait time in ticks
1228
1229 while ( xSemaphoreTake ( SPIsem, ctry ) != pdTRUE ) // Claim SPI bus
1230 {
1231 if ( count++ > 10 )
1232 {
1233 dbgprint ( "SPI semaphore not taken within %d ticks by CPU %d, id %s",
1234 count * ctry,
1235 xPortGetCoreID(),
1236 p ) ;
1237 }
1238 }
1239}
1240
1241
1242//**************************************************************************************************
1243// R E L E A S E S P I *
1244//**************************************************************************************************
1245// Free the the SPI bus. Uses FreeRTOS semaphores. *
1246//**************************************************************************************************
1247void releaseSPI()
1248{
1249 xSemaphoreGive ( SPIsem ) ; // Release SPI bus
1250}
1251
1252
1253//**************************************************************************************************
1254// Q U E U E F U N C *
1255//**************************************************************************************************
1256// Queue a special function for the play task. *
1257//**************************************************************************************************
1258void queuefunc ( int func )
1259{
1260 qdata_struct specchunk ; // Special function to queue
1261
1262 specchunk.datatyp = func ; // Put function in datatyp
1263 xQueueSend ( dataqueue, &specchunk, 200 ) ; // Send to queue
1264}
1265
1266
1267//**************************************************************************************************
1268// N V S S E A R C H *
1269//**************************************************************************************************
1270// Check if key exists in nvs. *
1271//**************************************************************************************************
1272bool nvssearch ( const char* key )
1273{
1274 size_t len = NVSBUFSIZE ; // Length of the string
1275
1276 nvsopen() ; // Be sure to open nvs
1277 nvserr = nvs_get_str ( nvshandle, key, NULL, &len ) ; // Get length of contents
1278 return ( nvserr == ESP_OK ) ; // Return true if found
1279}
1280
1281
1282//**************************************************************************************************
1283// T F T S E T *
1284//**************************************************************************************************
1285// Request to display a segment on TFT. Version for char* and String parameter. *
1286//**************************************************************************************************
1287void tftset ( uint16_t inx, const char *str )
1288{
1289 if ( inx < TFTSECS ) // Segment available on display
1290 {
1291 if ( str ) // String specified?
1292 {
1293 tftdata[inx].str = String ( str ) ; // Yes, set string
1294 }
1295 tftdata[inx].update_req = true ; // and request flag
1296 }
1297}
1298
1299void tftset ( uint16_t inx, String& str )
1300{
1301 if ( inx < TFTSECS ) // Segment available on display
1302 {
1303 tftdata[inx].str = str ; // Set string
1304 tftdata[inx].update_req = true ; // and request flag
1305 }
1306}
1307
1308
1309//**************************************************************************************************
1310// U T F 8 A S C I I *
1311//**************************************************************************************************
1312// UTF8-Decoder: convert UTF8-string to extended ASCII. *
1313// Convert a single Character from UTF8 to Extended ASCII. *
1314// Return "0" if a byte has to be ignored. *
1315//**************************************************************************************************
1316byte utf8ascii ( byte ascii )
1317{
1318 static const byte lut_C3[] =
1319 { "AAAAAAACEEEEIIIIDNOOOOO#0UUUU###aaaaaaaceeeeiiiidnooooo##uuuuyyy" } ;
1320 static byte c1 ; // Last character buffer
1321 byte res = 0 ; // Result, default 0
1322
1323 if ( ascii <= 0x7F ) // Standard ASCII-set 0..0x7F handling
1324 {
1325 c1 = 0 ;
1326 res = ascii ; // Return unmodified
1327 }
1328 else
1329 { //Windows Hungarian characters
1330 if (ascii == 0xE1) res = 'a';
1331 else if (ascii == 0xE9) res = 'e';
1332 else if (ascii == 0xED) res = 'i';
1333 else if (ascii == 0xF3) res = 'o';
1334 else if (ascii == 0xF6) res = 'o';
1335 else if (ascii == 0xF5) res = 'o';
1336 else if (ascii == 0xFA) res = 'u';
1337 else if (ascii == 0xFC) res = 'u';
1338 else if (ascii == 0xFB) res = 'u';
1339 //--------------------------------
1340 else if (ascii == 0xC1) res = 'a';
1341 else if (ascii == 0xC9) res = 'e';
1342 else if (ascii == 0xCD) res = 'i';
1343 else if (ascii == 0xD3) res = 'o';
1344 else if (ascii == 0xD6) res = 'o';
1345 else if (ascii == 0xD5) res = 'o';
1346 else if (ascii == 0xDA) res = 'u';
1347 else if (ascii == 0xDC) res = 'u';
1348 else if (ascii == 0xDB) res = 'u';
1349 else switch ( c1 ) // Conversion depending on first UTF8-character
1350 {
1351 case 0xC2: res = '~' ;
1352 break ;
1353 case 0xC3:
1354 res = lut_C3[ascii - 128] ;
1355 break ;
1356 case 0x82: if ( ascii == 0xAC )
1357 {
1358 res = 'E' ; // Special case Euro-symbol
1359 }
1360 }
1361 c1 = ascii ; // Remember actual character
1362 }
1363 return res ; // Otherwise: return zero, if character has to be ignored
1364}
1365
1366
1367//**************************************************************************************************
1368// U T F 8 A S C I I *
1369//**************************************************************************************************
1370// In Place conversion UTF8-string to Extended ASCII (ASCII is shorter!). *
1371//**************************************************************************************************
1372void utf8ascii ( char* s )
1373{
1374 int i, k = 0 ; // Indexes for in en out string
1375 char c ;
1376
1377 for ( i = 0 ; s[i] ; i++ ) // For every input character
1378 {
1379 c = utf8ascii ( s[i] ) ; // Translate if necessary
1380 if ( c ) // Good translation?
1381 {
1382 s[k++] = c ; // Yes, put in output string
1383 }
1384 }
1385 s[k] = 0 ; // Take care of delimeter
1386}
1387
1388
1389//**************************************************************************************************
1390// U T F 8 A S C I I *
1391//**************************************************************************************************
1392// Conversion UTF8-String to Extended ASCII String. *
1393//**************************************************************************************************
1394String utf8ascii ( const char* s )
1395{
1396 int i ; // Index for input string
1397 char c ;
1398 String res = "" ; // Result string
1399
1400 for ( i = 0 ; s[i] ; i++ ) // For every input character
1401 {
1402 c = utf8ascii ( s[i] ) ; // Translate if necessary
1403 if ( c ) // Good translation?
1404 {
1405 res += String ( c ) ; // Yes, put in output string
1406 }
1407 }
1408 return res ;
1409}
1410
1411
1412//**************************************************************************************************
1413// D B G P R I N T *
1414//**************************************************************************************************
1415// Send a line of info to serial output. Works like vsprintf(), but checks the DEBUG flag. *
1416// Print only if DEBUG flag is true. Always returns the formatted string. *
1417//**************************************************************************************************
1418char* dbgprint ( const char* format, ... )
1419{
1420 static char sbuf[DEBUG_BUFFER_SIZE] ; // For debug lines
1421 va_list varArgs ; // For variable number of params
1422
1423 va_start ( varArgs, format ) ; // Prepare parameters
1424 vsnprintf ( sbuf, sizeof(sbuf), format, varArgs ) ; // Format the message
1425 va_end ( varArgs ) ; // End of using parameters
1426 if ( DEBUG ) // DEBUG on?
1427 {
1428 Serial.print ( "D: " ) ; // Yes, print prefix
1429 Serial.println ( sbuf ) ; // and the info
1430 }
1431 return sbuf ; // Return stored string
1432}
1433
1434
1435
1436
1437//**************************************************************************************************
1438// S E L E C T N E X T S D N O D E *
1439//**************************************************************************************************
1440// Select the next or previous mp3 file from SD. If the last selected song was random, the next *
1441// track is a random one too. Otherwise the next/previous node is choosen. *
1442// If nodeID is "0" choose a random nodeID. *
1443// Delta is +1 or -1 for next or previous track. *
1444// The nodeID will be returned to the caller. *
1445//**************************************************************************************************
1446String selectnextSDnode ( String curnod, int16_t delta )
1447{
1448 int16_t inx, inx2 ; // Position in nodelist
1449
1450 if ( hostreq ) // Host request already set?
1451 {
1452 return "" ; // Yes, no action
1453 }
1454 dbgprint ( "SD_currentnode is %s, "
1455 "curnod is %s, "
1456 "delta is %d",
1457 SD_currentnode.c_str(),
1458 curnod.c_str(),
1459 delta ) ;
1460 if ( SD_currentnode == "0" ) // Random playing?
1461 {
1462 return SD_currentnode ; // Yes, return random nodeID
1463 }
1464 else
1465 {
1466 inx = SD_nodelist.indexOf ( curnod ) ; // Get position of current nodeID in list
1467 if ( delta > 0 ) // Next track?
1468 {
1469 inx += curnod.length() + 1 ; // Get position of next nodeID in list
1470 if ( inx >= SD_nodelist.length() ) // End of list?
1471 {
1472 inx = 0 ; // Yes, wrap around
1473 }
1474 }
1475 else
1476 {
1477 if ( inx == 0 ) // At the begin of the list?
1478 {
1479 inx = SD_nodelist.length() ; // Yes, goto end of list
1480 }
1481 inx-- ; // Index of delimeter of previous node ID
1482 while ( ( inx > 0 ) &&
1483 ( SD_nodelist[inx - 1] != '\n' ) )
1484 {
1485 inx-- ;
1486 }
1487 }
1488 inx2 = SD_nodelist.indexOf ( "\n", inx ) ; // Find end of node ID
1489 }
1490 return SD_nodelist.substring ( inx, inx2 ) ; // Return nodeID
1491}
1492
1493
1494//**************************************************************************************************
1495// G E T S D F I L E N A M E *
1496//**************************************************************************************************
1497// Translate the nodeID of a track to the full filename that can be used as a station. *
1498// If nodeID is "0" choose a random nodeID. *
1499//**************************************************************************************************
1500String getSDfilename ( String nodeID )
1501{
1502 String res ; // Function result
1503 File root, file ; // Handle to root and directory entry
1504 uint16_t n, i ; // Current seqnr and counter in directory
1505 int16_t inx ; // Position in nodeID
1506 const char* p = "/" ; // Points to directory/file
1507 uint16_t rndnum ; // Random index in SD_nodelist
1508 int nodeinx = 0 ; // Points to node ID in SD_nodecount
1509 int nodeinx2 ; // Points to end of node ID in SD_nodecount
1510
1511 SD_currentnode = nodeID ; // Save current node
1512 if ( nodeID == "0" ) // Empty parameter?
1513 {
1514 dbgprint ( "getSDfilename random choice" ) ;
1515 rndnum = random ( SD_nodecount ) ; // Yes, choose a random node
1516 for ( i = 0 ; i < rndnum ; i++ ) // Find the node ID
1517 {
1518 // Search to begin of the random node by skipping lines
1519 nodeinx = SD_nodelist.indexOf ( "\n", nodeinx ) + 1 ;
1520 }
1521 nodeinx2 = SD_nodelist.indexOf ( "\n", nodeinx ) ; // Find end of node ID
1522 nodeID = SD_nodelist.substring ( nodeinx,
1523 nodeinx2 ) ; // Get node ID
1524 }
1525 dbgprint ( "getSDfilename requested node ID is %s", // Show requeste node ID
1526 nodeID.c_str() ) ;
1527 while ( ( n = nodeID.toInt() ) ) // Next sequence in current level
1528 {
1529 inx = nodeID.indexOf ( "," ) ; // Find position of comma
1530 if ( inx >= 0 )
1531 {
1532 nodeID = nodeID.substring ( inx + 1 ) ; // Remove sequence in this level from nodeID
1533 }
1534 claimSPI ( "sdopen" ) ; // Claim SPI bus
1535 root = SD.open ( p ) ; // Open the directory (this level)
1536 releaseSPI() ; // Release SPI bus
1537 for ( i = 1 ; i <= n ; i++ )
1538 {
1539 claimSPI ( "sdopenxt" ) ; // Claim SPI bus
1540 file = root.openNextFile() ; // Get next directory entry
1541 releaseSPI() ; // Release SPI bus
1542 delay ( 10 ) ; // Allow playtask
1543 }
1544 p = file.name() ; // Points to directory- or file name
1545 }
1546 res = String ( "localhost" ) + String ( p ) ; // Format result
1547 return res ; // Return full station spec
1548}
1549
1550
1551//**************************************************************************************************
1552// L I S T S D T R A C K S *
1553//**************************************************************************************************
1554// Search all MP3 files on directory of SD card. Return the number of files found. *
1555// A "node" of max. 4 levels ( the subdirectory level) will be generated for every file. *
1556// The numbers within the node-array is the sequence number of the file/directory in that *
1557// subdirectory. *
1558// A node ID is a string like "2,1,4,0", which means the 4th file in the first directory *
1559// of the second directory. *
1560// The list will be send to the webinterface if parameter "send"is true. *
1561//**************************************************************************************************
1562int listsdtracks ( const char * dirname, int level = 0, bool send = true )
1563{
1564 const uint16_t SD_MAXDEPTH = 4 ; // Maximum depts. Note: see mp3play_html.
1565 static uint16_t fcount, oldfcount ; // Total number of files
1566 static uint16_t SD_node[SD_MAXDEPTH + 1] ; // Node ISs, max levels deep
1567 static String SD_outbuf ; // Output buffer for cmdclient
1568 uint16_t ldirname ; // Length of dirname to remove from filename
1569 File root, file ; // Handle to root and directory entry
1570 String filename ; // Copy of filename for lowercase test
1571 uint16_t i ; // Loop control to compute single node id
1572 String tmpstr ; // Tijdelijke opslag node ID
1573
1574 if ( strcmp ( dirname, "/" ) == 0 ) // Are we at the root directory?
1575 {
1576 fcount = 0 ; // Yes, reset count
1577 memset ( SD_node, 0, sizeof(SD_node) ) ; // And sequence counters
1578 SD_outbuf = String() ; // And output buffer
1579 SD_nodelist = String() ; // And nodelist
1580 if ( !SD_okay ) // See if known card
1581 {
1582 if ( send )
1583 {
1584 cmdclient.println ( "0/No tracks found" ) ; // No SD card, emppty list
1585 }
1586 return 0 ;
1587 }
1588 }
1589 oldfcount = fcount ; // To see if files found in this directory
1590 //dbgprint ( "SD directory is %s", dirname ) ; // Show current directory
1591 ldirname = strlen ( dirname ) ; // Length of dirname to remove from filename
1592 claimSPI ( "sdopen2" ) ; // Claim SPI bus
1593 root = SD.open ( dirname ) ; // Open the current directory level
1594 releaseSPI() ; // Release SPI bus
1595 if ( !root || !root.isDirectory() ) // Success?
1596 {
1597 dbgprint ( "%s is not a directory or not root", // No, print debug message
1598 dirname ) ;
1599 return fcount ; // and return
1600 }
1601 while ( true ) // Find all mp3 files
1602 {
1603 claimSPI ( "opennextf" ) ; // Claim SPI bus
1604 file = root.openNextFile() ; // Try to open next
1605 releaseSPI() ; // Release SPI bus
1606 if ( !file )
1607 {
1608 break ; // End of list
1609 }
1610 SD_node[level]++ ; // Set entry sequence of current level
1611 if ( file.name()[0] == '.' ) // Skip hidden directories
1612 {
1613 continue ;
1614 }
1615 if ( file.isDirectory() ) // Is it a directory?
1616 {
1617 if ( level < SD_MAXDEPTH ) // Yes, dig deeper
1618 {
1619 listsdtracks ( file.name(), level + 1, send ) ; // Note: called recursively
1620 SD_node[level + 1] = 0 ; // Forget counter for one level up
1621 }
1622 }
1623 else
1624 {
1625 filename = String ( file.name() ) ; // Copy filename
1626 filename.toLowerCase() ; // Force lowercase
1627 if ( filename.endsWith ( ".mp3" ) ) // It is a file, but is it an MP3?
1628 {
1629 fcount++ ; // Yes, count total number of MP3 files
1630 tmpstr = String() ; // Empty
1631 for ( i = 0 ; i < SD_MAXDEPTH ; i++ ) // Add a line containing the node to SD_outbuf
1632 {
1633 if ( i ) // Need to add separating comma?
1634 {
1635 tmpstr += String ( "," ) ; // Yes, add comma
1636 }
1637 tmpstr += String ( SD_node[i] ) ; // Add sequence number
1638 }
1639 if ( send ) // Need to add to string for webinterface?
1640 {
1641 SD_outbuf += tmpstr + // Form line for mp3play_html page
1642 utf8ascii ( file.name() + // Filename starts after directoryname
1643 ldirname ) +
1644 String ( "\n" ) ;
1645 }
1646 SD_nodelist += tmpstr + String ( "\n" ) ; // Add to nodelist
1647 //dbgprint ( "Track: %s", // Show debug info
1648 // file.name() + ldirname ) ;
1649 if ( SD_outbuf.length() > 1000 ) // Buffer full?
1650 {
1651 cmdclient.print ( SD_outbuf ) ; // Yes, send it
1652 SD_outbuf = String() ; // Clear buffer
1653 }
1654 }
1655 }
1656 if ( send )
1657 {
1658 mp3loop() ; // Keep playing
1659 }
1660 }
1661 if ( fcount != oldfcount ) // Files in this directory?
1662 {
1663 SD_outbuf += String ( "-1/ \n" ) ; // Spacing in list
1664 }
1665 if ( SD_outbuf.length() ) // Flush buffer if not empty
1666 {
1667 cmdclient.print ( SD_outbuf ) ; // Filled, send it
1668 SD_outbuf = String() ; // Continue with empty buffer
1669 }
1670 return fcount ; // Return number of MP3s (sofar)
1671}
1672
1673
1674//**************************************************************************************************
1675// G E T E N C R Y P T I O N T Y P E *
1676//**************************************************************************************************
1677// Read the encryption type of the network and return as a 4 byte name *
1678//**************************************************************************************************
1679const char* getEncryptionType ( wifi_auth_mode_t thisType )
1680{
1681 switch ( thisType )
1682 {
1683 case WIFI_AUTH_OPEN:
1684 return "OPEN" ;
1685 case WIFI_AUTH_WEP:
1686 return "WEP" ;
1687 case WIFI_AUTH_WPA_PSK:
1688 return "WPA_PSK" ;
1689 case WIFI_AUTH_WPA2_PSK:
1690 return "WPA2_PSK" ;
1691 case WIFI_AUTH_WPA_WPA2_PSK:
1692 return "WPA_WPA2_PSK" ;
1693 case WIFI_AUTH_MAX:
1694 return "MAX" ;
1695 default:
1696 break ;
1697 }
1698 return "????" ;
1699}
1700
1701
1702//**************************************************************************************************
1703// L I S T N E T W O R K S *
1704//**************************************************************************************************
1705// List the available networks. *
1706// Acceptable networks are those who have an entry in the preferences. *
1707// SSIDs of available networks will be saved for use in webinterface. *
1708//**************************************************************************************************
1709void listNetworks()
1710{
1711 WifiInfo_t winfo ; // Entry from wifilist
1712 wifi_auth_mode_t encryption ; // TKIP(WPA), WEP, etc.
1713 const char* acceptable ; // Netwerk is acceptable for connection
1714 int i, j ; // Loop control
1715
1716 dbgprint ( "Scan Networks" ) ; // Scan for nearby networks
1717 numSsid = WiFi.scanNetworks() ;
1718 dbgprint ( "Scan completed" ) ;
1719 if ( numSsid <= 0 )
1720 {
1721 dbgprint ( "Couldn't get a wifi connection" ) ;
1722 return ;
1723 }
1724 // print the list of networks seen:
1725 dbgprint ( "Number of available networks: %d",
1726 numSsid ) ;
1727 // Print the network number and name for each network found and
1728 for ( i = 0 ; i < numSsid ; i++ )
1729 {
1730 acceptable = "" ; // Assume not acceptable
1731 for ( j = 0 ; j < wifilist.size() ; j++ ) // Search in wifilist
1732 {
1733 winfo = wifilist[j] ; // Get one entry
1734 if ( WiFi.SSID(i).indexOf ( winfo.ssid ) == 0 ) // Is this SSID acceptable?
1735 {
1736 acceptable = "Acceptable" ;
1737 break ;
1738 }
1739 }
1740 encryption = WiFi.encryptionType ( i ) ;
1741 dbgprint ( "%2d - %-25s Signal: %3d dBm, Encryption %4s, %s",
1742 i + 1, WiFi.SSID(i).c_str(), WiFi.RSSI(i),
1743 getEncryptionType ( encryption ),
1744 acceptable ) ;
1745 // Remember this network for later use
1746 networks += WiFi.SSID(i) + String ( "|" ) ;
1747 }
1748 dbgprint ( "End of list" ) ;
1749}
1750
1751
1752//**************************************************************************************************
1753// T I M E R 1 0 S E C *
1754//**************************************************************************************************
1755// Extra watchdog. Called every 10 seconds. *
1756// If totalcount has not been changed, there is a problem and playing will stop. *
1757// Note that calling timely procedures within this routine or in called functions will *
1758// cause a crash! *
1759//**************************************************************************************************
1760void IRAM_ATTR timer10sec()
1761{
1762 static uint32_t oldtotalcount = 7321 ; // Needed for change detection
1763 static uint8_t morethanonce = 0 ; // Counter for succesive fails
1764 uint32_t bytesplayed ; // Bytes send to MP3 converter
1765
1766 if ( datamode & ( INIT | HEADER | DATA | // Test op playing
1767 METADATA | PLAYLISTINIT |
1768 PLAYLISTHEADER |
1769 PLAYLISTDATA ) )
1770 {
1771 bytesplayed = totalcount - oldtotalcount ; // Nunber of bytes played in the 10 seconds
1772 oldtotalcount = totalcount ; // Save for comparison in next cycle
1773 if ( bytesplayed == 0 ) // Still playing?
1774 {
1775 if ( morethanonce > 10 ) // No! Happened too many times?
1776 {
1777 ESP.restart() ; // Reset the CPU, probably no return
1778 }
1779 if ( datamode & ( PLAYLISTDATA | // In playlist mode?
1780 PLAYLISTINIT |
1781 PLAYLISTHEADER ) )
1782 {
1783 playlist_num = 0 ; // Yes, end of playlist
1784 }
1785 if ( ( morethanonce > 0 ) || // Happened more than once?
1786 ( playlist_num > 0 ) ) // Or playlist active?
1787 {
1788 datamode = STOPREQD ; // Stop player
1789 ini_block.newpreset++ ; // Yes, try next channel
1790 }
1791 morethanonce++ ; // Count the fails
1792 }
1793 else
1794 {
1795 // // Data has been send to MP3 decoder
1796 // Bitrate in kbits/s is bytesplayed / 10 / 1000 * 8
1797 mbitrate = ( bytesplayed + 625 ) / 1250 ; // Measured bitrate
1798 morethanonce = 0 ; // Data seen, reset failcounter
1799 }
1800 }
1801}
1802
1803
1804//**************************************************************************************************
1805// T I M E R 1 0 0 *
1806//**************************************************************************************************
1807// Called every 100 msec on interrupt level, so must be in IRAM and no lengthy operations *
1808// allowed. *
1809//**************************************************************************************************
1810void IRAM_ATTR timer100()
1811{
1812 sv int16_t count10sec = 0 ; // Counter for activatie 10 seconds process
1813 sv int16_t eqcount = 0 ; // Counter for equal number of clicks
1814 sv int16_t oldclickcount = 0 ; // To detect difference
1815
1816 if ( ++count10sec == 100 ) // 10 seconds passed?
1817 {
1818 timer10sec() ; // Yes, do 10 second procedure
1819 count10sec = 0 ; // Reset count
1820 }
1821 if ( ( count10sec % 10 ) == 0 ) // One second over?
1822 {
1823 scaniocount = scanios ; // TEST*TEST*TEST
1824 scanios = 0 ;
1825 if ( ++timeinfo.tm_sec >= 60 ) // Yes, update number of seconds
1826 {
1827 timeinfo.tm_sec = 0 ; // Wrap after 60 seconds
1828 if ( ++timeinfo.tm_min >= 60 )
1829 {
1830 timeinfo.tm_min = 0 ; // Wrap after 60 minutes
1831 if ( ++timeinfo.tm_hour >= 24 )
1832 {
1833 timeinfo.tm_hour = 0 ; // Wrap after 24 hours
1834 }
1835 }
1836 }
1837 time_req = true ; // Yes, show current time request
1838 if ( ++bltimer == BL_TIME ) // Time to blank the TFT screen?
1839 {
1840 bltimer = 0 ; // Yes, reset counter
1841 blset ( false ) ; // Disable TFT (backlight)
1842 }
1843 }
1844 // Handle rotary encoder. Inactivity counter will be reset by encoder interrupt
1845 if ( ++enc_inactivity == 36000 ) // Count inactivity time
1846 {
1847 enc_inactivity = 1000 ; // Prevent wrap
1848 }
1849 // Now detection of single/double click of rotary encoder switch
1850 if ( clickcount ) // Any click?
1851 {
1852 if ( oldclickcount == clickcount ) // Yes, stable situation?
1853 {
1854 if ( ++eqcount == 4 ) // Long time stable?
1855 {
1856 eqcount = 0 ;
1857 if ( clickcount > 2 ) // Triple click?
1858 {
1859 tripleclick = true ; // Yes, set result
1860 }
1861 else if ( clickcount == 2 ) // Double click?
1862 {
1863 doubleclick = true ; // Yes, set result
1864 }
1865 else
1866 {
1867 singleclick = true ; // Just one click seen
1868 }
1869 clickcount = 0 ; // Reset number of clicks
1870 }
1871 }
1872 else
1873 {
1874 oldclickcount = clickcount ; // To detect change
1875 eqcount = 0 ; // Not stable, reset count
1876 }
1877 }
1878}
1879
1880
1881//**************************************************************************************************
1882// I S R _ I R *
1883//**************************************************************************************************
1884// Interrupts received from VS1838B on every change of the signal. *
1885// Intervals are 640 or 1640 microseconds for data. syncpulses are 3400 micros or longer. *
1886// Input is complete after 65 level changes. *
1887// Only the last 32 level changes are significant and will be handed over to common data. *
1888//**************************************************************************************************
1889void IRAM_ATTR isr_IR()
1890{
1891 sv uint32_t t0 = 0 ; // To get the interval
1892 sv uint32_t ir_locvalue = 0 ; // IR code
1893 sv int ir_loccount = 0 ; // Length of code
1894 uint32_t t1, intval ; // Current time and interval since last change
1895 uint32_t mask_in = 2 ; // Mask input for conversion
1896 uint16_t mask_out = 1 ; // Mask output for conversion
1897
1898 t1 = micros() ; // Get current time
1899 intval = t1 - t0 ; // Compute interval
1900 t0 = t1 ; // Save for next compare
1901 if ( ( intval > 300 ) && ( intval < 800 ) ) // Short pulse?
1902 {
1903 ir_locvalue = ir_locvalue << 1 ; // Shift in a "zero" bit
1904 ir_loccount++ ; // Count number of received bits
1905 ir_0 = ( ir_0 * 3 + intval ) / 4 ; // Compute average durartion of a short pulse
1906 }
1907 else if ( ( intval > 1400 ) && ( intval < 1900 ) ) // Long pulse?
1908 {
1909 ir_locvalue = ( ir_locvalue << 1 ) + 1 ; // Shift in a "one" bit
1910 ir_loccount++ ; // Count number of received bits
1911 ir_1 = ( ir_1 * 3 + intval ) / 4 ; // Compute average durartion of a short pulse
1912 }
1913 else if ( ir_loccount == 65 ) // Value is correct after 65 level changes
1914 {
1915 while ( mask_in ) // Convert 32 bits to 16 bits
1916 {
1917 if ( ir_locvalue & mask_in ) // Bit set in pattern?
1918 {
1919 ir_value |= mask_out ; // Set set bit in result
1920 }
1921 mask_in <<= 2 ; // Shift input mask 2 positions
1922 mask_out <<= 1 ; // Shift output mask 1 position
1923 }
1924 ir_loccount = 0 ; // Ready for next input
1925 }
1926 else
1927 {
1928 ir_locvalue = 0 ; // Reset decoding
1929 ir_loccount = 0 ;
1930 }
1931}
1932
1933
1934//**************************************************************************************************
1935// I S R _ E N C _ S W I T C H *
1936//**************************************************************************************************
1937// Interrupts received from rotary encoder switch. *
1938//**************************************************************************************************
1939void IRAM_ATTR isr_enc_switch()
1940{
1941 sv uint32_t oldtime = 0 ; // Time in millis previous interrupt
1942 sv bool sw_state ; // True is pushed (LOW)
1943 bool newstate ; // Current state of input signal
1944 uint32_t newtime ; // Current timestamp
1945
1946 // Read current state of SW pin
1947 newstate = ( digitalRead ( ini_block.enc_sw_pin ) == LOW ) ;
1948 newtime = millis() ;
1949 if ( newtime == oldtime ) // Debounce
1950 {
1951 return ;
1952 }
1953 if ( newstate != sw_state ) // State changed?
1954 {
1955 sw_state = newstate ; // Yes, set current (new) state
1956 if ( !sw_state ) // SW released?
1957 {
1958 if ( ( newtime - oldtime ) > 1000 ) // More than 1 second?
1959 {
1960 longclick = true ; // Yes, register longclick
1961 }
1962 else
1963 {
1964 clickcount++ ; // Yes, click detected
1965 }
1966 enc_inactivity = 0 ; // Not inactive anymore
1967 }
1968 }
1969 oldtime = newtime ; // For next compare
1970}
1971
1972
1973//**************************************************************************************************
1974// I S R _ E N C _ T U R N *
1975//**************************************************************************************************
1976// Interrupts received from rotary encoder (clk signal) knob turn. *
1977// The encoder is a Manchester coded device, the outcomes (-1,0,1) of all the previous state and *
1978// actual state are stored in the enc_states[]. *
1979// Full_status is a 4 bit variable, the upper 2 bits are the previous encoder values, the lower *
1980// ones are the actual ones. *
1981// 4 bits cover all the possible previous and actual states of the 2 PINs, so this variable is *
1982// the index enc_states[]. *
1983// No debouncing is needed, because only the valid states produce values different from 0. *
1984// Rotation is 4 if position is moved from one fixed position to the next, so it is devided by 4. *
1985//**************************************************************************************************
1986void IRAM_ATTR isr_enc_turn()
1987{
1988 sv uint32_t old_state = 0x0001 ; // Previous state
1989 sv int16_t locrotcount = 0 ; // Local rotation count
1990 uint8_t act_state = 0 ; // The current state of the 2 PINs
1991 uint8_t inx ; // Index in enc_state
1992 sv const int8_t enc_states [] = // Table must be in DRAM (iram safe)
1993 { 0, // 00 -> 00
1994 -1, // 00 -> 01 // dt goes HIGH
1995 1, // 00 -> 10
1996 0, // 00 -> 11
1997 1, // 01 -> 00 // dt goes LOW
1998 0, // 01 -> 01
1999 0, // 01 -> 10
2000 -1, // 01 -> 11 // clk goes HIGH
2001 -1, // 10 -> 00 // clk goes LOW
2002 0, // 10 -> 01
2003 0, // 10 -> 10
2004 1, // 10 -> 11 // dt goes HIGH
2005 0, // 11 -> 00
2006 1, // 11 -> 01 // clk goes LOW
2007 -1, // 11 -> 10 // dt goes HIGH
2008 0 // 11 -> 11
2009 } ;
2010 // Read current state of CLK, DT pin. Result is a 2 bit binary number: 00, 01, 10 or 11.
2011 act_state = ( digitalRead ( ini_block.enc_clk_pin ) << 1 ) +
2012 digitalRead ( ini_block.enc_dt_pin ) ;
2013 inx = ( old_state << 2 ) + act_state ; // Form index in enc_states
2014 locrotcount += enc_states[inx] ; // Get delta: 0, +1 or -1
2015 if ( locrotcount == 4 )
2016 {
2017 rotationcount++ ; // Divide by 4
2018 locrotcount = 0 ;
2019 }
2020 else if ( locrotcount == -4 )
2021 {
2022 rotationcount-- ; // Divide by 4
2023 locrotcount = 0 ;
2024 }
2025 old_state = act_state ; // Remember current status
2026 enc_inactivity = 0 ;
2027}
2028
2029
2030//**************************************************************************************************
2031// S H O W S T R E A M T I T L E *
2032//**************************************************************************************************
2033// Show artist and songtitle if present in metadata. *
2034// Show always if full=true. *
2035//**************************************************************************************************
2036void showstreamtitle ( const char *ml, bool full )
2037{
2038 char* p1 ;
2039 char* p2 ;
2040 char streamtitle[150] ; // Streamtitle from metadata
2041
2042 if ( strstr ( ml, "StreamTitle=" ) )
2043 {
2044 dbgprint ( "Streamtitle found, %d bytes", strlen ( ml ) ) ;
2045 dbgprint ( ml ) ;
2046 p1 = (char*)ml + 12 ; // Begin of artist and title
2047 if ( ( p2 = strstr ( ml, ";" ) ) ) // Search for end of title
2048 {
2049 if ( *p1 == '\'' ) // Surrounded by quotes?
2050 {
2051 p1++ ;
2052 p2-- ;
2053 }
2054 *p2 = '\0' ; // Strip the rest of the line
2055 }
2056 // Save last part of string as streamtitle. Protect against buffer overflow
2057 strncpy ( streamtitle, p1, sizeof ( streamtitle ) ) ;
2058 streamtitle[sizeof ( streamtitle ) - 1] = '\0' ;
2059 }
2060 else if ( full )
2061 {
2062 // Info probably from playlist
2063 strncpy ( streamtitle, ml, sizeof ( streamtitle ) ) ;
2064 streamtitle[sizeof ( streamtitle ) - 1] = '\0' ;
2065 }
2066 else
2067 {
2068 icystreamtitle = "" ; // Unknown type
2069 return ; // Do not show
2070 }
2071 // Save for status request from browser and for MQTT
2072 icystreamtitle = streamtitle ;
2073 if ( ( p1 = strstr ( streamtitle, " - " ) ) ) // look for artist/title separator
2074 {
2075 p2 = p1 + 3 ; // 2nd part of text at this position
2076 if ( displaytype == T_NEXTION )
2077 {
2078 *p1++ = '\\' ; // Found: replace 3 characters by "\r"
2079 *p1++ = 'r' ; // Found: replace 3 characters by "\r"
2080 }
2081 else
2082 {
2083 *p1++ = '\n' ; // Found: replace 3 characters by newline
2084 }
2085 if ( *p2 == ' ' ) // Leading space in title?
2086 {
2087 p2++ ;
2088 }
2089 strcpy ( p1, p2 ) ; // Shift 2nd part of title 2 or 3 places
2090 }
2091 tftset ( 1, streamtitle ) ; // Set screen segment text middle part
2092}
2093
2094
2095//**************************************************************************************************
2096// S T O P _ M P 3 C L I E N T *
2097//**************************************************************************************************
2098// Disconnect from the server. *
2099//**************************************************************************************************
2100void stop_mp3client ()
2101{
2102
2103 while ( mp3client.connected() )
2104 {
2105 dbgprint ( "Stopping client" ) ; // Stop connection to host
2106 mp3client.flush() ;
2107 mp3client.stop() ;
2108 delay ( 500 ) ;
2109 }
2110 mp3client.flush() ; // Flush stream client
2111 mp3client.stop() ; // Stop stream client
2112}
2113
2114
2115//**************************************************************************************************
2116// C O N N E C T T O H O S T *
2117//**************************************************************************************************
2118// Connect to the Internet radio server specified by newpreset. *
2119//**************************************************************************************************
2120bool connecttohost()
2121{
2122 int inx ; // Position of ":" in hostname
2123 uint16_t port = 80 ; // Port number for host
2124 String extension = "/" ; // May be like "/mp3" in "skonto.ls.lv:8002/mp3"
2125 String hostwoext = host ; // Host without extension and portnumber
2126 String auth ; // For basic authentication
2127
2128 stop_mp3client() ; // Disconnect if still connected
2129 dbgprint ( "Connect to new host %s", host.c_str() ) ;
2130 tftset ( 0, "ESP32-Radio" ) ; // Set screen segment text top line
2131 displaytime ( "" ) ; // Clear time on TFT screen
2132 datamode = INIT ; // Start default in metamode
2133 chunked = false ; // Assume not chunked
2134 if ( host.endsWith ( ".m3u" ) ) // Is it an m3u playlist?
2135 {
2136 playlist = host ; // Save copy of playlist URL
2137 datamode = PLAYLISTINIT ; // Yes, start in PLAYLIST mode
2138 if ( playlist_num == 0 ) // First entry to play?
2139 {
2140 playlist_num = 1 ; // Yes, set index
2141 }
2142 dbgprint ( "Playlist request, entry %d", playlist_num ) ;
2143 }
2144 // In the URL there may be an extension, like noisefm.ru:8000/play.m3u&t=.m3u
2145 inx = host.indexOf ( "/" ) ; // Search for begin of extension
2146 if ( inx > 0 ) // Is there an extension?
2147 {
2148 extension = host.substring ( inx ) ; // Yes, change the default
2149 hostwoext = host.substring ( 0, inx ) ; // Host without extension
2150 }
2151 // In the host there may be a portnumber
2152 inx = hostwoext.indexOf ( ":" ) ; // Search for separator
2153 if ( inx >= 0 ) // Portnumber available?
2154 {
2155 port = host.substring ( inx + 1 ).toInt() ; // Get portnumber as integer
2156 hostwoext = host.substring ( 0, inx ) ; // Host without portnumber
2157 }
2158 dbgprint ( "Connect to %s on port %d, extension %s",
2159 hostwoext.c_str(), port, extension.c_str() ) ;
2160 if ( mp3client.connect ( hostwoext.c_str(), port ) )
2161 {
2162 dbgprint ( "Connected to server" ) ;
2163 auth = nvsgetstr ( "basicauth" ) ; // Use basic authentication?
2164 if ( auth != "" ) // Should be user:passwd
2165 {
2166 auth = base64::encode ( auth.c_str() ) ; // Encode
2167 auth = String ( "Authorization: Basic " ) +
2168 auth + String ( "\r\n" ) ;
2169 }
2170 mp3client.print ( String ( "GET " ) +
2171 extension +
2172 String ( " HTTP/1.1\r\n" ) +
2173 String ( "Host: " ) +
2174 hostwoext +
2175 String ( "\r\n" ) +
2176 String ( "Icy-MetaData:1\r\n" ) +
2177 auth +
2178 String ( "Connection: close\r\n\r\n" ) ) ;
2179 return true ;
2180 }
2181 dbgprint ( "Request %s failed!", host.c_str() ) ;
2182 return false ;
2183}
2184
2185
2186//**************************************************************************************************
2187// S S C O N V *
2188//**************************************************************************************************
2189// Convert an array with 4 "synchsafe integers" to a number. *
2190// There are 7 bits used per byte. *
2191//**************************************************************************************************
2192uint32_t ssconv ( const uint8_t* bytes )
2193{
2194 uint32_t res = 0 ; // Result of conversion
2195 uint8_t i ; // Counter number of bytes to convert
2196
2197 for ( i = 0 ; i < 4 ; i++ ) // Handle 4 bytes
2198 {
2199 res = res * 128 + bytes[i] ; // Convert next 7 bits
2200 }
2201 return res ; // Return the result
2202}
2203
2204
2205//**************************************************************************************************
2206// H A N D L E _ I D 3 *
2207//**************************************************************************************************
2208// Check file on SD card for ID3 tags and use them to display some info. *
2209// Extended headers are not parsed. *
2210//**************************************************************************************************
2211void handle_ID3 ( String &path )
2212{
2213 char* p ; // Pointer to filename
2214 struct ID3head_t // First part of ID3 info
2215 {
2216 char fid[3] ; // Should be filled with "ID3"
2217 uint8_t majV, minV ; // Major and minor version
2218 uint8_t hflags ; // Headerflags
2219 uint8_t ttagsize[4] ; // Total tag size
2220 } ID3head ;
2221 uint8_t exthsiz[4] ; // Extended header size
2222 uint32_t stx ; // Ext header size converted
2223 uint32_t sttg ; // Total tagsize converted
2224 uint32_t stg ; // Size of a single tag
2225 struct ID3tag_t // Tag in ID3 info
2226 {
2227 char tagid[4] ; // Things like "TCON", "TYER", ...
2228 uint8_t tagsize[4] ; // Size of the tag
2229 uint8_t tagflags[2] ; // Tag flags
2230 } ID3tag ;
2231 uint8_t tmpbuf[4] ; // Scratch buffer
2232 uint8_t tenc ; // Text encoding
2233 String albttl = String() ; // Album and title
2234
2235 tftset ( 2, "Playing from local file" ) ; // Assume no ID3
2236 p = (char*)path.c_str() + 1 ; // Point to filename
2237 showstreamtitle ( p, true ) ; // Show the filename as title (middle part)
2238 mp3file = SD.open ( path ) ; // Open the file
2239 mp3file.read ( (uint8_t*)&ID3head, sizeof(ID3head) ) ; // Read first part of ID3 info
2240 if ( strncmp ( ID3head.fid, "ID3", 3 ) == 0 )
2241 {
2242 sttg = ssconv ( ID3head.ttagsize ) ; // Convert tagsize
2243 dbgprint ( "Found ID3 info" ) ;
2244 if ( ID3head.hflags & 0x40 ) // Extended header?
2245 {
2246 stx = ssconv ( exthsiz ) ; // Yes, get size of extended header
2247 while ( stx-- )
2248 {
2249 mp3file.read () ; // Skip next byte of extended header
2250 }
2251 }
2252 while ( sttg > 10 ) // Now handle the tags
2253 {
2254 sttg -= mp3file.read ( (uint8_t*)&ID3tag,
2255 sizeof(ID3tag) ) ; // Read first part of a tag
2256 if ( ID3tag.tagid[0] == 0 ) // Reached the end of the list?
2257 {
2258 break ; // Yes, quit the loop
2259 }
2260 stg = ssconv ( ID3tag.tagsize ) ; // Convert size of tag
2261 if ( ID3tag.tagflags[1] & 0x08 ) // Compressed?
2262 {
2263 sttg -= mp3file.read ( tmpbuf, 4 ) ; // Yes, ignore 4 bytes
2264 stg -= 4 ; // Reduce tag size
2265 }
2266 if ( ID3tag.tagflags[1] & 0x044 ) // Encrypted or grouped?
2267 {
2268 sttg -= mp3file.read ( tmpbuf, 1 ) ; // Yes, ignore 1 byte
2269 stg-- ; // Reduce tagsize by 1
2270 }
2271 if ( stg > ( sizeof(metalinebf) + 2 ) ) // Room for tag?
2272 {
2273 break ; // No, skip this and further tags
2274 }
2275 sttg -= mp3file.read ( (uint8_t*)metalinebf,
2276 stg ) ; // Read tag contents
2277 metalinebf[stg] = '\0' ; // Add delimeter
2278 tenc = metalinebf[0] ; // First byte is encoding type
2279 if ( tenc == '\0' ) // Debug all tags with encoding 0
2280 {
2281 dbgprint ( "ID3 %s = %s", ID3tag.tagid,
2282 metalinebf + 1 ) ;
2283 }
2284 if ( ( strncmp ( ID3tag.tagid, "TALB", 4 ) == 0 ) || // Album title
2285 ( strncmp ( ID3tag.tagid, "TPE1", 4 ) == 0 ) ) // or artist?
2286 {
2287 albttl += String ( metalinebf + 1 ) ; // Yes, add to string
2288 if ( displaytype == T_NEXTION ) // NEXTION display?
2289 {
2290 albttl += String ( "\\r" ) ; // Add code for newline (2 characters)
2291 }
2292 else
2293 {
2294 albttl += String ( "\n" ) ; // Add newline (1 character)
2295 }
2296 }
2297 if ( strncmp ( ID3tag.tagid, "TIT2", 4 ) == 0 ) // Songtitle?
2298 {
2299 tftset ( 2, metalinebf + 1 ) ; // Yes, show title
2300 }
2301 }
2302 tftset ( 1, albttl ) ; // Show album and title
2303 }
2304 mp3file.close() ; // Close the file
2305 mp3file = SD.open ( path ) ; // And open the file again
2306}
2307
2308
2309//**************************************************************************************************
2310// C O N N E C T T O F I L E *
2311//**************************************************************************************************
2312// Open the local mp3-file. *
2313//**************************************************************************************************
2314bool connecttofile()
2315{
2316 String path ; // Full file spec
2317
2318 tftset ( 0, "ESP32 MP3 Player" ) ; // Set screen segment top line
2319 displaytime ( "" ) ; // Clear time on TFT screen
2320 path = host.substring ( 9 ) ; // Path, skip the "localhost" part
2321 claimSPI ( "sdopen3" ) ; // Claim SPI bus
2322 handle_ID3 ( path ) ; // See if there are ID3 tags in this file
2323 mp3filelength = mp3file.available() ; // Get length
2324 releaseSPI() ; // Release SPI bus
2325 if ( !mp3file )
2326 {
2327 dbgprint ( "Error opening file %s", path.c_str() ) ; // No luck
2328 return false ;
2329 }
2330 mqttpub.trigger ( MQTT_STREAMTITLE ) ; // Request publishing to MQTT
2331 icyname = "" ; // No icy name yet
2332 chunked = false ; // File not chunked
2333 metaint = 0 ; // No metadata
2334 return true ;
2335}
2336
2337
2338//**************************************************************************************************
2339// C O N N E C T W I F I *
2340//**************************************************************************************************
2341// Connect to WiFi using the SSID's available in wifiMulti. *
2342// If only one AP if found in preferences (i.e. wifi_00) the connection is made without *
2343// using wifiMulti. *
2344// If connection fails, an AP is created and the function returns false. *
2345//**************************************************************************************************
2346bool connectwifi()
2347{
2348 char* pfs ; // Pointer to formatted string
2349 char* pfs2 ; // Pointer to formatted string
2350 bool localAP = false ; // True if only local AP is left
2351
2352 WifiInfo_t winfo ; // Entry from wifilist
2353
2354 WiFi.disconnect() ; // After restart the router could
2355 WiFi.softAPdisconnect(true) ; // still keep the old connection
2356 if ( wifilist.size() ) // Any AP defined?
2357 {
2358 if ( wifilist.size() == 1 ) // Just one AP defined in preferences?
2359 {
2360 winfo = wifilist[0] ; // Get this entry
2361 WiFi.begin ( winfo.ssid, winfo.passphrase ) ; // Connect to single SSID found in wifi_xx
2362 WiFi.setSleep(false);
2363 dbgprint ( "Try WiFi %s", winfo.ssid ) ; // Message to show during WiFi connect
2364 }
2365 else // More AP to try
2366 {
2367 wifiMulti.run() ; // Connect to best network
2368 }
2369 if ( WiFi.waitForConnectResult() != WL_CONNECTED ) // Try to connect
2370 {
2371 localAP = true ; // Error, setup own AP
2372 }
2373 }
2374 else
2375 {
2376 localAP = true ; // Not even a single AP defined
2377 }
2378 if ( localAP ) // Must setup local AP?
2379 {
2380 dbgprint ( "WiFi Failed! Trying to setup AP with name %s and password %s.", NAME, NAME ) ;
2381 WiFi.softAP ( NAME, NAME ) ; // This ESP will be an AP
2382 pfs = dbgprint ( "IP = 192.168.4.1" ) ; // Address for AP
2383 }
2384 else
2385 {
2386 ipaddress = WiFi.localIP().toString() ; // Form IP address
2387 pfs2 = dbgprint ( "Connected to %s", WiFi.SSID().c_str() ) ;
2388 tftlog ( pfs2 ) ;
2389 pfs = dbgprint ( "IP = %s", ipaddress.c_str() ) ; // String to dispay on TFT
2390 }
2391 tftlog ( pfs ) ; // Show IP
2392 delay ( 3000 ) ; // Allow user to read this
2393 tftlog ( "\f" ) ; // Select new page if NEXTION
2394 return ( localAP == false ) ; // Return result of connection
2395}
2396
2397
2398//**************************************************************************************************
2399// O T A S T A R T *
2400//**************************************************************************************************
2401// Update via WiFi has been started by Arduino IDE or update request. *
2402//**************************************************************************************************
2403void otastart()
2404{
2405 char* p ;
2406
2407 p = dbgprint ( "OTA update Started" ) ;
2408 tftset ( 2, p ) ; // Set screen segment bottom part
2409}
2410
2411
2412//**************************************************************************************************
2413// D O _ N E X T I O N _ U P D A T E *
2414//**************************************************************************************************
2415// Update NEXTION image from OTA stream. *
2416//**************************************************************************************************
2417bool do_nextion_update ( uint32_t clength )
2418{
2419 bool res = false ; // Update result
2420 uint32_t k ;
2421 int c ; // Reply from NEXTION
2422
2423 if ( nxtserial ) // NEXTION active?
2424 {
2425 vTaskDelete ( xspftask ) ; // Prevent output to NEXTION
2426 delay ( 1000 ) ;
2427 nxtserial->printf ( "\xFF\xFF\xFF" ) ; // Empty command
2428 for ( int i = 0 ; i < 100 ; i++ ) // Any input seen?
2429 {
2430 if ( nxtserial->available() )
2431 {
2432 c = nxtserial->read() ; // Read garbage
2433 }
2434 delay ( 20 ) ;
2435 }
2436 nxtserial->printf ( "whmi-wri %d,9600,0\xFF\xFF\xFF", // Start upload
2437 clength ) ;
2438 while ( !nxtserial->available() ) // Any input seen?
2439 {
2440 delay ( 20 ) ;
2441 }
2442 c = nxtserial->read() ; // Yes, read the 0x05 ACK
2443 while ( clength ) // Loop for the transfer
2444 {
2445 k = clength ;
2446 if ( k > 4096 )
2447 {
2448 k = 4096 ;
2449 }
2450 k = otaclient.read ( tmpbuff, k ) ; // Read a number of bytes from the stream
2451 dbgprint ( "TFT file, read %d bytes", k ) ;
2452 nxtserial->write ( tmpbuff, k ) ;
2453 while ( !nxtserial->available() ) // Any input seen?
2454 {
2455 delay ( 20 ) ;
2456 }
2457 c = (char)nxtserial->read() ; // Yes, read the 0x05 ACK
2458 if ( c != 0x05 )
2459 {
2460 break ;
2461 }
2462 clength -= k ;
2463 }
2464 otaclient.flush() ;
2465 if ( clength == 0 )
2466 {
2467 dbgprint ( "Update successfully completed" ) ;
2468 res = true ;
2469 }
2470 }
2471 return res ;
2472}
2473
2474
2475//**************************************************************************************************
2476// D O _ S O F T W A R E _ U P D A T E *
2477//**************************************************************************************************
2478// Update software from OTA stream. *
2479//**************************************************************************************************
2480bool do_software_update ( uint32_t clength )
2481{
2482 bool res = false ; // Update result
2483
2484 if ( Update.begin ( clength ) ) // Update possible?
2485 {
2486 dbgprint ( "Begin OTA update, length is %d",
2487 clength ) ;
2488 if ( Update.writeStream ( otaclient ) == clength ) // writeStream is the real download
2489 {
2490 dbgprint ( "Written %d bytes successfully", clength ) ;
2491 }
2492 else
2493 {
2494 dbgprint ( "Write failed!" ) ;
2495 }
2496 if ( Update.end() ) // Check for successful flash
2497 {
2498 dbgprint( "OTA done" ) ;
2499 if ( Update.isFinished() )
2500 {
2501 dbgprint ( "Update successfully completed" ) ;
2502 res = true ; // Positive result
2503 }
2504 else
2505 {
2506 dbgprint ( "Update not finished!" ) ;
2507 }
2508 }
2509 else
2510 {
2511 dbgprint ( "Error Occurred. Error %s", Update.getError() ) ;
2512 }
2513 }
2514 else
2515 {
2516 // Not enough space to begin OTA
2517 dbgprint ( "Not enough space to begin OTA" ) ;
2518 otaclient.flush() ;
2519 }
2520 return res ;
2521}
2522
2523
2524//**************************************************************************************************
2525// U P D A T E _ S O F T W A R E *
2526//**************************************************************************************************
2527// Update software by download from remote host. *
2528//**************************************************************************************************
2529void update_software ( const char* lstmodkey, const char* updatehost, const char* binfile )
2530{
2531 uint32_t timeout = millis() ; // To detect time-out
2532 String line ; // Input header line
2533 String lstmod = "" ; // Last modified timestamp in NVS
2534 String newlstmod ; // Last modified from host
2535
2536 updatereq = false ; // Clear update flag
2537 otastart() ; // Show something on screen
2538 stop_mp3client () ; // Stop input stream
2539 lstmod = nvsgetstr ( lstmodkey ) ; // Get current last modified timestamp
2540 dbgprint ( "Connecting to %s for %s",
2541 updatehost, binfile ) ;
2542 if ( !otaclient.connect ( updatehost, 80 ) ) // Connect to host
2543 {
2544 dbgprint ( "Connect to updatehost failed!" ) ;
2545 return ;
2546 }
2547 otaclient.printf ( "GET %s HTTP/1.1\r\n"
2548 "Host: %s\r\n"
2549 "Cache-Control: no-cache\r\n"
2550 "Connection: close\r\n\r\n",
2551 binfile,
2552 updatehost ) ;
2553 while ( otaclient.available() == 0 ) // Wait until response appears
2554 {
2555 if ( millis() - timeout > 5000 )
2556 {
2557 dbgprint ( "Connect to Update host Timeout!" ) ;
2558 otaclient.stop() ;
2559 return ;
2560 }
2561 }
2562 // Connected, handle response
2563 while ( otaclient.available() )
2564 {
2565 line = otaclient.readStringUntil ( '\n' ) ; // Read a line from response
2566 line.trim() ; // Remove garbage
2567 dbgprint ( line.c_str() ) ; // Debug info
2568 if ( !line.length() ) // End of headers?
2569 {
2570 break ; // Yes, get the OTA started
2571 }
2572 // Check if the HTTP Response is 200. Any other response is an error.
2573 if ( line.startsWith ( "HTTP/1.1" ) ) //
2574 {
2575 if ( line.indexOf ( " 200 " ) < 0 )
2576 {
2577 dbgprint ( "Got a non 200 status code from server!" ) ;
2578 return ;
2579 }
2580 }
2581 scan_content_length ( line.c_str() ) ; // Scan for content_length
2582 if ( line.startsWith ( "Last-Modified: " ) ) // Timestamp of binary file
2583 {
2584 newlstmod = line.substring ( 15 ) ; // Isolate timestamp
2585 }
2586 }
2587 // End of headers reached
2588 if ( newlstmod == lstmod ) // Need for update?
2589 {
2590 dbgprint ( "No new version available" ) ; // No, show reason
2591 otaclient.flush() ;
2592 return ;
2593 }
2594 if ( clength > 0 )
2595 {
2596 if ( strstr ( binfile, ".bin" ) ) // Update of the sketch?
2597 {
2598 if ( do_software_update ( clength ) ) // Flash updated sketch
2599 {
2600 nvssetstr ( lstmodkey, newlstmod ) ; // Update Last Modified in NVS
2601 }
2602 }
2603 if ( strstr ( binfile, ".tft" ) ) // Update of the NEXTION image?
2604 {
2605 if ( do_nextion_update ( clength ) ) // Flash updated NEXTION
2606 {
2607 nvssetstr ( lstmodkey, newlstmod ) ; // Update Last Modified in NVS
2608 }
2609 }
2610 }
2611 else
2612 {
2613 dbgprint ( "There was no content in the response" ) ;
2614 otaclient.flush() ;
2615 }
2616}
2617
2618
2619//**************************************************************************************************
2620// R E A D H O S T F R O M P R E F *
2621//**************************************************************************************************
2622// Read the mp3 host from the preferences specified by the parameter. *
2623// The host will be returned. *
2624//**************************************************************************************************
2625String readhostfrompref ( int8_t preset )
2626{
2627 char tkey[12] ; // Key as an array of chars
2628
2629 sprintf ( tkey, "preset_%02d", preset ) ; // Form the search key
2630 if ( nvssearch ( tkey ) ) // Does it exists?
2631 {
2632 // Get the contents
2633 return nvsgetstr ( tkey ) ; // Get the station (or empty sring)
2634 }
2635 else
2636 {
2637 return String ( "" ) ; // Not found
2638 }
2639}
2640
2641
2642//**************************************************************************************************
2643// R E A D H O S T F R O M P R E F *
2644//**************************************************************************************************
2645// Search for the next mp3 host in preferences specified newpreset. *
2646// The host will be returned. newpreset will be updated *
2647//**************************************************************************************************
2648String readhostfrompref()
2649{
2650 String contents = "" ; // Result of search
2651 int maxtry = 0 ; // Limit number of tries
2652
2653 while ( ( contents = readhostfrompref ( ini_block.newpreset ) ) == "" )
2654 {
2655 if ( ++ maxtry > 99 )
2656 {
2657 return "" ;
2658 }
2659 if ( ++ini_block.newpreset > 99 ) // Next or wrap to 0
2660 {
2661 ini_block.newpreset = 0 ;
2662 }
2663 }
2664 // Get the contents
2665 return contents ; // Return the station
2666}
2667
2668
2669//**************************************************************************************************
2670// R E A D P R O G B U T T O N S *
2671//**************************************************************************************************
2672// Read the preferences for the programmable input pins and the touch pins. *
2673//**************************************************************************************************
2674void readprogbuttons()
2675{
2676 char mykey[20] ; // For numerated key
2677 int8_t pinnr ; // GPIO pinnumber to fill
2678 int i ; // Loop control
2679 String val ; // Contents of preference entry
2680
2681 for ( i = 0 ; ( pinnr = progpin[i].gpio ) >= 0 ; i++ ) // Scan for all programmable pins
2682 {
2683 sprintf ( mykey, "gpio_%02d", pinnr ) ; // Form key in preferences
2684 if ( nvssearch ( mykey ) )
2685 {
2686 val = nvsgetstr ( mykey ) ; // Get the contents
2687 if ( val.length() ) // Does it exists?
2688 {
2689 if ( !progpin[i].reserved ) // Do not use reserved pins
2690 {
2691 progpin[i].avail = true ; // This one is active now
2692 progpin[i].command = val ; // Set command
2693 dbgprint ( "gpio_%02d will execute %s", // Show result
2694 pinnr, val.c_str() ) ;
2695 }
2696 }
2697 }
2698 }
2699 // Now for the touch pins 0..9, identified by their GPIO pin number
2700 for ( i = 0 ; ( pinnr = touchpin[i].gpio ) >= 0 ; i++ ) // Scan for all programmable pins
2701 {
2702 sprintf ( mykey, "touch_%02d", i ) ; // Form key in preferences
2703 if ( nvssearch ( mykey ) )
2704 {
2705 val = nvsgetstr ( mykey ) ; // Get the contents
2706 if ( val.length() ) // Does it exists?
2707 {
2708 if ( !touchpin[i].reserved ) // Do not use reserved pins
2709 {
2710 touchpin[i].avail = true ; // This one is active now
2711 touchpin[i].command = val ; // Set command
2712 //pinMode ( touchpin[i].gpio, INPUT ) ; // Free floating input
2713 dbgprint ( "touch_%02d will execute %s", // Show result
2714 i, val.c_str() ) ;
2715 dbgprint ( "Level is now %d",
2716 touchRead ( pinnr ) ) ; // Sample the pin
2717 }
2718 else
2719 {
2720 dbgprint ( "touch_%02d pin (GPIO%02d) is reserved for I/O!",
2721 i, pinnr ) ;
2722 }
2723 }
2724 }
2725 }
2726}
2727
2728
2729//**************************************************************************************************
2730// R E S E R V E P I N *
2731//**************************************************************************************************
2732// Set I/O pin to "reserved". *
2733// The pin is than not available for a programmable function. *
2734//**************************************************************************************************
2735void reservepin ( int8_t rpinnr )
2736{
2737 uint8_t i = 0 ; // Index in progpin/touchpin array
2738 int8_t pin ; // Pin number in progpin array
2739
2740 while ( ( pin = progpin[i].gpio ) >= 0 ) // Find entry for requested pin
2741 {
2742 if ( pin == rpinnr ) // Entry found?
2743 {
2744 if ( progpin[i].reserved ) // Already reserved?
2745 {
2746 dbgprint ( "Pin %d is already reserved!", rpinnr ) ;
2747 }
2748 //dbgprint ( "GPIO%02d unavailabe for 'gpio_'-command", pin ) ;
2749 progpin[i].reserved = true ; // Yes, pin is reserved now
2750 break ; // No need to continue
2751 }
2752 i++ ; // Next entry
2753 }
2754 // Also reserve touchpin numbers
2755 i = 0 ;
2756 while ( ( pin = touchpin[i].gpio ) >= 0 ) // Find entry for requested pin
2757 {
2758 if ( pin == rpinnr ) // Entry found?
2759 {
2760 //dbgprint ( "GPIO%02d unavailabe for 'touch'-command", pin ) ;
2761 touchpin[i].reserved = true ; // Yes, pin is reserved now
2762 break ; // No need to continue
2763 }
2764 i++ ; // Next entry
2765 }
2766}
2767
2768
2769//**************************************************************************************************
2770// R E A D I O P R E F S *
2771//**************************************************************************************************
2772// Scan the preferences for IO-pin definitions. *
2773//**************************************************************************************************
2774void readIOprefs()
2775{
2776 struct iosetting
2777 {
2778 const char* gname ; // Name in preferences
2779 int8_t* gnr ; // GPIO pin number
2780 int8_t pdefault ; // Default pin
2781 };
2782 struct iosetting klist[] = { // List of I/O related keys
2783 { "pin_ir", &ini_block.ir_pin, -1 },
2784 { "pin_enc_clk", &ini_block.enc_clk_pin, -1 },
2785 { "pin_enc_dt", &ini_block.enc_dt_pin, -1 },
2786 { "pin_enc_sw", &ini_block.enc_sw_pin, -1 },
2787 { "pin_tft_cs", &ini_block.tft_cs_pin, -1 }, // Display SPI version
2788 { "pin_tft_dc", &ini_block.tft_dc_pin, -1 }, // Display SPI version
2789 { "pin_tft_scl", &ini_block.tft_scl_pin, -1 }, // Display I2C version
2790 { "pin_tft_sda", &ini_block.tft_sda_pin, -1 }, // Display I2C version
2791 { "pin_tft_bl", &ini_block.tft_bl_pin, -1 }, // Display backlight
2792 { "pin_tft_blx", &ini_block.tft_blx_pin, -1 }, // Display backlight (inversed logic)
2793 { "pin_sd_cs", &ini_block.sd_cs_pin, -1 },
2794 { "pin_vs_cs", &ini_block.vs_cs_pin, -1 },
2795 { "pin_vs_dcs", &ini_block.vs_dcs_pin, -1 },
2796 { "pin_vs_dreq", &ini_block.vs_dreq_pin, -1 },
2797 { "pin_shutdown", &ini_block.vs_shutdown_pin, -1 }, // Amplifier shut-down pin
2798 { "pin_shutdownx", &ini_block.vs_shutdownx_pin, -1 }, // Amplifier shut-down pin (inversed logic)
2799 { "pin_spi_sck", &ini_block.spi_sck_pin, 18 },
2800 { "pin_spi_miso", &ini_block.spi_miso_pin, 19 },
2801 { "pin_spi_mosi", &ini_block.spi_mosi_pin, 23 },
2802 { NULL, NULL, 0 } // End of list
2803 } ;
2804 int i ; // Loop control
2805 int count = 0 ; // Number of keys found
2806 String val ; // Contents of preference entry
2807 int8_t ival ; // Value converted to integer
2808 int8_t* p ; // Points to variable
2809
2810 for ( i = 0 ; klist[i].gname ; i++ ) // Loop trough all I/O related keys
2811 {
2812 p = klist[i].gnr ; // Point to target variable
2813 ival = klist[i].pdefault ; // Assume pin number to be the default
2814 if ( nvssearch ( klist[i].gname ) ) // Does it exist?
2815 {
2816 val = nvsgetstr ( klist[i].gname ) ; // Read value of key
2817 if ( val.length() ) // Parameter in preference?
2818 {
2819 count++ ; // Yes, count number of filled keys
2820 ival = val.toInt() ; // Convert value to integer pinnumber
2821 reservepin ( ival ) ; // Set pin to "reserved"
2822 }
2823 }
2824 *p = ival ; // Set pinnumber in ini_block
2825 dbgprint ( "%s set to %d", // Show result
2826 klist[i].gname,
2827 ival ) ;
2828 }
2829}
2830
2831
2832//**************************************************************************************************
2833// R E A D P R E F S *
2834//**************************************************************************************************
2835// Read the preferences and interpret the commands. *
2836// If output == true, the key / value pairs are returned to the caller as a String. *
2837//**************************************************************************************************
2838String readprefs ( bool output )
2839{
2840 uint16_t i ; // Loop control
2841 String val ; // Contents of preference entry
2842 String cmd ; // Command for analyzCmd
2843 String outstr = "" ; // Outputstring
2844 char* key ; // Point to nvskeys[i]
2845 uint8_t winx ; // Index in wifilist
2846 uint16_t last2char = 0 ; // To detect paragraphs
2847
2848 i = 0 ;
2849 while ( *( key = nvskeys[i] ) ) // Loop trough all available keys
2850 {
2851 val = nvsgetstr ( key ) ; // Read value of this key
2852 cmd = String ( key ) + // Yes, form command
2853 String ( " = " ) +
2854 val ;
2855 if ( strstr ( key, "wifi_" ) ) // Is it a wifi ssid/password?
2856 {
2857 winx = atoi ( key + 5 ) ; // Get index in wifilist
2858 if ( ( winx < wifilist.size() ) && // Existing wifi spec in wifilist?
2859 ( val.indexOf ( wifilist[winx].ssid ) == 0 ) )
2860 {
2861 val = String ( wifilist[winx].ssid ) + // Yes, hide password
2862 String ( "/*******" ) ;
2863 }
2864 cmd = String ( "" ) ; // Do not analyze this
2865
2866 }
2867 else if ( strstr ( key, "mqttpasswd" ) ) // Is it a MQTT password?
2868 {
2869 val = String ( "*******" ) ; // Yes, hide it
2870 }
2871 if ( output )
2872 {
2873 if ( ( i > 0 ) &&
2874 ( *(uint16_t*)key != last2char ) ) // New paragraph?
2875 {
2876 outstr += String ( "#\n" ) ; // Yes, add separator
2877 }
2878 last2char = *(uint16_t*)key ; // Save 2 chars for next compare
2879 outstr += String ( key ) + // Add to outstr
2880 String ( " = " ) +
2881 val +
2882 String ( "\n" ) ; // Add newline
2883 }
2884 else
2885 {
2886 analyzeCmd ( cmd.c_str() ) ; // Analyze it
2887 }
2888 i++ ; // Next key
2889 }
2890 if ( i == 0 )
2891 {
2892 outstr = String ( "No preferences found.\n"
2893 "Use defaults or run Esp32_radio_init first.\n" ) ;
2894 }
2895 return outstr ;
2896}
2897
2898
2899//**************************************************************************************************
2900// M Q T T R E C O N N E C T *
2901//**************************************************************************************************
2902// Reconnect to broker. *
2903//**************************************************************************************************
2904bool mqttreconnect()
2905{
2906 static uint32_t retrytime = 0 ; // Limit reconnect interval
2907 bool res = false ; // Connect result
2908 char clientid[20] ; // Client ID
2909 char subtopic[60] ; // Topic to subscribe
2910
2911 if ( ( millis() - retrytime ) < 5000 ) // Don't try to frequently
2912 {
2913 return res ;
2914 }
2915 retrytime = millis() ; // Set time of last try
2916 if ( mqttcount > MAXMQTTCONNECTS ) // Tried too much?
2917 {
2918 mqtt_on = false ; // Yes, switch off forever
2919 return res ; // and quit
2920 }
2921 mqttcount++ ; // Count the retries
2922 dbgprint ( "(Re)connecting number %d to MQTT %s", // Show some debug info
2923 mqttcount,
2924 ini_block.mqttbroker.c_str() ) ;
2925 sprintf ( clientid, "%s-%04d", // Generate client ID
2926 NAME, (int) random ( 10000 ) % 10000 ) ;
2927 res = mqttclient.connect ( clientid, // Connect to broker
2928 ini_block.mqttuser.c_str(),
2929 ini_block.mqttpasswd.c_str()
2930 ) ;
2931 if ( res )
2932 {
2933 sprintf ( subtopic, "%s/%s", // Add prefix to subtopic
2934 ini_block.mqttprefix.c_str(),
2935 MQTT_SUBTOPIC ) ;
2936 res = mqttclient.subscribe ( subtopic ) ; // Subscribe to MQTT
2937 if ( !res )
2938 {
2939 dbgprint ( "MQTT subscribe failed!" ) ; // Failure
2940 }
2941 mqttpub.trigger ( MQTT_IP ) ; // Publish own IP
2942 }
2943 else
2944 {
2945 dbgprint ( "MQTT connection failed, rc=%d",
2946 mqttclient.state() ) ;
2947
2948 }
2949 return res ;
2950}
2951
2952
2953//**************************************************************************************************
2954// O N M Q T T M E S S A G E *
2955//**************************************************************************************************
2956// Executed when a subscribed message is received. *
2957// Note that message is not delimited by a '\0'. *
2958// Note that cmd buffer is shared with serial input. *
2959//**************************************************************************************************
2960void onMqttMessage ( char* topic, byte* payload, unsigned int len )
2961{
2962 const char* reply ; // Result from analyzeCmd
2963
2964 if ( strstr ( topic, MQTT_SUBTOPIC ) ) // Check on topic, maybe unnecessary
2965 {
2966 if ( len >= sizeof(cmd) ) // Message may not be too long
2967 {
2968 len = sizeof(cmd) - 1 ;
2969 }
2970 strncpy ( cmd, (char*)payload, len ) ; // Make copy of message
2971 cmd[len] = '\0' ; // Take care of delimeter
2972 dbgprint ( "MQTT message arrived [%s], lenght = %d, %s", topic, len, cmd ) ;
2973 reply = analyzeCmd ( cmd ) ; // Analyze command and handle it
2974 dbgprint ( reply ) ; // Result for debugging
2975 }
2976}
2977
2978
2979//**************************************************************************************************
2980// S C A N S E R I A L *
2981//**************************************************************************************************
2982// Listen to commands on the Serial inputline. *
2983//**************************************************************************************************
2984void scanserial()
2985{
2986 static String serialcmd ; // Command from Serial input
2987 char c ; // Input character
2988 const char* reply = "" ; // Reply string from analyzeCmd
2989 uint16_t len ; // Length of input string
2990
2991 while ( Serial.available() ) // Any input seen?
2992 {
2993 c = (char)Serial.read() ; // Yes, read the next input character
2994 //Serial.write ( c ) ; // Echo
2995 len = serialcmd.length() ; // Get the length of the current string
2996 if ( ( c == '\n' ) || ( c == '\r' ) )
2997 {
2998 if ( len )
2999 {
3000 strncpy ( cmd, serialcmd.c_str(), sizeof(cmd) ) ;
3001 if ( nxtserial ) // NEXTION test possible?
3002 {
3003 if ( serialcmd.startsWith ( "N:" ) ) // Command meant to test Nextion display?
3004 {
3005 nxtserial->printf ( "%s\xFF\xFF\xFF", cmd + 2 ) ;
3006 }
3007 }
3008 reply = analyzeCmd ( cmd ) ; // Analyze command and handle it
3009 dbgprint ( reply ) ; // Result for debugging
3010 serialcmd = "" ; // Prepare for new command
3011 }
3012 }
3013 if ( c >= ' ' ) // Only accept useful characters
3014 {
3015 serialcmd += c ; // Add to the command
3016 }
3017 if ( len >= ( sizeof(cmd) - 2 ) ) // Check for excessive length
3018 {
3019 serialcmd = "" ; // Too long, reset
3020 }
3021 }
3022}
3023
3024
3025//**************************************************************************************************
3026// S C A N S E R I A L 2 *
3027//**************************************************************************************************
3028// Listen to commands on the 2nd Serial inputline (NEXTION). *
3029//**************************************************************************************************
3030void scanserial2()
3031{
3032 static String serialcmd ; // Command from Serial input
3033 char c ; // Input character
3034 const char* reply = "" ; // Reply string from analyzeCmd
3035 uint16_t len ; // Length of input string
3036 static uint8_t ffcount = 0 ; // Counter for 3 tmes "0xFF"
3037
3038 if ( nxtserial ) // NEXTION active?
3039 {
3040 while ( nxtserial->available() ) // Yes, any input seen?
3041 {
3042 c = (char)nxtserial->read() ; // Yes, read the next input character
3043 len = serialcmd.length() ; // Get the length of the current string
3044 if ( c == 0xFF ) // End of command?
3045 {
3046 if ( ++ffcount < 3 ) // 3 times FF?
3047 {
3048 continue ; // No, continue to read
3049 }
3050 ffcount = 0 ; // For next command
3051 if ( len )
3052 {
3053 strncpy ( cmd, serialcmd.c_str(), sizeof(cmd) ) ;
3054 dbgprint ( "NEXTION command seen %02X %s",
3055 cmd[0], cmd + 1 ) ;
3056 if ( cmd[0] == 0x70 ) // Button pressed?
3057 {
3058 reply = analyzeCmd ( cmd + 1 ) ; // Analyze command and handle it
3059 dbgprint ( reply ) ; // Result for debugging
3060 }
3061 serialcmd = "" ; // Prepare for new command
3062 }
3063 }
3064 else if ( c >= ' ' ) // Only accept useful characters
3065 {
3066 serialcmd += c ; // Add to the command
3067 }
3068 if ( len >= ( sizeof(cmd) - 2 ) ) // Check for excessive length
3069 {
3070 serialcmd = "" ; // Too long, reset
3071 }
3072 }
3073 }
3074}
3075
3076
3077//**************************************************************************************************
3078// S C A N D I G I T A L *
3079//**************************************************************************************************
3080// Scan digital inputs. *
3081//**************************************************************************************************
3082void scandigital()
3083{
3084 static uint32_t oldmillis = 5000 ; // To compare with current time
3085 int i ; // Loop control
3086 int8_t pinnr ; // Pin number to check
3087 bool level ; // Input level
3088 const char* reply ; // Result of analyzeCmd
3089 int16_t tlevel ; // Level found by touch pin
3090 const int16_t THRESHOLD = 30 ; // Threshold or touch pins
3091
3092 if ( ( millis() - oldmillis ) < 100 ) // Debounce
3093 {
3094 return ;
3095 }
3096 scanios++ ; // TEST*TEST*TEST
3097 oldmillis = millis() ; // 100 msec over
3098 for ( i = 0 ; ( pinnr = progpin[i].gpio ) >= 0 ; i++ ) // Scan all inputs
3099 {
3100 if ( !progpin[i].avail || progpin[i].reserved ) // Skip unused and reserved pins
3101 {
3102 continue ;
3103 }
3104 level = ( digitalRead ( pinnr ) == HIGH ) ; // Sample the pin
3105 if ( level != progpin[i].cur ) // Change seen?
3106 {
3107 progpin[i].cur = level ; // And the new level
3108 if ( !level ) // HIGH to LOW change?
3109 {
3110 dbgprint ( "GPIO_%02d is now LOW, execute %s",
3111 pinnr, progpin[i].command.c_str() ) ;
3112 reply = analyzeCmd ( progpin[i].command.c_str() ) ; // Analyze command and handle it
3113 dbgprint ( reply ) ; // Result for debugging
3114 }
3115 }
3116 }
3117 // Now for the touch pins
3118 for ( i = 0 ; ( pinnr = touchpin[i].gpio ) >= 0 ; i++ ) // Scan all inputs
3119 {
3120 if ( !touchpin[i].avail || touchpin[i].reserved ) // Skip unused and reserved pins
3121 {
3122 continue ;
3123 }
3124 tlevel = ( touchRead ( pinnr ) ) ; // Sample the pin
3125 level = ( tlevel >= THRESHOLD ) ; // True if below threshold
3126 if ( level ) // Level HIGH?
3127 {
3128 touchpin[i].count = 0 ; // Reset count number of times
3129 }
3130 else
3131 {
3132 if ( ++touchpin[i].count < 3 ) // Count number of times LOW
3133 {
3134 level = true ; // Not long enough: handle as HIGH
3135 }
3136 }
3137 if ( level != touchpin[i].cur ) // Change seen?
3138 {
3139 touchpin[i].cur = level ; // And the new level
3140 if ( !level ) // HIGH to LOW change?
3141 {
3142 dbgprint ( "TOUCH_%02d is now %d ( < %d ), execute %s",
3143 pinnr, tlevel, THRESHOLD,
3144 touchpin[i].command.c_str() ) ;
3145 reply = analyzeCmd ( touchpin[i].command.c_str() ); // Analyze command and handle it
3146 dbgprint ( reply ) ; // Result for debugging
3147 }
3148 }
3149 }
3150}
3151
3152
3153//**************************************************************************************************
3154// S C A N I R *
3155//**************************************************************************************************
3156// See if IR input is available. Execute the programmed command. *
3157//**************************************************************************************************
3158void scanIR()
3159{
3160 char mykey[20] ; // For numerated key
3161 String val ; // Contents of preference entry
3162 const char* reply ; // Result of analyzeCmd
3163
3164 if ( ir_value ) // Any input?
3165 {
3166 sprintf ( mykey, "ir_%04X", ir_value ) ; // Form key in preferences
3167 if ( nvssearch ( mykey ) )
3168 {
3169 val = nvsgetstr ( mykey ) ; // Get the contents
3170 dbgprint ( "IR code %04X received. Will execute %s",
3171 ir_value, val.c_str() ) ;
3172 reply = analyzeCmd ( val.c_str() ) ; // Analyze command and handle it
3173 dbgprint ( reply ) ; // Result for debugging
3174 }
3175 else
3176 {
3177 dbgprint ( "IR code %04X received, but not found in preferences! Timing %d/%d",
3178 ir_value, ir_0, ir_1 ) ;
3179 }
3180 ir_value = 0 ; // Reset IR code received
3181 }
3182}
3183
3184
3185//**************************************************************************************************
3186// M K _ L S A N *
3187//**************************************************************************************************
3188// Make al list of acceptable networks in preferences. *
3189// Will be called only once by setup(). *
3190// The result will be stored in wifilist. *
3191// Not that the last found SSID and password are kept in common data. If only one SSID is *
3192// defined, the connect is made without using wifiMulti. In this case a connection will *
3193// be made even if de SSID is hidden. *
3194//**************************************************************************************************
3195void mk_lsan()
3196{
3197 uint8_t i ; // Loop control
3198 char key[10] ; // For example: "wifi_03"
3199 String buf ; // "SSID/password"
3200 String lssid, lpw ; // Last read SSID and password from nvs
3201 int inx ; // Place of "/"
3202 WifiInfo_t winfo ; // Element to store in list
3203
3204 dbgprint ( "Create list with acceptable WiFi networks" ) ;
3205 for ( i = 0 ; i < 100 ; i++ ) // Examine wifi_00 .. wifi_99
3206 {
3207 sprintf ( key, "wifi_%02d", i ) ; // Form key in preferences
3208 if ( nvssearch ( key ) ) // Does it exists?
3209 {
3210 buf = nvsgetstr ( key ) ; // Get the contents
3211 inx = buf.indexOf ( "/" ) ; // Find separator between ssid and password
3212 if ( inx > 0 ) // Separator found?
3213 {
3214 lpw = buf.substring ( inx + 1 ) ; // Isolate password
3215 lssid = buf.substring ( 0, inx ) ; // Holds SSID now
3216 dbgprint ( "Added %s to list of networks",
3217 lssid.c_str() ) ;
3218 winfo.inx = i ; // Create new element for wifilist ;
3219 winfo.ssid = strdup ( lssid.c_str() ) ; // Set ssid of element
3220 winfo.passphrase = strdup ( lpw.c_str() ) ;
3221 wifilist.push_back ( winfo ) ; // Add to list
3222 wifiMulti.addAP ( winfo.ssid, // Add to wifi acceptable network list
3223 winfo.passphrase ) ;
3224 }
3225 }
3226 }
3227 dbgprint ( "End adding networks" ) ; ////
3228}
3229
3230
3231//**************************************************************************************************
3232// G E T R A D I O S T A T U S *
3233//**************************************************************************************************
3234// Return preset-, tone- and volume status. *
3235// Included are the presets, the current station, the volume and the tone settings. *
3236//**************************************************************************************************
3237String getradiostatus()
3238{
3239 char pnr[3] ; // Preset as 2 character, i.e. "03"
3240
3241 sprintf ( pnr, "%02d", ini_block.newpreset ) ; // Current preset
3242 return String ( "preset=" ) + // Add preset setting
3243 String ( pnr ) +
3244 String ( "\nvolume=" ) + // Add volume setting
3245 String ( String ( ini_block.reqvol ) ) +
3246 String ( "\ntoneha=" ) + // Add tone setting HA
3247 String ( ini_block.rtone[0] ) +
3248 String ( "\ntonehf=" ) + // Add tone setting HF
3249 String ( ini_block.rtone[1] ) +
3250 String ( "\ntonela=" ) + // Add tone setting LA
3251 String ( ini_block.rtone[2] ) +
3252 String ( "\ntonelf=" ) + // Add tone setting LF
3253 String ( ini_block.rtone[3] ) ;
3254}
3255
3256
3257//**************************************************************************************************
3258// G E T S E T T I N G S *
3259//**************************************************************************************************
3260// Send some settings to the webserver. *
3261// Included are the presets, the current station, the volume and the tone settings. *
3262//**************************************************************************************************
3263void getsettings()
3264{
3265 String val ; // Result to send
3266 String statstr ; // Station string
3267 int inx ; // Position of search char in line
3268 int i ; // Loop control, preset number
3269 char tkey[12] ; // Key for preset preference
3270
3271 for ( i = 0 ; i < 100 ; i++ ) // Max 99 presets
3272 {
3273 sprintf ( tkey, "preset_%02d", i ) ; // Preset plus number
3274 if ( nvssearch ( tkey ) ) // Does it exists?
3275 {
3276 // Get the contents
3277 statstr = nvsgetstr ( tkey ) ; // Get the station
3278 // Show just comment if available. Otherwise the preset itself.
3279 inx = statstr.indexOf ( "#" ) ; // Get position of "#"
3280 if ( inx > 0 ) // Hash sign present?
3281 {
3282 statstr.remove ( 0, inx + 1 ) ; // Yes, remove non-comment part
3283 }
3284 chomp ( statstr ) ; // Remove garbage from description
3285 val += String ( tkey ) +
3286 String ( "=" ) +
3287 statstr +
3288 String ( "\n" ) ; // Add delimeter
3289 if ( val.length() > 1000 ) // Time to flush?
3290 {
3291 cmdclient.print ( val ) ; // Yes, send
3292 val = "" ; // Start new string
3293 }
3294 }
3295 }
3296 val += getradiostatus() + // Add radio setting
3297 String ( "\n\n" ) ; // End of reply
3298 cmdclient.print ( val ) ; // And send
3299}
3300
3301
3302//**************************************************************************************************
3303// T F T L O G *
3304//**************************************************************************************************
3305// Log to TFT if enabled. *
3306//**************************************************************************************************
3307void tftlog ( const char *str )
3308{
3309 if ( tft ) // TFT configured?
3310 {
3311 dsp_println ( str ) ; // Yes, show error on TFT
3312 dsp_update() ; // To physical screen
3313 }
3314}
3315
3316
3317//**************************************************************************************************
3318// F I N D N S I D *
3319//**************************************************************************************************
3320// Find the namespace ID for the namespace passed as parameter. *
3321//**************************************************************************************************
3322uint8_t FindNsID ( const char* ns )
3323{
3324 esp_err_t result = ESP_OK ; // Result of reading partition
3325 uint32_t offset = 0 ; // Offset in nvs partition
3326 uint8_t i ; // Index in Entry 0..125
3327 uint8_t bm ; // Bitmap for an entry
3328 uint8_t res = 0xFF ; // Function result
3329
3330 while ( offset < nvs->size )
3331 {
3332 result = esp_partition_read ( nvs, offset, // Read 1 page in nvs partition
3333 &nvsbuf,
3334 sizeof(nvsbuf) ) ;
3335 if ( result != ESP_OK )
3336 {
3337 dbgprint ( "Error reading NVS!" ) ;
3338 break ;
3339 }
3340 i = 0 ;
3341 while ( i < 126 )
3342 {
3343
3344 bm = ( nvsbuf.Bitmap[i / 4] >> ( ( i % 4 ) * 2 ) ) ; // Get bitmap for this entry,
3345 bm &= 0x03 ; // 2 bits for one entry
3346 if ( ( bm == 2 ) &&
3347 ( nvsbuf.Entry[i].Ns == 0 ) &&
3348 ( strcmp ( ns, nvsbuf.Entry[i].Key ) == 0 ) )
3349 {
3350 res = nvsbuf.Entry[i].Data & 0xFF ; // Return the ID
3351 offset = nvs->size ; // Stop outer loop as well
3352 break ;
3353 }
3354 else
3355 {
3356 if ( bm == 2 )
3357 {
3358 i += nvsbuf.Entry[i].Span ; // Next entry
3359 }
3360 else
3361 {
3362 i++ ;
3363 }
3364 }
3365 }
3366 offset += sizeof(nvs_page) ; // Prepare to read next page in nvs
3367 }
3368 return res ;
3369}
3370
3371
3372//**************************************************************************************************
3373// B U B B L E S O R T K E Y S *
3374//**************************************************************************************************
3375// Bubblesort the nvskeys. *
3376//**************************************************************************************************
3377void bubbleSortKeys ( uint16_t n )
3378{
3379 uint16_t i, j ; // Indexes in nvskeys
3380 char tmpstr[16] ; // Temp. storage for a key
3381
3382 for ( i = 0 ; i < n - 1 ; i++ ) // Examine all keys
3383 {
3384 for ( j = 0 ; j < n - i - 1 ; j++ ) // Compare to following keys
3385 {
3386 if ( strcmp ( nvskeys[j], nvskeys[j + 1] ) > 0 ) // Next key out of order?
3387 {
3388 strcpy ( tmpstr, nvskeys[j] ) ; // Save current key a while
3389 strcpy ( nvskeys[j], nvskeys[j + 1] ) ; // Replace current with next key
3390 strcpy ( nvskeys[j + 1], tmpstr ) ; // Replace next with saved current
3391 }
3392 }
3393 }
3394}
3395
3396
3397//**************************************************************************************************
3398// F I L L K E Y L I S T *
3399//**************************************************************************************************
3400// File the list of all relevant keys in NVS. *
3401// The keys will be sorted. *
3402//**************************************************************************************************
3403void fillkeylist()
3404{
3405 esp_err_t result = ESP_OK ; // Result of reading partition
3406 uint32_t offset = 0 ; // Offset in nvs partition
3407 uint16_t i ; // Index in Entry 0..125.
3408 uint8_t bm ; // Bitmap for an entry
3409 uint16_t nvsinx = 0 ; // Index in nvskey table
3410
3411 keynames.clear() ; // Clear the list
3412 while ( offset < nvs->size )
3413 {
3414 result = esp_partition_read ( nvs, offset, // Read 1 page in nvs partition
3415 &nvsbuf,
3416 sizeof(nvsbuf) ) ;
3417 if ( result != ESP_OK )
3418 {
3419 dbgprint ( "Error reading NVS!" ) ;
3420 break ;
3421 }
3422 i = 0 ;
3423 while ( i < 126 )
3424 {
3425 bm = ( nvsbuf.Bitmap[i / 4] >> ( ( i % 4 ) * 2 ) ) ; // Get bitmap for this entry,
3426 bm &= 0x03 ; // 2 bits for one entry
3427 if ( bm == 2 ) // Entry is active?
3428 {
3429 if ( nvsbuf.Entry[i].Ns == namespace_ID ) // Namespace right?
3430 {
3431 strcpy ( nvskeys[nvsinx], nvsbuf.Entry[i].Key ) ; // Yes, save in table
3432 if ( ++nvsinx == MAXKEYS )
3433 {
3434 nvsinx-- ; // Prevent excessive index
3435 }
3436 }
3437 i += nvsbuf.Entry[i].Span ; // Next entry
3438 }
3439 else
3440 {
3441 i++ ;
3442 }
3443 }
3444 offset += sizeof(nvs_page) ; // Prepare to read next page in nvs
3445 }
3446 nvskeys[nvsinx][0] = '\0' ; // Empty key at the end
3447 dbgprint ( "Read %d keys from NVS", nvsinx ) ;
3448 bubbleSortKeys ( nvsinx ) ; // Sort the keys
3449}
3450
3451
3452//**************************************************************************************************
3453// S E T U P *
3454//**************************************************************************************************
3455// Setup for the program. *
3456//**************************************************************************************************
3457void setup()
3458{
3459 int i ; // Loop control
3460 int pinnr ; // Input pinnumber
3461 const char* p ;
3462 byte mac[6] ; // WiFi mac address
3463 char tmpstr[20] ; // For version and Mac address
3464 const char* partname = "nvs" ; // Partition with NVS info
3465 esp_partition_iterator_t pi ; // Iterator for find
3466 const char* dtyp = "Display type is %s" ;
3467 const char* wvn = "Include file %s_html has the wrong version number! "
3468 "Replace header file." ;
3469
3470 Serial.begin ( 115200 ) ; // For debug
3471 Serial.println() ;
3472 // Version tests for some vital include files
3473 if ( about_html_version < 170626 ) dbgprint ( wvn, "about" ) ;
3474 if ( config_html_version < 180806 ) dbgprint ( wvn, "config" ) ;
3475 if ( index_html_version < 180102 ) dbgprint ( wvn, "index" ) ;
3476 if ( mp3play_html_version < 180918 ) dbgprint ( wvn, "mp3play" ) ;
3477 if ( defaultprefs_version < 180816 ) dbgprint ( wvn, "defaultprefs" ) ;
3478 // Print some memory and sketch info
3479 dbgprint ( "Starting ESP32-radio running on CPU %d at %d MHz. Version %s. Free memory %d",
3480 xPortGetCoreID(),
3481 ESP.getCpuFreqMHz(),
3482 VERSION,
3483 ESP.getFreeHeap() ) ; // Normally about 170 kB
3484#if defined ( BLUETFT ) // Report display option
3485 dbgprint ( dtyp, "BLUETFT" ) ;
3486#endif
3487#if defined ( ILI9341 ) // Report display option
3488 dbgprint ( dtyp, "ILI9341" ) ;
3489#endif
3490#if defined ( OLED )
3491 dbgprint ( dtyp, "OLED" ) ;
3492#endif
3493#if defined ( DUMMYTFT )
3494 dbgprint ( dtyp, "DUMMYTFT" ) ;
3495#endif
3496#if defined ( LCD1602I2C )
3497 dbgprint ( dtyp, "LCD1602" ) ;
3498#endif
3499#if defined ( NEXTION )
3500 dbgprint ( dtyp, "NEXTION" ) ;
3501#endif
3502 maintask = xTaskGetCurrentTaskHandle() ; // My taskhandle
3503 SPIsem = xSemaphoreCreateMutex(); ; // Semaphore for SPI bus
3504 pi = esp_partition_find ( ESP_PARTITION_TYPE_DATA, // Get partition iterator for
3505 ESP_PARTITION_SUBTYPE_ANY, // the NVS partition
3506 partname ) ;
3507 if ( pi )
3508 {
3509 nvs = esp_partition_get ( pi ) ; // Get partition struct
3510 esp_partition_iterator_release ( pi ) ; // Release the iterator
3511 dbgprint ( "Partition %s found, %d bytes",
3512 partname,
3513 nvs->size ) ;
3514 }
3515 else
3516 {
3517 dbgprint ( "Partition %s not found!", partname ) ; // Very unlikely...
3518 while ( true ) ; // Impossible to continue
3519 }
3520 namespace_ID = FindNsID ( NAME ) ; // Find ID of our namespace in NVS
3521 fillkeylist() ; // Fill keynames with all keys
3522 memset ( &ini_block, 0, sizeof(ini_block) ) ; // Init ini_block
3523 ini_block.mqttport = 1883 ; // Default port for MQTT
3524 ini_block.mqttprefix = "" ; // No prefix for MQTT topics seen yet
3525 ini_block.clk_server = "pool.ntp.org" ; // Default server for NTP
3526 ini_block.clk_offset = 1 ; // Default Amsterdam time zone
3527 ini_block.clk_dst = 1 ; // DST is +1 hour
3528 ini_block.bat0 = 0 ; // Battery ADC levels not yet defined
3529 ini_block.bat100 = 0 ;
3530 readIOprefs() ; // Read pins used for SPI, TFT, VS1053, IR,
3531 // Rotary encoder
3532 for ( i = 0 ; (pinnr = progpin[i].gpio) >= 0 ; i++ ) // Check programmable input pins
3533 {
3534 pinMode ( pinnr, INPUT_PULLUP ) ; // Input for control button
3535 delay ( 10 ) ;
3536 // Check if pull-up active
3537 if ( ( progpin[i].cur = digitalRead ( pinnr ) ) == HIGH )
3538 {
3539 p = "HIGH" ;
3540 }
3541 else
3542 {
3543 p = "LOW, probably no PULL-UP" ; // No Pull-up
3544 }
3545 dbgprint ( "GPIO%d is %s", pinnr, p ) ;
3546 }
3547 readprogbuttons() ; // Program the free input pins
3548 SPI.begin ( ini_block.spi_sck_pin, // Init VSPI bus with default or modified pins
3549 ini_block.spi_miso_pin,
3550 ini_block.spi_mosi_pin ) ;
3551 vs1053player = new VS1053 ( ini_block.vs_cs_pin, // Make instance of player
3552 ini_block.vs_dcs_pin,
3553 ini_block.vs_dreq_pin,
3554 ini_block.vs_shutdown_pin,
3555 ini_block.vs_shutdownx_pin ) ;
3556 if ( ini_block.ir_pin >= 0 )
3557 {
3558 dbgprint ( "Enable pin %d for IR",
3559 ini_block.ir_pin ) ;
3560 pinMode ( ini_block.ir_pin, INPUT ) ; // Pin for IR receiver VS1838B
3561 attachInterrupt ( ini_block.ir_pin, // Interrupts will be handle by isr_IR
3562 isr_IR, CHANGE ) ;
3563 }
3564 if ( ( ini_block.tft_cs_pin >= 0 ) || // Display configured?
3565 ( ini_block.tft_scl_pin >= 0 ) )
3566 {
3567 dbgprint ( "Start display" ) ;
3568 if ( dsp_begin() ) // Init display
3569 {
3570 dsp_setRotation() ; // Use landscape format
3571 dsp_erase() ; // Clear screen
3572 dsp_setTextSize ( 1 ) ; // Small character font
3573 dsp_setTextColor ( WHITE ) ; // Info in white
3574 dsp_setCursor ( 0, 0 ) ; // Top of screen
3575 dsp_print ( "Starting..." "\n" "Version:" ) ;
3576 strncpy ( tmpstr, VERSION, 16 ) ; // Limit version length
3577 dsp_println ( tmpstr ) ;
3578 dsp_println ( "By Ed Smallenburg" ) ;
3579 dsp_update() ; // Show on physical screen
3580 }
3581 }
3582 if ( ini_block.tft_bl_pin >= 0 ) // Backlight for TFT control?
3583 {
3584 pinMode ( ini_block.tft_bl_pin, OUTPUT ) ; // Yes, enable output
3585 }
3586 if ( ini_block.tft_blx_pin >= 0 ) // Backlight for TFT (inversed logic) control?
3587 {
3588 pinMode ( ini_block.tft_blx_pin, OUTPUT ) ; // Yes, enable output
3589 }
3590 blset ( true ) ; // Enable backlight (if configured)
3591 if ( ini_block.sd_cs_pin >= 0 ) // SD configured?
3592 {
3593 if ( !SD.begin ( ini_block.sd_cs_pin, SPI, // Yes,
3594 SDSPEED ) ) // try to init SD card driver
3595 {
3596 p = dbgprint ( "SD Card Mount Failed!" ) ; // No success, check formatting (FAT)
3597 tftlog ( p ) ; // Show error on TFT as well
3598 }
3599 else
3600 {
3601 SD_okay = ( SD.cardType() != CARD_NONE ) ; // See if known card
3602 if ( !SD_okay )
3603 {
3604 p = dbgprint ( "No SD card attached" ) ; // Card not readable
3605 tftlog ( p ) ; // Show error on TFT as well
3606 }
3607 else
3608 {
3609 dbgprint ( "Locate mp3 files on SD, may take a while..." ) ;
3610 tftlog ( "Read SD card" ) ;
3611 SD_nodecount = listsdtracks ( "/", 0, false ) ; // Build nodelist
3612 p = dbgprint ( "%d tracks on SD", SD_nodecount ) ;
3613 tftlog ( p ) ; // Show number of tracks on TFT
3614 }
3615 }
3616 }
3617 mk_lsan() ; // Make a list of acceptable networks
3618 // in preferences.
3619 WiFi.mode ( WIFI_STA ) ; // This ESP is a station
3620 WiFi.setSleep(false);
3621 WiFi.persistent ( false ) ; // Do not save SSID and password
3622 WiFi.disconnect() ; // After restart router could still
3623 delay ( 100 ) ; // keep old connection
3624 listNetworks() ; // Find WiFi networks
3625 readprefs ( false ) ; // Read preferences
3626 tcpip_adapter_set_hostname ( TCPIP_ADAPTER_IF_STA, NAME ) ;
3627 vs1053player->begin() ; // Initialize VS1053 player
3628 delay(10);
3629 p = dbgprint ( "Connect to WiFi" ) ; // Show progress
3630 tftlog ( p ) ; // On TFT too
3631 NetworkFound = connectwifi() ; // Connect to WiFi network
3632 dbgprint ( "Start server for commands" ) ;
3633 cmdserver.begin() ; // Start http server
3634 if ( NetworkFound ) // OTA and MQTT only if Wifi network found
3635 {
3636 dbgprint ( "Network found. Starting mqtt and OTA" ) ;
3637 mqtt_on = ( ini_block.mqttbroker.length() > 0 ) && // Use MQTT if broker specified
3638 ( ini_block.mqttbroker != "none" ) ;
3639 ArduinoOTA.setHostname ( NAME ) ; // Set the hostname
3640 ArduinoOTA.onStart ( otastart ) ;
3641 ArduinoOTA.begin() ; // Allow update over the air
3642 if ( mqtt_on ) // Broker specified?
3643 {
3644 if ( ( ini_block.mqttprefix.length() == 0 ) || // No prefix?
3645 ( ini_block.mqttprefix == "none" ) )
3646 {
3647 WiFi.macAddress ( mac ) ; // Get mac-adress
3648 sprintf ( tmpstr, "P%02X%02X%02X%02X", // Generate string from last part
3649 mac[3], mac[2],
3650 mac[1], mac[0] ) ;
3651 ini_block.mqttprefix = String ( tmpstr ) ; // Save for further use
3652 }
3653 dbgprint ( "MQTT uses prefix %s", ini_block.mqttprefix.c_str() ) ;
3654 dbgprint ( "Init MQTT" ) ;
3655 mqttclient.setServer(ini_block.mqttbroker.c_str(), // Specify the broker
3656 ini_block.mqttport ) ; // And the port
3657 mqttclient.setCallback ( onMqttMessage ) ; // Set callback on receive
3658 }
3659 if ( MDNS.begin ( NAME ) ) // Start MDNS transponder
3660 {
3661 dbgprint ( "MDNS responder started" ) ;
3662 }
3663 else
3664 {
3665 dbgprint ( "Error setting up MDNS responder!" ) ;
3666 }
3667 }
3668 else
3669 {
3670 currentpreset = ini_block.newpreset ; // No network: do not start radio
3671 }
3672 timer = timerBegin ( 0, 80, true ) ; // User 1st timer with prescaler 80
3673 timerAttachInterrupt ( timer, &timer100, true ) ; // Call timer100() on timer alarm
3674 timerAlarmWrite ( timer, 100000, true ) ; // Alarm every 100 msec
3675 timerAlarmEnable ( timer ) ; // Enable the timer
3676 delay ( 1000 ) ; // Show IP for a while
3677 configTime ( ini_block.clk_offset * 3600,
3678 ini_block.clk_dst * 3600,
3679 ini_block.clk_server.c_str() ) ; // GMT offset, daylight offset in seconds
3680 timeinfo.tm_year = 0 ; // Set TOD to illegal
3681 // Init settings for rotary switch (if existing).
3682 if ( ( ini_block.enc_clk_pin + ini_block.enc_dt_pin + ini_block.enc_sw_pin ) > 2 )
3683 {
3684 attachInterrupt ( ini_block.enc_clk_pin, isr_enc_turn, CHANGE ) ;
3685 attachInterrupt ( ini_block.enc_dt_pin, isr_enc_turn, CHANGE ) ;
3686 attachInterrupt ( ini_block.enc_sw_pin, isr_enc_switch, CHANGE ) ;
3687 dbgprint ( "Rotary encoder is enabled" ) ;
3688 }
3689 else
3690 {
3691 dbgprint ( "Rotary encoder is disabled (%d/%d/%d)",
3692 ini_block.enc_clk_pin,
3693 ini_block.enc_dt_pin,
3694 ini_block.enc_sw_pin) ;
3695 }
3696 if ( NetworkFound )
3697 {
3698 gettime() ; // Sync time
3699 }
3700 if ( tft )
3701 {
3702 dsp_fillRect ( 0, 8, // Clear most of the screen
3703 dsp_getwidth(),
3704 dsp_getheight() - 8, BLACK ) ;
3705 }
3706 outchunk.datatyp = QDATA ; // This chunk dedicated to QDATA
3707 adc1_config_width ( ADC_WIDTH_12Bit ) ;
3708 adc1_config_channel_atten ( ADC1_CHANNEL_0, ADC_ATTEN_0db ) ;
3709 dataqueue = xQueueCreate ( QSIZ, // Create queue for communication
3710 sizeof ( qdata_struct ) ) ;
3711 xTaskCreatePinnedToCore (
3712 playtask, // Task to play data in dataqueue.
3713 "Playtask", // name of task.
3714 1600, // Stack size of task
3715 NULL, // parameter of the task
3716 2, // priority of the task
3717 &xplaytask, // Task handle to keep track of created task
3718 0 ) ; // Run on CPU 0
3719 xTaskCreate (
3720 spftask, // Task to handle special functions.
3721 "Spftask", // name of task.
3722 2048, // Stack size of task
3723 NULL, // parameter of the task
3724 1, // priority of the task
3725 &xspftask ) ; // Task handle to keep track of created task
3726}
3727
3728
3729//**************************************************************************************************
3730// R I N B Y T *
3731//**************************************************************************************************
3732// Read next byte from http inputbuffer. Buffered for speed reasons. *
3733//**************************************************************************************************
3734uint8_t rinbyt ( bool forcestart )
3735{
3736 static uint8_t buf[1024] ; // Inputbuffer
3737 static uint16_t i ; // Pointer in inputbuffer
3738 static uint16_t len ; // Number of bytes in buf
3739 uint16_t tlen ; // Number of available bytes
3740 uint16_t trycount = 0 ; // Limit max. time to read
3741
3742 if ( forcestart || ( i == len ) ) // Time to read new buffer
3743 {
3744 while ( cmdclient.connected() ) // Loop while the client's connected
3745 {
3746 tlen = cmdclient.available() ; // Number of bytes to read from the client
3747 len = tlen ; // Try to read whole input
3748 if ( len == 0 ) // Any input available?
3749 {
3750 if ( ++trycount > 3 ) // Not for a long time?
3751 {
3752 dbgprint ( "HTTP input shorter than expected" ) ;
3753 return '\n' ; // Error! No input
3754 }
3755 delay ( 10 ) ; // Give communication some time
3756 continue ; // Next loop of no input yet
3757 }
3758 if ( len > sizeof(buf) ) // Limit number of bytes
3759 {
3760 len = sizeof(buf) ;
3761 }
3762 len = cmdclient.read ( buf, len ) ; // Read a number of bytes from the stream
3763 i = 0 ; // Pointer to begin of buffer
3764 break ;
3765 }
3766 }
3767 return buf[i++] ;
3768}
3769
3770
3771//**************************************************************************************************
3772// W R I T E P R E F S *
3773//**************************************************************************************************
3774// Update the preferences. Called from the web interface. *
3775//**************************************************************************************************
3776void writeprefs()
3777{
3778 int inx ; // Position in inputstr
3779 uint8_t winx ; // Index in wifilist
3780 char c ; // Input character
3781 String inputstr = "" ; // Input regel
3782 String key, contents ; // Pair for Preferences entry
3783 String dstr ; // Contents for debug
3784
3785 timerAlarmDisable ( timer ) ; // Disable the timer
3786 nvsclear() ; // Remove all preferences
3787 while ( true )
3788 {
3789 c = rinbyt ( false ) ; // Get next inputcharacter
3790 if ( c == '\n' ) // Newline?
3791 {
3792 if ( inputstr.length() == 0 )
3793 {
3794 dbgprint ( "End of writing preferences" ) ;
3795 break ; // End of contents
3796 }
3797 if ( !inputstr.startsWith ( "#" ) ) // Skip pure comment lines
3798 {
3799 inx = inputstr.indexOf ( "=" ) ;
3800 if ( inx >= 0 ) // Line with "="?
3801 {
3802 key = inputstr.substring ( 0, inx ) ; // Yes, isolate the key
3803 key.trim() ;
3804 contents = inputstr.substring ( inx + 1 ) ; // and contents
3805 contents.trim() ;
3806 dstr = contents ; // Copy for debug
3807 if ( ( key.indexOf ( "wifi_" ) == 0 ) ) // Sensitive info?
3808 {
3809 winx = key.substring(5).toInt() ; // Get index in wifilist
3810 if ( ( winx < wifilist.size() ) && // Existing wifi spec in wifilist?
3811 ( contents.indexOf ( wifilist[winx].ssid ) == 0 ) &&
3812 ( contents.indexOf ( "/****" ) > 0 ) ) // Hidden password?
3813 {
3814 contents = String ( wifilist[winx].ssid ) + // Retrieve ssid and password
3815 String ( "/" ) +
3816 String ( wifilist[winx].passphrase ) ;
3817 dstr = String ( wifilist[winx].ssid ) +
3818 String ( "/*******" ) ; // Hide in debug line
3819 }
3820 }
3821 if ( ( key.indexOf ( "mqttpasswd" ) == 0 ) ) // Sensitive info?
3822 {
3823 if ( contents.indexOf ( "****" ) == 0 ) // Hidden password?
3824 {
3825 contents = ini_block.mqttpasswd ; // Retrieve mqtt password
3826 }
3827 dstr = String ( "*******" ) ; // Hide in debug line
3828 }
3829 dbgprint ( "writeprefs setstr %s = %s",
3830 key.c_str(), dstr.c_str() ) ;
3831 nvssetstr ( key.c_str(), contents ) ; // Save new pair
3832 }
3833 }
3834 inputstr = "" ;
3835 }
3836 else
3837 {
3838 if ( c != '\r' ) // Not newline. Is is a CR?
3839 {
3840 inputstr += String ( c ) ; // No, normal char, add to string
3841 }
3842 }
3843 }
3844 timerAlarmEnable ( timer ) ; // Enable the timer
3845 fillkeylist() ; // Update list with keys
3846}
3847
3848
3849//**************************************************************************************************
3850// H A N D L E H T T P R E P L Y *
3851//**************************************************************************************************
3852// Handle the output after an http request. *
3853//**************************************************************************************************
3854void handlehttpreply()
3855{
3856 const char* p ; // Pointer to reply if command
3857 String sndstr = "" ; // String to send
3858 int n ; // Number of files on SD card
3859
3860 if ( http_response_flag )
3861 {
3862 http_response_flag = false ;
3863 if ( cmdclient.connected() )
3864 {
3865 if ( http_rqfile.length() == 0 && // An empty "GET"?
3866 http_getcmd.length() == 0 )
3867 {
3868 if ( NetworkFound ) // Yes, check network
3869 {
3870 handleFSf ( String( "index.html") ) ; // Okay, send the startpage
3871 }
3872 else
3873 {
3874 handleFSf ( String( "config.html") ) ; // Or the configuration page if in AP mode
3875 }
3876 }
3877 else
3878 {
3879 if ( http_getcmd.length() ) // Command to analyze?
3880 {
3881 dbgprint ( "Send reply for %s", http_getcmd.c_str() ) ;
3882 sndstr = httpheader ( String ( "text/html" ) ) ; // Set header
3883 if ( http_getcmd.startsWith ( "getprefs" ) ) // Is it a "Get preferences"?
3884 {
3885 if ( datamode != STOPPED ) // Still playing?
3886 {
3887 datamode = STOPREQD ; // Stop playing
3888 }
3889 sndstr += readprefs ( true ) ; // Read and send
3890 }
3891 else if ( http_getcmd.startsWith ( "getdefs" ) ) // Is it a "Get default preferences"?
3892 {
3893 sndstr += String ( defprefs_txt + 1 ) ; // Yes, read initial values
3894 }
3895 else if ( http_getcmd.startsWith ("saveprefs") ) // Is is a "Save preferences"
3896 {
3897 writeprefs() ; // Yes, handle it
3898 }
3899 else if ( http_getcmd.startsWith ( "mp3list" ) ) // Is is a "Get SD MP3 tracklist"?
3900 {
3901 if ( datamode != STOPPED ) // Still playing?
3902 {
3903 datamode = STOPREQD ; // Stop playing
3904 }
3905 cmdclient.print ( sndstr ) ; // Yes, send header
3906 n = listsdtracks ( "/" ) ; // Handle it
3907 dbgprint ( "%d tracks found on SD card", n ) ;
3908 return ; // Do not send empty line
3909 }
3910 else if ( http_getcmd.startsWith ( "settings" ) ) // Is is a "Get settings" (like presets and tone)?
3911 {
3912 cmdclient.print ( sndstr ) ; // Yes, send header
3913 getsettings() ; // Handle settings request
3914 return ; // Do not send empty line
3915 }
3916 else
3917 {
3918 p = analyzeCmd ( http_getcmd.c_str() ) ; // Yes, do so
3919 sndstr += String ( p ) ; // Content of HTTP response follows the header
3920 }
3921 sndstr += String ( "\n" ) ; // The HTTP response ends with a blank line
3922 cmdclient.print ( sndstr ) ;
3923 }
3924 else if ( http_rqfile.length() ) // File requested?
3925 {
3926 dbgprint ( "Start file reply for %s",
3927 http_rqfile.c_str() ) ;
3928 handleFSf ( http_rqfile ) ; // Yes, send it
3929 }
3930 else
3931 {
3932 httpheader ( String ( "text/html" ) ) ; // Send header
3933 // the content of the HTTP response follows the header:
3934 cmdclient.println ( "Dummy response\n" ) ; // Text ending with double newline
3935 dbgprint ( "Dummy response sent" ) ;
3936 }
3937 }
3938 }
3939 }
3940}
3941
3942
3943//**************************************************************************************************
3944// H A N D L E H T T P *
3945//**************************************************************************************************
3946// Handle the input of an http request. *
3947//**************************************************************************************************
3948void handlehttp()
3949{
3950 bool first = true ; // First call to rinbyt()
3951 char c ; // Next character from http input
3952 int inx0, inx ; // Pos. of search string in currenLine
3953 String currentLine = "" ; // Build up to complete line
3954 bool reqseen = false ; // No GET seen yet
3955
3956 if ( !cmdclient.connected() ) // Action if client is connected
3957 {
3958 return ; // No client active
3959 }
3960 dbgprint ( "handlehttp started" ) ;
3961 while ( true ) // Loop till command/file seen
3962 {
3963 c = rinbyt ( first ) ; // Get a byte
3964 first = false ; // No more first call
3965 if ( c == '\n' )
3966 {
3967 // If the current line is blank, you got two newline characters in a row.
3968 // that's the end of the client HTTP request, so send a response:
3969 if ( currentLine.length() == 0 )
3970 {
3971 http_response_flag = reqseen ; // Response required or not
3972 break ;
3973 }
3974 else
3975 {
3976 // Newline seen, remember if it is like "GET /xxx?y=2&b=9 HTTP/1.1"
3977 if ( currentLine.startsWith ( "GET /" ) ) // GET request?
3978 {
3979 inx0 = 5 ; // Start search at pos 5
3980 }
3981 else if ( currentLine.startsWith ( "POST /" ) ) // POST request?
3982 {
3983 inx0 = 6 ;
3984 }
3985 else
3986 {
3987 inx0 = 0 ; // Not GET nor POST
3988 }
3989 if ( inx0 ) // GET or POST request?
3990 {
3991 reqseen = true ; // Request seen
3992 inx = currentLine.indexOf ( "&" ) ; // Search for 2nd parameter
3993 if ( inx < 0 )
3994 {
3995 inx = currentLine.indexOf ( " HTTP" ) ; // Search for end of GET command
3996 }
3997 // Isolate the command
3998 http_getcmd = currentLine.substring ( inx0, inx ) ;
3999 inx = http_getcmd.indexOf ( "?" ) ; // Search for command
4000 if ( inx == 0 ) // Arguments only?
4001 {
4002 http_getcmd = http_getcmd.substring ( 1 ) ; // Yes, get rid of question mark
4003 http_rqfile = "" ; // No file
4004 }
4005 else if ( inx > 0 ) // Filename present?
4006 {
4007 http_rqfile = http_getcmd.substring ( 0, inx ) ; // Remember filename
4008 http_getcmd = http_getcmd.substring ( inx + 1 ) ; // Remove filename from GET command
4009 }
4010 else
4011 {
4012 http_rqfile = http_getcmd ; // No parameters, set filename
4013 http_getcmd = "" ;
4014 }
4015 if ( http_getcmd.length() )
4016 {
4017 dbgprint ( "Get command is: %s", // Show result
4018 http_getcmd.c_str() ) ;
4019 }
4020 if ( http_rqfile.length() )
4021 {
4022 dbgprint ( "Filename is: %s", // Show requested file
4023 http_rqfile.c_str() ) ;
4024 }
4025 }
4026 currentLine = "" ;
4027 }
4028 }
4029 else if ( c != '\r' ) // No LINFEED. Is it a CR?
4030 {
4031 currentLine += c ; // No, add normal char to currentLine
4032 }
4033 }
4034 //cmdclient.stop() ;
4035}
4036
4037
4038//**************************************************************************************************
4039// X M L P A R S E *
4040//**************************************************************************************************
4041// Parses line with XML data and put result in variable specified by parameter. *
4042//**************************************************************************************************
4043void xmlparse ( String &line, const char *selstr, String &res )
4044{
4045 String sel = "</" ; // Will be like "</status-code"
4046 int inx ; // Position of "</..." in line
4047
4048 sel += selstr ; // Form searchstring
4049 if ( line.endsWith ( sel ) ) // Is this the line we are looking for?
4050 {
4051 inx = line.indexOf ( sel ) ; // Get position of end tag
4052 res = line.substring ( 0, inx ) ; // Set result
4053 }
4054}
4055
4056
4057//**************************************************************************************************
4058// X M L G E T H O S T *
4059//**************************************************************************************************
4060// Parses streams from XML data. *
4061// Example URL for XML Data Stream: *
4062// http://playerservices.streamtheworld.com/api/livestream?version=1.5&mount=IHR_TRANAAC&lang=en *
4063//**************************************************************************************************
4064String xmlgethost ( String mount )
4065{
4066 const char* xmlhost = "playerservices.streamtheworld.com" ; // XML data source
4067 const char* xmlget = "GET /api/livestream" // XML get parameters
4068 "?version=1.5" // API Version of IHeartRadio
4069 "&mount=%sAAC" // MountPoint with Station Callsign
4070 "&lang=en" ; // Language
4071
4072 String stationServer = "" ; // Radio stream server
4073 String stationPort = "" ; // Radio stream port
4074 String stationMount = "" ; // Radio stream Callsign
4075 uint16_t timeout = 0 ; // To detect time-out
4076 String sreply = "" ; // Reply from playerservices.streamtheworld.com
4077 String statuscode = "200" ; // Assume good reply
4078 char tmpstr[200] ; // Full GET command, later stream URL
4079 String urlout ; // Result URL
4080
4081 stop_mp3client() ; // Stop any current wificlient connections.
4082 dbgprint ( "Connect to new iHeartRadio host: %s", mount.c_str() ) ;
4083 datamode = INIT ; // Start default in metamode
4084 chunked = false ; // Assume not chunked
4085 sprintf ( tmpstr, xmlget, mount.c_str() ) ; // Create a GET commmand for the request
4086 dbgprint ( "%s", tmpstr ) ;
4087 if ( mp3client.connect ( xmlhost, 80 ) ) // Connect to XML stream
4088 {
4089 dbgprint ( "Connected to %s", xmlhost ) ;
4090 mp3client.print ( String ( tmpstr ) + " HTTP/1.1\r\n"
4091 "Host: " + xmlhost + "\r\n"
4092 "User-Agent: Mozilla/5.0\r\n"
4093 "Connection: close\r\n\r\n" ) ;
4094 while ( mp3client.available() == 0 )
4095 {
4096 delay ( 200 ) ; // Give server some time
4097 if ( ++timeout > 25 ) // No answer in 5 seconds?
4098 {
4099 dbgprint ( "Client Timeout !" ) ;
4100 }
4101 }
4102 dbgprint ( "XML parser processing..." ) ;
4103 while ( mp3client.available() )
4104 {
4105 sreply = mp3client.readStringUntil ( '>' ) ;
4106 sreply.trim() ;
4107 // Search for relevant info in in reply and store in variable
4108 xmlparse ( sreply, "status-code", statuscode ) ;
4109 xmlparse ( sreply, "ip", stationServer ) ;
4110 xmlparse ( sreply, "port", stationPort ) ;
4111 xmlparse ( sreply, "mount", stationMount ) ;
4112 if ( statuscode != "200" ) // Good result sofar?
4113 {
4114 dbgprint ( "Bad xml status-code %s", // No, show and stop interpreting
4115 statuscode.c_str() ) ;
4116 tmpstr[0] = '\0' ; // Clear result
4117 break ;
4118 }
4119 }
4120 if ( ( stationServer != "" ) && // Check if all station values are stored
4121 ( stationPort != "" ) &&
4122 ( stationMount != "" ) )
4123 {
4124 sprintf ( tmpstr, "%s:%s/%s_SC", // Build URL for ESP-Radio to stream.
4125 stationServer.c_str(),
4126 stationPort.c_str(),
4127 stationMount.c_str() ) ;
4128 dbgprint ( "Found: %s", tmpstr ) ;
4129 }
4130 }
4131 else
4132 {
4133 dbgprint ( "Can't connect to XML host!" ) ; // Connection failed
4134 tmpstr[0] = '\0' ;
4135 }
4136 mp3client.stop() ;
4137 return String ( tmpstr ) ; // Return final streaming URL.
4138}
4139
4140
4141//**************************************************************************************************
4142// H A N D L E S A V E R E Q *
4143//**************************************************************************************************
4144// Handle save volume/preset/tone. This will save current settings every 10 minutes to *
4145// the preferences. On the next restart these values will be loaded. *
4146// Note that saving prefences will only take place if contents has changed. *
4147//**************************************************************************************************
4148void handleSaveReq()
4149{
4150 static uint32_t savetime = 0 ; // Limit save to once per 10 minutes
4151
4152 if ( ( millis() - savetime ) < 600000 ) // 600 sec is 10 minutes
4153 {
4154 return ;
4155 }
4156 savetime = millis() ; // Set time of last save
4157 nvssetstr ( "preset", String ( currentpreset ) ) ; // Save current preset
4158 nvssetstr ( "volume", String ( ini_block.reqvol ) ); // Save current volue
4159 nvssetstr ( "toneha", String ( ini_block.rtone[0] ) ) ; // Save current toneha
4160 nvssetstr ( "tonehf", String ( ini_block.rtone[1] ) ) ; // Save current tonehf
4161 nvssetstr ( "tonela", String ( ini_block.rtone[2] ) ) ; // Save current tonela
4162 nvssetstr ( "tonelf", String ( ini_block.rtone[3] ) ) ; // Save current tonelf
4163}
4164
4165
4166//**************************************************************************************************
4167// H A N D L E I P P U B *
4168//**************************************************************************************************
4169// Handle publish op IP to MQTT. This will happen every 10 minutes. *
4170//**************************************************************************************************
4171void handleIpPub()
4172{
4173 static uint32_t pubtime = 300000 ; // Limit save to once per 10 minutes
4174
4175 if ( ( millis() - pubtime ) < 600000 ) // 600 sec is 10 minutes
4176 {
4177 return ;
4178 }
4179 pubtime = millis() ; // Set time of last publish
4180 mqttpub.trigger ( MQTT_IP ) ; // Request re-publish IP
4181}
4182
4183
4184//**************************************************************************************************
4185// H A N D L E V O L P U B *
4186//**************************************************************************************************
4187// Handle publish of Volume to MQTT. This will happen max every 10 seconds. *
4188//**************************************************************************************************
4189void handleVolPub()
4190{
4191 static uint32_t pubtime = 10000 ; // Limit save to once per 10 seconds
4192 static uint8_t oldvol = -1 ; // For comparison
4193
4194 if ( ( millis() - pubtime ) < 10000 ) // 10 seconds
4195 {
4196 return ;
4197 }
4198 pubtime = millis() ; // Set time of last publish
4199 if ( ini_block.reqvol != oldvol ) // Volume change?
4200 {
4201 mqttpub.trigger ( MQTT_VOLUME ) ; // Request publish VOLUME
4202 oldvol = ini_block.reqvol ; // Remember publishe volume
4203 }
4204}
4205
4206
4207
4208//**************************************************************************************************
4209// C H K _ E N C *
4210//**************************************************************************************************
4211// See if rotary encoder is activated and perform its functions. *
4212//**************************************************************************************************
4213void chk_enc()
4214{
4215 static int8_t enc_preset ; // Selected preset
4216 static String enc_nodeID ; // Node of selected track
4217 static String enc_filename ; // Filename of selected track
4218 String tmp ; // Temporary string
4219 int16_t inx ; // Position in string
4220
4221 if ( enc_menu_mode != VOLUME ) // In default mode?
4222 {
4223 if ( enc_inactivity > 40 ) // No, more than 4 seconds inactive
4224 {
4225 enc_inactivity = 0 ;
4226 enc_menu_mode = VOLUME ; // Return to VOLUME mode
4227 dbgprint ( "Encoder mode back to VOLUME" ) ;
4228 tftset ( 2, (char*)NULL ) ; // Restore original text at bottom
4229 }
4230 }
4231 if ( singleclick || doubleclick || // Any activity?
4232 tripleclick || longclick ||
4233 ( rotationcount != 0 ) )
4234 {
4235 blset ( true ) ; // Yes, activate display if needed
4236 }
4237 else
4238 {
4239 return ; // No, nothing to do
4240 }
4241 if ( tripleclick ) // First handle triple click
4242 {
4243 dbgprint ( "Triple click") ;
4244 tripleclick = false ;
4245 if ( SD_nodecount ) // Tracks on SD?
4246 {
4247 enc_menu_mode = TRACK ; // Swich to TRACK mode
4248 dbgprint ( "Encoder mode set to TRACK" ) ;
4249 tftset ( 3, "Turn to select track\n" // Show current option
4250 "Press to confirm" ) ;
4251 enc_nodeID = selectnextSDnode ( SD_currentnode, +1 ) ; // Start with next file on SD
4252 if ( enc_nodeID == "" ) // Current track available?
4253 {
4254 inx = SD_nodelist.indexOf ( "\n" ) ; // No, find first
4255 enc_nodeID = SD_nodelist.substring ( 0, inx ) ;
4256 }
4257 // Stop playing as reading filenames saturates SD I/O.
4258 if ( datamode != STOPPED )
4259 {
4260 datamode = STOPREQD ; // Request STOP
4261 }
4262 }
4263 }
4264 if ( doubleclick ) // Handle the doubleclick
4265 {
4266 dbgprint ( "Double click") ;
4267 doubleclick = false ;
4268 enc_menu_mode = PRESET ; // Swich to PRESET mode
4269 dbgprint ( "Encoder mode set to PRESET" ) ;
4270 tftset ( 3, "Turn to select station\n" // Show current option
4271 "Press to confirm" ) ;
4272 enc_preset = ini_block.newpreset + 1 ; // Start with current preset + 1
4273 }
4274 if ( singleclick )
4275 {
4276 dbgprint ( "Single click") ;
4277 singleclick = false ;
4278 switch ( enc_menu_mode ) // Which mode (VOLUME, PRESET, TRACK)?
4279 {
4280 case VOLUME :
4281 if ( muteflag )
4282 {
4283 tftset ( 3, "" ) ; // Clear text
4284 }
4285 else
4286 {
4287 tftset ( 3, "Mute" ) ;
4288 }
4289 muteflag = !muteflag ; // Mute/unmute
4290 break ;
4291 case PRESET :
4292 currentpreset = -1 ; // Make sure current is different
4293 ini_block.newpreset = enc_preset ; // Make a definite choice
4294 enc_menu_mode = VOLUME ; // Back to default mode
4295 tftset ( 3, "" ) ; // Clear text
4296 break ;
4297 case TRACK :
4298 host = enc_filename ; // Selected track as new host
4299 hostreq = true ; // Request this host
4300 enc_menu_mode = VOLUME ; // Back to default mode
4301 tftset ( 3, "" ) ; // Clear text
4302 break ;
4303 }
4304 }
4305 if ( longclick ) // Check for long click
4306 {
4307 dbgprint ( "Long click") ;
4308 if ( datamode != STOPPED )
4309 {
4310 datamode = STOPREQD ; // Request STOP, do not touch logclick flag
4311 }
4312 else
4313 {
4314 longclick = false ; // Reset condition
4315 dbgprint ( "Long click detected" ) ;
4316 if ( SD_nodecount ) // Tracks on SD?
4317 {
4318 host = getSDfilename ( "0" ) ; // Get random track
4319 hostreq = true ; // Request this host
4320 }
4321 muteflag = false ; // Be sure muteing is off
4322 }
4323 }
4324 if ( rotationcount == 0 ) // Any rotation?
4325 {
4326 return ; // No, return
4327 }
4328 dbgprint ( "Rotation count %d", rotationcount ) ;
4329 switch ( enc_menu_mode ) // Which mode (VOLUME, PRESET, TRACK)?
4330 {
4331 case VOLUME :
4332 if ( ( ini_block.reqvol + rotationcount ) < 0 ) // Limit volume
4333 {
4334 ini_block.reqvol = 0 ; // Limit to normal values
4335 }
4336 else if ( ( ini_block.reqvol + rotationcount ) > 100 )
4337 {
4338 ini_block.reqvol = 100 ; // Limit to normal values
4339 }
4340 else
4341 {
4342 ini_block.reqvol += rotationcount ;
4343 }
4344 muteflag = false ; // Mute off
4345 break ;
4346 case PRESET :
4347 if ( ( enc_preset + rotationcount ) < 0 ) // Negative not allowed
4348 {
4349 enc_preset = 0 ; // Stay at 0
4350 }
4351 else
4352 {
4353 enc_preset += rotationcount ; // Next preset
4354 }
4355 tmp = readhostfrompref ( enc_preset ) ; // Get host spec and possible comment
4356 if ( tmp == "" ) // End of presets?
4357 {
4358 enc_preset = 0 ; // Yes, wrap
4359 tmp = readhostfrompref ( enc_preset ) ; // Get host spec and possible comment
4360 }
4361 dbgprint ( "Preset is %d", enc_preset ) ;
4362 // Show just comment if available. Otherwise the preset itself.
4363 inx = tmp.indexOf ( "#" ) ; // Get position of "#"
4364 if ( inx > 0 ) // Hash sign present?
4365 {
4366 tmp.remove ( 0, inx + 1 ) ; // Yes, remove non-comment part
4367 }
4368 chomp ( tmp ) ; // Remove garbage from description
4369 tftset ( 3, tmp ) ; // Set screen segment bottom part
4370 break ;
4371 case TRACK :
4372 enc_nodeID = selectnextSDnode ( enc_nodeID,
4373 rotationcount ) ; // Select the next file on SD
4374 enc_filename = getSDfilename ( enc_nodeID ) ; // Set new filename
4375 tmp = enc_filename ; // Copy for display
4376 dbgprint ( "Select %s", tmp.c_str() ) ;
4377 while ( ( inx = tmp.indexOf ( "/" ) ) >= 0 ) // Search for last slash
4378 {
4379 tmp.remove ( 0, inx + 1 ) ; // Remove before the slash
4380 }
4381 dbgprint ( "Simplified %s", tmp.c_str() ) ;
4382 tftset ( 3, tmp ) ;
4383 // Set screen segment bottom part
4384 default :
4385 break ;
4386 }
4387 rotationcount = 0 ; // Reset
4388}
4389
4390
4391//**************************************************************************************************
4392// M P 3 L O O P *
4393//**************************************************************************************************
4394// Called from the mail loop() for the mp3 functions. *
4395// A connection to an MP3 server is active and we are ready to receive data. *
4396// Normally there is about 2 to 4 kB available in the data stream. This depends on the sender. *
4397//**************************************************************************************************
4398void mp3loop()
4399{
4400 uint32_t maxchunk ; // Max number of bytes to read
4401 int res = 0 ; // Result reading from mp3 stream
4402 uint32_t av = 0 ; // Available in stream
4403 String nodeID ; // Next nodeID of track on SD
4404 uint32_t timing ; // Startime and duration this function
4405 uint32_t qspace ; // Free space in data queue
4406
4407 // Try to keep the Queue to playtask filled up by adding as much bytes as possible
4408 if ( datamode & ( INIT | HEADER | DATA | // Test op playing
4409 METADATA | PLAYLISTINIT |
4410 PLAYLISTHEADER |
4411 PLAYLISTDATA ) )
4412 {
4413 timing = millis() ; // Start time this function
4414 maxchunk = sizeof(tmpbuff) ; // Reduce byte count for this mp3loop()
4415 qspace = uxQueueSpacesAvailable( dataqueue ) * // Compute free space in data queue
4416 sizeof(qdata_struct) ;
4417 if ( localfile ) // Playing file from SD card?
4418 {
4419 av = mp3filelength ; // Bytes left in file
4420 if ( av < maxchunk ) // Reduce byte count for this mp3loop()
4421 {
4422 maxchunk = av ;
4423 }
4424 if ( maxchunk > qspace ) // Enough space in queue?
4425 {
4426 maxchunk = qspace ; // No, limit to free queue space
4427 }
4428 if ( maxchunk ) // Anything to read?
4429 {
4430 claimSPI ( "sdread" ) ; // Claim SPI bus
4431 res = mp3file.read ( tmpbuff, maxchunk ) ; // Read a block of data
4432 releaseSPI() ; // Release SPI bus
4433 mp3filelength -= res ; // Number of bytes left
4434 }
4435 }
4436 else
4437 {
4438 av = mp3client.available() ; // Available from stream
4439 if ( av < maxchunk ) // Limit read size
4440 {
4441 maxchunk = av ;
4442 }
4443 if ( maxchunk > qspace ) // Enough space in queue?
4444 {
4445 maxchunk = qspace ; // No, limit to free queue space
4446 }
4447 if ( maxchunk ) // Anything to read?
4448 {
4449 res = mp3client.read ( tmpbuff, maxchunk ) ; // Read a number of bytes from the stream
4450 }
4451 else
4452 {
4453 if ( datamode == PLAYLISTDATA ) // End of playlist
4454 {
4455 playlist_num = 0 ; // And reset
4456 dbgprint ( "End of playlist seen" ) ;
4457 datamode = STOPPED ;
4458 ini_block.newpreset++ ; // Go to next preset
4459 }
4460 }
4461 }
4462 for ( int i = 0 ; i < res ; i++ )
4463 {
4464 handlebyte_ch ( tmpbuff[i] ) ; // Handle one byte
4465 }
4466 timing = millis() - timing ; // Duration this function
4467 if ( timing > max_mp3loop_time ) // New maximum found?
4468 {
4469 max_mp3loop_time = timing ; // Yes, set new maximum
4470 dbgprint ( "Duration mp3loop %d", timing ) ; // and report it
4471 }
4472 }
4473 if ( datamode == STOPREQD ) // STOP requested?
4474 {
4475 dbgprint ( "STOP requested" ) ;
4476 if ( localfile )
4477 {
4478 claimSPI ( "close" ) ; // Claim SPI bus
4479 mp3file.close() ;
4480 releaseSPI() ; // Release SPI bus
4481 }
4482 else
4483 {
4484 stop_mp3client() ; // Disconnect if still connected
4485 }
4486 chunked = false ; // Not longer chunked
4487 datacount = 0 ; // Reset datacount
4488 outqp = outchunk.buf ; // and pointer
4489 queuefunc ( QSTOPSONG ) ; // Queue a request to stop the song
4490 metaint = 0 ; // No metaint known now
4491 datamode = STOPPED ; // Yes, state becomes STOPPED
4492 return ;
4493 }
4494 if ( localfile ) // Playing from SD?
4495 {
4496 if ( datamode & DATA ) // Test op playing
4497 {
4498 if ( av == 0 ) // End of mp3 data?
4499 {
4500 datamode = STOPREQD ; // End of local mp3-file detected
4501 nodeID = selectnextSDnode ( SD_currentnode,
4502 +1 ) ; // Select the next file on SD
4503 host = getSDfilename ( nodeID ) ;
4504 hostreq = true ; // Request this host
4505 }
4506 }
4507 }
4508 if ( ini_block.newpreset != currentpreset ) // New station or next from playlist requested?
4509 {
4510 if ( datamode != STOPPED ) // Yes, still busy?
4511 {
4512 datamode = STOPREQD ; // Yes, request STOP
4513 }
4514 else
4515 {
4516 if ( playlist_num ) // Playing from playlist?
4517 { // Yes, retrieve URL of playlist
4518 playlist_num += ini_block.newpreset -
4519 currentpreset ; // Next entry in playlist
4520 ini_block.newpreset = currentpreset ; // Stay at current preset
4521 }
4522 else
4523 {
4524 host = readhostfrompref() ; // Lookup preset in preferences
4525 chomp ( host ) ; // Get rid of part after "#"
4526 }
4527 dbgprint ( "New preset/file requested (%d/%d) from %s",
4528 ini_block.newpreset, playlist_num, host.c_str() ) ;
4529 if ( host != "" ) // Preset in ini-file?
4530 {
4531 hostreq = true ; // Force this station as new preset
4532 }
4533 else
4534 {
4535 // This preset is not available, return to preset 0, will be handled in next mp3loop()
4536 dbgprint ( "No host for this preset" ) ;
4537 ini_block.newpreset = 0 ; // Wrap to first station
4538 }
4539 }
4540 }
4541 if ( hostreq ) // New preset or station?
4542 {
4543 hostreq = false ;
4544 currentpreset = ini_block.newpreset ; // Remember current preset
4545 mqttpub.trigger ( MQTT_PRESET ) ; // Request publishing to MQTT
4546 // Find out if this URL is on localhost (SD).
4547 localfile = ( host.indexOf ( "localhost/" ) >= 0 ) ;
4548 if ( localfile ) // Play file from localhost?
4549 {
4550 if ( connecttofile() ) // Yes, open mp3-file
4551 {
4552 datamode = DATA ; // Start in DATA mode
4553 }
4554 }
4555 else
4556 {
4557 if ( host.startsWith ( "ihr/" ) ) // iHeartRadio station requested?
4558 {
4559 host = host.substring ( 4 ) ; // Yes, remove "ihr/"
4560 host = xmlgethost ( host ) ; // Parse the xml to get the host
4561 }
4562 connecttohost() ; // Switch to new host
4563 }
4564 }
4565}
4566
4567
4568//**************************************************************************************************
4569// L O O P *
4570//**************************************************************************************************
4571// Main loop of the program. *
4572//**************************************************************************************************
4573void loop()
4574{
4575 mp3loop() ; // Do mp3 related actions
4576 if ( updatereq ) // Software update requested?
4577 {
4578 if ( displaytype == T_NEXTION ) // NEXTION in use?
4579 {
4580 update_software ( "lstmodn", // Yes, update NEXTION image from remote image
4581 UPDATEHOST, TFTFILE ) ;
4582 }
4583 update_software ( "lstmods", // Update sketch from remote file
4584 UPDATEHOST, BINFILE ) ;
4585 resetreq = true ; // And reset
4586 }
4587 if ( resetreq ) // Reset requested?
4588 {
4589 delay ( 1000 ) ; // Yes, wait some time
4590 ESP.restart() ; // Reboot
4591 }
4592 scanserial() ; // Handle serial input
4593 scanserial2() ; // Handle serial input from NEXTION (if active)
4594 scandigital() ; // Scan digital inputs
4595 scanIR() ; // See if IR input
4596 ArduinoOTA.handle() ; // Check for OTA
4597 mp3loop() ; // Do more mp3 related actions
4598 handlehttpreply() ;
4599 cmdclient = cmdserver.available() ; // Check Input from client?
4600 if ( cmdclient ) // Client connected?
4601 {
4602 dbgprint ( "Command client available" ) ;
4603 handlehttp() ;
4604 }
4605 // Handle MQTT.
4606 if ( mqtt_on )
4607 {
4608 mqttclient.loop() ; // Handling of MQTT connection
4609 }
4610 handleSaveReq() ; // See if time to save settings
4611 handleIpPub() ; // See if time to publish IP
4612 handleVolPub() ; // See if time to publish volume
4613 chk_enc() ; // Check rotary encoder functions
4614}
4615
4616
4617//**************************************************************************************************
4618// C H K H D R L I N E *
4619//**************************************************************************************************
4620// Check if a line in the header is a reasonable headerline. *
4621// Normally it should contain something like "icy-xxxx:abcdef". *
4622//**************************************************************************************************
4623bool chkhdrline ( const char* str )
4624{
4625 char b ; // Byte examined
4626 int len = 0 ; // Lengte van de string
4627
4628 while ( ( b = *str++ ) ) // Search to end of string
4629 {
4630 len++ ; // Update string length
4631 if ( ! isalpha ( b ) ) // Alpha (a-z, A-Z)
4632 {
4633 if ( b != '-' ) // Minus sign is allowed
4634 {
4635 if ( b == ':' ) // Found a colon?
4636 {
4637 return ( ( len > 5 ) && ( len < 50 ) ) ; // Yes, okay if length is okay
4638 }
4639 else
4640 {
4641 return false ; // Not a legal character
4642 }
4643 }
4644 }
4645 }
4646 return false ; // End of string without colon
4647}
4648
4649
4650//**************************************************************************************************
4651// S C A N _ C O N T E N T _ L E N G T H *
4652//**************************************************************************************************
4653// If the line contains content-length information: set clength (content length counter). *
4654//**************************************************************************************************
4655void scan_content_length ( const char* metalinebf )
4656{
4657 if ( strstr ( metalinebf, "Content-Length" ) ) // Line contains content length
4658 {
4659 clength = atoi ( metalinebf + 15 ) ; // Yes, set clength
4660 dbgprint ( "Content-Length is %d", clength ) ; // Show for debugging purposes
4661 }
4662}
4663
4664
4665//**************************************************************************************************
4666// H A N D L E B Y T E _ C H *
4667//**************************************************************************************************
4668// Handle the next byte of data from server. *
4669// Chunked transfer encoding aware. Chunk extensions are not supported. *
4670//**************************************************************************************************
4671void handlebyte_ch ( uint8_t b )
4672{
4673 static int chunksize = 0 ; // Chunkcount read from stream
4674 static uint16_t playlistcnt ; // Counter to find right entry in playlist
4675 static int LFcount ; // Detection of end of header
4676 static bool ctseen = false ; // First line of header seen or not
4677
4678 if ( chunked &&
4679 ( datamode & ( DATA | // Test op DATA handling
4680 METADATA |
4681 PLAYLISTDATA ) ) )
4682 {
4683 if ( chunkcount == 0 ) // Expecting a new chunkcount?
4684 {
4685 if ( b == '\r' ) // Skip CR
4686 {
4687 return ;
4688 }
4689 else if ( b == '\n' ) // LF ?
4690 {
4691 chunkcount = chunksize ; // Yes, set new count
4692 chunksize = 0 ; // For next decode
4693 return ;
4694 }
4695 // We have received a hexadecimal character. Decode it and add to the result.
4696 b = toupper ( b ) - '0' ; // Be sure we have uppercase
4697 if ( b > 9 )
4698 {
4699 b = b - 7 ; // Translate A..F to 10..15
4700 }
4701 chunksize = ( chunksize << 4 ) + b ;
4702 return ;
4703 }
4704 chunkcount-- ; // Update count to next chunksize block
4705 }
4706 if ( datamode == DATA ) // Handle next byte of MP3/Ogg data
4707 {
4708 *outqp++ = b ;
4709 if ( outqp == ( outchunk.buf + sizeof(outchunk.buf) ) ) // Buffer full?
4710 {
4711 // Send data to playtask queue. If the buffer cannot be placed within 200 ticks,
4712 // the queue is full, while the sender tries to send more. The chunk will be dis-
4713 // carded it that case.
4714 xQueueSend ( dataqueue, &outchunk, 200 ) ; // Send to queue
4715 outqp = outchunk.buf ; // Item empty now
4716 }
4717 if ( metaint ) // No METADATA on Ogg streams or mp3 files
4718 {
4719 if ( --datacount == 0 ) // End of datablock?
4720 {
4721 datamode = METADATA ;
4722 metalinebfx = -1 ; // Expecting first metabyte (counter)
4723 }
4724 }
4725 return ;
4726 }
4727 if ( datamode == INIT ) // Initialize for header receive
4728 {
4729 ctseen = false ; // Contents type not seen yet
4730 metaint = 0 ; // No metaint found
4731 LFcount = 0 ; // For detection end of header
4732 bitrate = 0 ; // Bitrate still unknown
4733 dbgprint ( "Switch to HEADER" ) ;
4734 datamode = HEADER ; // Handle header
4735 totalcount = 0 ; // Reset totalcount
4736 metalinebfx = 0 ; // No metadata yet
4737 metalinebf[0] = '\0' ;
4738 }
4739 if ( datamode == HEADER ) // Handle next byte of MP3 header
4740 {
4741 if ( ( b > 0x7F ) || // Ignore unprintable characters
4742 ( b == '\r' ) || // Ignore CR
4743 ( b == '\0' ) ) // Ignore NULL
4744 {
4745 // Yes, ignore
4746 }
4747 else if ( b == '\n' ) // Linefeed ?
4748 {
4749 LFcount++ ; // Count linefeeds
4750 metalinebf[metalinebfx] = '\0' ; // Take care of delimiter
4751 if ( chkhdrline ( metalinebf ) ) // Reasonable input?
4752 {
4753 dbgprint ( "Headerline: %s", // Show headerline
4754 metalinebf ) ;
4755 String metaline = String ( metalinebf ) ; // Convert to string
4756 String lcml = metaline ; // Use lower case for compare
4757 lcml.toLowerCase() ;
4758 if ( lcml.startsWith ( "location: http://" ) ) // Redirection?
4759 {
4760 host = metaline.substring ( 17 ) ; // Yes, get new URL
4761 hostreq = true ; // And request this one
4762 }
4763 if ( lcml.indexOf ( "content-type" ) >= 0) // Line with "Content-Type: xxxx/yyy"
4764 {
4765 ctseen = true ; // Yes, remember seeing this
4766 String ct = metaline.substring ( 13 ) ; // Set contentstype. Not used yet
4767 ct.trim() ;
4768 dbgprint ( "%s seen.", ct.c_str() ) ;
4769 }
4770 if ( lcml.startsWith ( "icy-br:" ) )
4771 {
4772 bitrate = metaline.substring(7).toInt() ; // Found bitrate tag, read the bitrate
4773 if ( bitrate == 0 ) // For Ogg br is like "Quality 2"
4774 {
4775 bitrate = 87 ; // Dummy bitrate
4776 }
4777 }
4778 else if ( lcml.startsWith ("icy-metaint:" ) )
4779 {
4780 metaint = metaline.substring(12).toInt() ; // Found metaint tag, read the value
4781 }
4782 else if ( lcml.startsWith ( "icy-name:" ) )
4783 {
4784 icyname = metaline.substring(9) ; // Get station name
4785 icyname.trim() ; // Remove leading and trailing spaces
4786 tftset ( 2, icyname ) ; // Set screen segment bottom part
4787 mqttpub.trigger ( MQTT_ICYNAME ) ; // Request publishing to MQTT
4788 }
4789 else if ( lcml.startsWith ( "transfer-encoding:" ) )
4790 {
4791 // Station provides chunked transfer
4792 if ( lcml.endsWith ( "chunked" ) )
4793 {
4794 chunked = true ; // Remember chunked transfer mode
4795 chunkcount = 0 ; // Expect chunkcount in DATA
4796 }
4797 }
4798 }
4799 metalinebfx = 0 ; // Reset this line
4800 if ( ( LFcount == 2 ) && ctseen ) // Content type seen and a double LF?
4801 {
4802 dbgprint ( "Switch to DATA, bitrate is %d" // Show bitrate
4803 ", metaint is %d", // and metaint
4804 bitrate, metaint ) ;
4805 datamode = DATA ; // Expecting data now
4806 datacount = metaint ; // Number of bytes before first metadata
4807 queuefunc ( QSTARTSONG ) ; // Queue a request to start song
4808 }
4809 }
4810 else
4811 {
4812 metalinebf[metalinebfx++] = (char)b ; // Normal character, put new char in metaline
4813 if ( metalinebfx >= METASIZ ) // Prevent overflow
4814 {
4815 metalinebfx-- ;
4816 }
4817 LFcount = 0 ; // Reset double CRLF detection
4818 }
4819 return ;
4820 }
4821 if ( datamode == METADATA ) // Handle next byte of metadata
4822 {
4823 if ( metalinebfx < 0 ) // First byte of metadata?
4824 {
4825 metalinebfx = 0 ; // Prepare to store first character
4826 metacount = b * 16 + 1 ; // New count for metadata including length byte
4827 if ( metacount > 1 )
4828 {
4829 dbgprint ( "Metadata block %d bytes",
4830 metacount - 1 ) ; // Most of the time there are zero bytes of metadata
4831 }
4832 }
4833 else
4834 {
4835 metalinebf[metalinebfx++] = (char)b ; // Normal character, put new char in metaline
4836 if ( metalinebfx >= METASIZ ) // Prevent overflow
4837 {
4838 metalinebfx-- ;
4839 }
4840 }
4841 if ( --metacount == 0 )
4842 {
4843 metalinebf[metalinebfx] = '\0' ; // Make sure line is limited
4844 if ( strlen ( metalinebf ) ) // Any info present?
4845 {
4846 // metaline contains artist and song name. For example:
4847 // "StreamTitle='Don McLean - American Pie';StreamUrl='';"
4848 // Sometimes it is just other info like:
4849 // "StreamTitle='60s 03 05 Magic60s';StreamUrl='';"
4850 // Isolate the StreamTitle, remove leading and trailing quotes if present.
4851 showstreamtitle ( metalinebf ) ; // Show artist and title if present in metadata
4852 mqttpub.trigger ( MQTT_STREAMTITLE ) ; // Request publishing to MQTT
4853 }
4854 if ( metalinebfx > ( METASIZ - 10 ) ) // Unlikely metaline length?
4855 {
4856 dbgprint ( "Metadata block too long! Skipping all Metadata from now on." ) ;
4857 metaint = 0 ; // Probably no metadata
4858 }
4859 datacount = metaint ; // Reset data count
4860 //bufcnt = 0 ; // Reset buffer count
4861 datamode = DATA ; // Expecting data
4862 }
4863 }
4864 if ( datamode == PLAYLISTINIT ) // Initialize for receive .m3u file
4865 {
4866 // We are going to use metadata to read the lines from the .m3u file
4867 // Sometimes this will only contain a single line
4868 metalinebfx = 0 ; // Prepare for new line
4869 LFcount = 0 ; // For detection end of header
4870 datamode = PLAYLISTHEADER ; // Handle playlist data
4871 playlistcnt = 1 ; // Reset for compare
4872 totalcount = 0 ; // Reset totalcount
4873 clength = 0xFFFFFFFF ; // Content-length unknown
4874 dbgprint ( "Read from playlist" ) ;
4875 }
4876 if ( datamode == PLAYLISTHEADER ) // Read header
4877 {
4878 if ( ( b > 0x7F ) || // Ignore unprintable characters
4879 ( b == '\r' ) || // Ignore CR
4880 ( b == '\0' ) ) // Ignore NULL
4881 {
4882 return ; // Quick return
4883 }
4884 else if ( b == '\n' ) // Linefeed ?
4885 {
4886 LFcount++ ; // Count linefeeds
4887 metalinebf[metalinebfx] = '\0' ; // Take care of delimeter
4888 dbgprint ( "Playlistheader: %s", // Show playlistheader
4889 metalinebf ) ;
4890 scan_content_length ( metalinebf ) ; // Check if it is a content-length line
4891 metalinebfx = 0 ; // Ready for next line
4892 if ( LFcount == 2 )
4893 {
4894 dbgprint ( "Switch to PLAYLISTDATA, " // For debug
4895 "search for entry %d",
4896 playlist_num ) ;
4897 datamode = PLAYLISTDATA ; // Expecting data now
4898 mqttpub.trigger ( MQTT_PLAYLISTPOS ) ; // Playlistposition to MQTT
4899 return ;
4900 }
4901 }
4902 else
4903 {
4904 metalinebf[metalinebfx++] = (char)b ; // Normal character, put new char in metaline
4905 if ( metalinebfx >= METASIZ ) // Prevent overflow
4906 {
4907 metalinebfx-- ;
4908 }
4909 LFcount = 0 ; // Reset double CRLF detection
4910 }
4911 }
4912 if ( datamode == PLAYLISTDATA ) // Read next byte of .m3u file data
4913 {
4914 clength-- ; // Decrease content length by 1
4915 if ( ( b > 0x7F ) || // Ignore unprintable characters
4916 ( b == '\r' ) || // Ignore CR
4917 ( b == '\0' ) ) // Ignore NULL
4918 {
4919 // Yes, ignore
4920 }
4921 if ( b != '\n' ) // Linefeed?
4922 { // No, normal character in playlistdata,
4923 metalinebf[metalinebfx++] = (char)b ; // add it to metaline
4924 if ( metalinebfx >= METASIZ ) // Prevent overflow
4925 {
4926 metalinebfx-- ;
4927 }
4928 }
4929 if ( ( b == '\n' ) || // linefeed ?
4930 ( clength == 0 ) ) // Or end of playlist data contents
4931 {
4932 int inx ; // Pointer in metaline
4933 metalinebf[metalinebfx] = '\0' ; // Take care of delimeter
4934 dbgprint ( "Playlistdata: %s", // Show playlistheader
4935 metalinebf ) ;
4936 if ( strlen ( metalinebf ) < 5 ) // Skip short lines
4937 {
4938 metalinebfx = 0 ; // Flush line
4939 metalinebf[0] = '\0' ;
4940 return ;
4941 }
4942 String metaline = String ( metalinebf ) ; // Convert to string
4943 if ( metaline.indexOf ( "#EXTINF:" ) >= 0 ) // Info?
4944 {
4945 if ( playlist_num == playlistcnt ) // Info for this entry?
4946 {
4947 inx = metaline.indexOf ( "," ) ; // Comma in this line?
4948 if ( inx > 0 )
4949 {
4950 // Show artist and title if present in metadata
4951 showstreamtitle ( metaline.substring ( inx + 1 ).c_str(), true ) ;
4952 mqttpub.trigger ( MQTT_STREAMTITLE ) ; // Request publishing to MQTT
4953 }
4954 }
4955 }
4956 if ( metaline.startsWith ( "#" ) ) // Commentline?
4957 {
4958 metalinebfx = 0 ; // Yes, ignore
4959 return ; // Ignore commentlines
4960 }
4961 // Now we have an URL for a .mp3 file or stream. Is it the rigth one?
4962 dbgprint ( "Entry %d in playlist found: %s", playlistcnt, metalinebf ) ;
4963 if ( playlist_num == playlistcnt )
4964 {
4965 inx = metaline.indexOf ( "http://" ) ; // Search for "http://"
4966 if ( inx >= 0 ) // Does URL contain "http://"?
4967 {
4968 host = metaline.substring ( inx + 7 ) ; // Yes, remove it and set host
4969 }
4970 else
4971 {
4972 host = metaline ; // Yes, set new host
4973 }
4974 connecttohost() ; // Connect to it
4975 }
4976 metalinebfx = 0 ; // Prepare for next line
4977 host = playlist ; // Back to the .m3u host
4978 playlistcnt++ ; // Next entry in playlist
4979 }
4980 }
4981}
4982
4983
4984//**************************************************************************************************
4985// G E T C O N T E N T T Y P E *
4986//**************************************************************************************************
4987// Returns the contenttype of a file to send. *
4988//**************************************************************************************************
4989String getContentType ( String filename )
4990{
4991 if ( filename.endsWith ( ".html" ) ) return "text/html" ;
4992 else if ( filename.endsWith ( ".png" ) ) return "image/png" ;
4993 else if ( filename.endsWith ( ".gif" ) ) return "image/gif" ;
4994 else if ( filename.endsWith ( ".jpg" ) ) return "image/jpeg" ;
4995 else if ( filename.endsWith ( ".ico" ) ) return "image/x-icon" ;
4996 else if ( filename.endsWith ( ".css" ) ) return "text/css" ;
4997 else if ( filename.endsWith ( ".zip" ) ) return "application/x-zip" ;
4998 else if ( filename.endsWith ( ".gz" ) ) return "application/x-gzip" ;
4999 else if ( filename.endsWith ( ".mp3" ) ) return "audio/mpeg" ;
5000 else if ( filename.endsWith ( ".pw" ) ) return "" ; // Passwords are secret
5001 return "text/plain" ;
5002}
5003
5004
5005//**************************************************************************************************
5006// H A N D L E F S F *
5007//**************************************************************************************************
5008// Handling of requesting pages from the PROGMEM. Example: favicon.ico *
5009//**************************************************************************************************
5010void handleFSf ( const String& pagename )
5011{
5012 String ct ; // Content type
5013 const char* p ;
5014 int l ; // Size of requested page
5015 int TCPCHUNKSIZE = 1024 ; // Max number of bytes per write
5016
5017 dbgprint ( "FileRequest received %s", pagename.c_str() ) ;
5018 ct = getContentType ( pagename ) ; // Get content type
5019 if ( ( ct == "" ) || ( pagename == "" ) ) // Empty is illegal
5020 {
5021 cmdclient.println ( "HTTP/1.1 404 Not Found" ) ;
5022 cmdclient.println ( "" ) ;
5023 return ;
5024 }
5025 else
5026 {
5027 if ( pagename.indexOf ( "index.html" ) >= 0 ) // Index page is in PROGMEM
5028 {
5029 p = index_html ;
5030 l = sizeof ( index_html ) ;
5031 }
5032 else if ( pagename.indexOf ( "radio.css" ) >= 0 ) // CSS file is in PROGMEM
5033 {
5034 p = radio_css + 1 ;
5035 l = sizeof ( radio_css ) ;
5036 }
5037 else if ( pagename.indexOf ( "config.html" ) >= 0 ) // Config page is in PROGMEM
5038 {
5039 p = config_html ;
5040 l = sizeof ( config_html ) ;
5041 }
5042 else if ( pagename.indexOf ( "mp3play.html" ) >= 0 ) // Mp3player page is in PROGMEM
5043 {
5044 p = mp3play_html ;
5045 l = sizeof ( mp3play_html ) ;
5046 }
5047 else if ( pagename.indexOf ( "about.html" ) >= 0 ) // About page is in PROGMEM
5048 {
5049 p = about_html ;
5050 l = sizeof ( about_html ) ;
5051 }
5052 else if ( pagename.indexOf ( "favicon.ico" ) >= 0 ) // Favicon icon is in PROGMEM
5053 {
5054 p = (char*)favicon_ico ;
5055 l = sizeof ( favicon_ico ) ;
5056 }
5057 else
5058 {
5059 p = index_html ;
5060 l = sizeof ( index_html ) ;
5061 }
5062 if ( *p == '\n' ) // If page starts with newline:
5063 {
5064 p++ ; // Skip first character
5065 l-- ;
5066 }
5067 dbgprint ( "Length of page is %d", strlen ( p ) ) ;
5068 cmdclient.print ( httpheader ( ct ) ) ; // Send header
5069 // The content of the HTTP response follows the header:
5070 if ( l < 10 )
5071 {
5072 cmdclient.println ( "Testline<br>" ) ;
5073 }
5074 else
5075 {
5076 while ( l ) // Loop through the output page
5077 {
5078 if ( l <= TCPCHUNKSIZE ) // Near the end?
5079 {
5080 cmdclient.write ( p, l ) ; // Yes, send last part
5081 l = 0 ;
5082 }
5083 else
5084 {
5085 cmdclient.write ( p, TCPCHUNKSIZE ) ; // Send part of the page
5086 p += TCPCHUNKSIZE ; // Update startpoint and rest of bytes
5087 l -= TCPCHUNKSIZE ;
5088 }
5089 }
5090 }
5091 // The HTTP response ends with another blank line:
5092 cmdclient.println() ;
5093 dbgprint ( "Response send" ) ;
5094 }
5095}
5096
5097
5098//**************************************************************************************************
5099// C H O M P *
5100//**************************************************************************************************
5101// Do some filtering on de inputstring: *
5102// - String comment part (starting with "#"). *
5103// - Strip trailing CR. *
5104// - Strip leading spaces. *
5105// - Strip trailing spaces. *
5106//**************************************************************************************************
5107void chomp ( String &str )
5108{
5109 int inx ; // Index in de input string
5110
5111 if ( ( inx = str.indexOf ( "#" ) ) >= 0 ) // Comment line or partial comment?
5112 {
5113 str.remove ( inx ) ; // Yes, remove
5114 }
5115 str.trim() ; // Remove spaces and CR
5116}
5117
5118
5119//**************************************************************************************************
5120// A N A L Y Z E C M D *
5121//**************************************************************************************************
5122// Handling of the various commands from remote webclient, Serial or MQTT. *
5123// Version for handling string with: <parameter>=<value> *
5124//**************************************************************************************************
5125const char* analyzeCmd ( const char* str )
5126{
5127 char* value ; // Points to value after equalsign in command
5128 const char* res ; // Result of analyzeCmd
5129
5130 value = strstr ( str, "=" ) ; // See if command contains a "="
5131 if ( value )
5132 {
5133 *value = '\0' ; // Separate command from value
5134 res = analyzeCmd ( str, value + 1 ) ; // Analyze command and handle it
5135 *value = '=' ; // Restore equal sign
5136 }
5137 else
5138 {
5139 res = analyzeCmd ( str, "0" ) ; // No value, assume zero
5140 }
5141 return res ;
5142}
5143
5144
5145//**************************************************************************************************
5146// A N A L Y Z E C M D *
5147//**************************************************************************************************
5148// Handling of the various commands from remote webclient, serial or MQTT. *
5149// par holds the parametername and val holds the value. *
5150// "wifi_00" and "preset_00" may appear more than once, like wifi_01, wifi_02, etc. *
5151// Examples with available parameters: *
5152// preset = 12 // Select start preset to connect to *
5153// preset_00 = <mp3 stream> // Specify station for a preset 00-99 *) *
5154// volume = 95 // Percentage between 0 and 100 *
5155// upvolume = 2 // Add percentage to current volume *
5156// downvolume = 2 // Subtract percentage from current volume *
5157// toneha = <0..15> // Setting treble gain *
5158// tonehf = <0..15> // Setting treble frequency *
5159// tonela = <0..15> // Setting bass gain *
5160// tonelf = <0..15> // Setting treble frequency *
5161// station = <mp3 stream> // Select new station (will not be saved) *
5162// station = <URL>.mp3 // Play standalone .mp3 file (not saved) *
5163// station = <URL>.m3u // Select playlist (will not be saved) *
5164// stop // Stop playing *
5165// resume // Resume playing *
5166// mute // Mute/unmute the music (toggle) *
5167// wifi_00 = mySSID/mypassword // Set WiFi SSID and password *) *
5168// mqttbroker = mybroker.com // Set MQTT broker to use *) *
5169// mqttprefix = XP93g // Set MQTT broker to use *
5170// mqttport = 1883 // Set MQTT port to use, default 1883 *) *
5171// mqttuser = myuser // Set MQTT user for authentication *) *
5172// mqttpasswd = mypassword // Set MQTT password for authentication *) *
5173// clk_server = pool.ntp.org // Time server to be used *) *
5174// clk_offset = <-11..+14> // Offset with respect to UTC in hours *) *
5175// clk_dst = <1..2> // Offset during daylight saving time in hours *) *
5176// mp3track = <nodeID> // Play track from SD card, nodeID 0 = random *
5177// settings // Returns setting like presets and tone *
5178// status // Show current URL to play *
5179// test // For test purposes *
5180// debug = 0 or 1 // Switch debugging on or off *
5181// reset // Restart the ESP32 *
5182// bat0 = 2318 // ADC value for an empty battery *
5183// bat100 = 2916 // ADC value for a fully charged battery *
5184// Commands marked with "*)" are sensible during initialization only *
5185//**************************************************************************************************
5186const char* analyzeCmd ( const char* par, const char* val )
5187{
5188 String argument ; // Argument as string
5189 String value ; // Value of an argument as a string
5190 int ivalue ; // Value of argument as an integer
5191 static char reply[180] ; // Reply to client, will be returned
5192 uint8_t oldvol ; // Current volume
5193 bool relative ; // Relative argument (+ or -)
5194 String tmpstr ; // Temporary for value
5195 uint32_t av ; // Available in stream/file
5196
5197 blset ( true ) ; // Enable backlight of TFT
5198 strcpy ( reply, "Command accepted" ) ; // Default reply
5199 argument = String ( par ) ; // Get the argument
5200 chomp ( argument ) ; // Remove comment and useless spaces
5201 if ( argument.length() == 0 ) // Lege commandline (comment)?
5202 {
5203 return reply ; // Ignore
5204 }
5205 argument.toLowerCase() ; // Force to lower case
5206 value = String ( val ) ; // Get the specified value
5207 chomp ( value ) ; // Remove comment and extra spaces
5208 ivalue = value.toInt() ; // Also as an integer
5209 ivalue = abs ( ivalue ) ; // Make positive
5210 relative = argument.indexOf ( "up" ) == 0 ; // + relative setting?
5211 if ( argument.indexOf ( "down" ) == 0 ) // - relative setting?
5212 {
5213 relative = true ; // It's relative
5214 ivalue = - ivalue ; // But with negative value
5215 }
5216 if ( value.startsWith ( "http://" ) ) // Does (possible) URL contain "http://"?
5217 {
5218 value.remove ( 0, 7 ) ; // Yes, remove it
5219 }
5220 if ( value.length() )
5221 {
5222 tmpstr = value ; // Make local copy of value
5223 if ( argument.indexOf ( "passw" ) >= 0 ) // Password in value?
5224 {
5225 tmpstr = String ( "*******" ) ; // Yes, hide it
5226 }
5227 dbgprint ( "Command: %s with parameter %s",
5228 argument.c_str(), tmpstr.c_str() ) ;
5229 }
5230 else
5231 {
5232 dbgprint ( "Command: %s (without parameter)",
5233 argument.c_str() ) ;
5234 }
5235 if ( argument.indexOf ( "volume" ) >= 0 ) // Volume setting?
5236 {
5237 // Volume may be of the form "upvolume", "downvolume" or "volume" for relative or absolute setting
5238 oldvol = vs1053player->getVolume() ; // Get current volume
5239 if ( relative ) // + relative setting?
5240 {
5241 ini_block.reqvol = oldvol + ivalue ; // Up/down by 0.5 or more dB
5242 }
5243 else
5244 {
5245 ini_block.reqvol = ivalue ; // Absolue setting
5246 }
5247 if ( ini_block.reqvol > 127 ) // Wrapped around?
5248 {
5249 ini_block.reqvol = 0 ; // Yes, keep at zero
5250 }
5251 if ( ini_block.reqvol > 100 )
5252 {
5253 ini_block.reqvol = 100 ; // Limit to normal values
5254 }
5255 muteflag = false ; // Stop possibly muting
5256 sprintf ( reply, "Volume is now %d", // Reply new volume
5257 ini_block.reqvol ) ;
5258 }
5259 else if ( argument == "mute" ) // Mute/unmute request
5260 {
5261 muteflag = !muteflag ; // Request volume to zero/normal
5262 }
5263 else if ( argument.indexOf ( "ir_" ) >= 0 ) // Ir setting?
5264 { // Do not handle here
5265 }
5266 else if ( argument.indexOf ( "preset_" ) >= 0 ) // Enumerated preset?
5267 { // Do not handle here
5268 }
5269 else if ( argument.indexOf ( "preset" ) >= 0 ) // (UP/DOWN)Preset station?
5270 {
5271 // If MP3 player is active: change track
5272 if ( localfile &&
5273 ( ( datamode & DATA ) != 0 ) && // MP# player active?
5274 relative )
5275 {
5276 datamode = STOPREQD ; // Force stop MP3 player
5277 tmpstr = selectnextSDnode ( SD_currentnode,
5278 ivalue ) ; // Select the next or previous file on SD
5279 host = getSDfilename ( tmpstr ) ;
5280 hostreq = true ; // Request this host
5281 sprintf ( reply, "Playing %s", // Reply new filename
5282 host.c_str() ) ;
5283 }
5284 else
5285 {
5286 if ( relative ) // Relative argument?
5287 {
5288 currentpreset = ini_block.newpreset ; // Remember currentpreset
5289 ini_block.newpreset += ivalue ; // Yes, adjust currentpreset
5290 }
5291 else
5292 {
5293 ini_block.newpreset = ivalue ; // Otherwise set station
5294 playlist_num = 0 ; // Absolute, reset playlist
5295 currentpreset = -1 ; // Make sure current is different
5296 }
5297 datamode = STOPREQD ; // Force stop MP3 player
5298 sprintf ( reply, "Preset is now %d", // Reply new preset
5299 ini_block.newpreset ) ;
5300 }
5301 }
5302 else if ( argument == "stop" ) // (un)Stop requested?
5303 {
5304 if ( datamode & ( HEADER | DATA | METADATA | PLAYLISTINIT |
5305 PLAYLISTHEADER | PLAYLISTDATA ) )
5306
5307 {
5308 datamode = STOPREQD ; // Request STOP
5309 }
5310 else
5311 {
5312 hostreq = true ; // Request UNSTOP
5313 }
5314 }
5315 else if ( ( value.length() > 0 ) &&
5316 ( ( argument == "mp3track" ) || // Select a track from SD card?
5317 ( argument == "station" ) ) ) // Station in the form address:port
5318 {
5319 if ( argument.startsWith ( "mp3" ) ) // MP3 track to search for
5320 {
5321 if ( !SD_okay ) // SD card present?
5322 {
5323 strcpy ( reply, "Command not accepted!" ) ; // Error reply
5324 return reply ;
5325 }
5326 value = getSDfilename ( value ) ; // like "localhost/........"
5327 }
5328 if ( datamode & ( HEADER | DATA | METADATA | PLAYLISTINIT |
5329 PLAYLISTHEADER | PLAYLISTDATA ) )
5330 {
5331 datamode = STOPREQD ; // Request STOP
5332 }
5333 host = value ; // Save it for storage and selection later
5334 hostreq = true ; // Force this station as new preset
5335 sprintf ( reply,
5336 "Playing %s", // Format reply
5337 host.c_str() ) ;
5338 utf8ascii ( reply ) ; // Remove possible strange characters
5339 }
5340 else if ( argument == "status" ) // Status request
5341 {
5342 if ( datamode == STOPPED )
5343 {
5344 sprintf ( reply, "Player stopped" ) ; // Format reply
5345 }
5346 else
5347 {
5348 sprintf ( reply, "%s - %s", icyname.c_str(),
5349 icystreamtitle.c_str() ) ; // Streamtitle from metadata
5350 }
5351 }
5352 else if ( argument.startsWith ( "reset" ) ) // Reset request
5353 {
5354 resetreq = true ; // Reset all
5355 }
5356 else if ( argument.startsWith ( "update" ) ) // Update request
5357 {
5358 updatereq = true ; // Reset all
5359 }
5360 else if ( argument == "test" ) // Test command
5361 {
5362 if ( localfile )
5363 {
5364 av = mp3filelength ; // Available bytes in file
5365 }
5366 else
5367 {
5368 av = mp3client.available() ; // Available in stream
5369 }
5370 sprintf ( reply, "Free memory is %d, chunks in queue %d, stream %d, bitrate %d kbps",
5371 ESP.getFreeHeap(),
5372 uxQueueMessagesWaiting ( dataqueue ),
5373 av,
5374 mbitrate ) ;
5375 dbgprint ( "Stack maintask is %d", uxTaskGetStackHighWaterMark ( maintask ) ) ;
5376 dbgprint ( "Stack playtask is %d", uxTaskGetStackHighWaterMark ( xplaytask ) ) ;
5377 dbgprint ( "Stack spftask is %d", uxTaskGetStackHighWaterMark ( xspftask ) ) ;
5378 dbgprint ( "ADC reading is %d", adcval ) ;
5379 dbgprint ( "scaniocount is %d", scaniocount ) ;
5380 dbgprint ( "Max. mp3_loop duration is %d", max_mp3loop_time ) ;
5381 max_mp3loop_time = 0 ; // Start new check
5382 }
5383 // Commands for bass/treble control
5384 else if ( argument.startsWith ( "tone" ) ) // Tone command
5385 {
5386 if ( argument.indexOf ( "ha" ) > 0 ) // High amplitue? (for treble)
5387 {
5388 ini_block.rtone[0] = ivalue ; // Yes, prepare to set ST_AMPLITUDE
5389 }
5390 if ( argument.indexOf ( "hf" ) > 0 ) // High frequency? (for treble)
5391 {
5392 ini_block.rtone[1] = ivalue ; // Yes, prepare to set ST_FREQLIMIT
5393 }
5394 if ( argument.indexOf ( "la" ) > 0 ) // Low amplitue? (for bass)
5395 {
5396 ini_block.rtone[2] = ivalue ; // Yes, prepare to set SB_AMPLITUDE
5397 }
5398 if ( argument.indexOf ( "lf" ) > 0 ) // High frequency? (for bass)
5399 {
5400 ini_block.rtone[3] = ivalue ; // Yes, prepare to set SB_FREQLIMIT
5401 }
5402 reqtone = true ; // Set change request
5403 sprintf ( reply, "Parameter for bass/treble %s set to %d",
5404 argument.c_str(), ivalue ) ;
5405 }
5406 else if ( argument == "rate" ) // Rate command?
5407 {
5408 vs1053player->AdjustRate ( ivalue ) ; // Yes, adjust
5409 }
5410 else if ( argument.startsWith ( "mqtt" ) ) // Parameter fo MQTT?
5411 {
5412 strcpy ( reply, "MQTT broker parameter changed. Save and restart to have effect" ) ;
5413 if ( argument.indexOf ( "broker" ) > 0 ) // Broker specified?
5414 {
5415 ini_block.mqttbroker = value ; // Yes, set broker accordingly
5416 }
5417 else if ( argument.indexOf ( "prefix" ) > 0 ) // Port specified?
5418 {
5419 ini_block.mqttprefix = value ; // Yes, set port user accordingly
5420 }
5421 else if ( argument.indexOf ( "port" ) > 0 ) // Port specified?
5422 {
5423 ini_block.mqttport = ivalue ; // Yes, set port user accordingly
5424 }
5425 else if ( argument.indexOf ( "user" ) > 0 ) // User specified?
5426 {
5427 ini_block.mqttuser = value ; // Yes, set user accordingly
5428 }
5429 else if ( argument.indexOf ( "passwd" ) > 0 ) // Password specified?
5430 {
5431 ini_block.mqttpasswd = value.c_str() ; // Yes, set broker password accordingly
5432 }
5433 }
5434 else if ( argument == "debug" ) // debug on/off request?
5435 {
5436 DEBUG = ivalue ; // Yes, set flag accordingly
5437 }
5438 else if ( argument == "getnetworks" ) // List all WiFi networks?
5439 {
5440 sprintf ( reply, networks.c_str() ) ; // Reply is SSIDs
5441 }
5442 else if ( argument.startsWith ( "clk_" ) ) // TOD parameter?
5443 {
5444 if ( argument.indexOf ( "server" ) > 0 ) // Yes, NTP server spec?
5445 {
5446 ini_block.clk_server = value ; // Yes, set server
5447 }
5448 if ( argument.indexOf ( "offset" ) > 0 ) // Offset with respect to UTC spec?
5449 {
5450 ini_block.clk_offset = value.toInt() ; // Yes, set offset
5451 }
5452 if ( argument.indexOf ( "dst" ) > 0 ) // Offset duringe DST spec?
5453 {
5454 ini_block.clk_dst = value.toInt() ; // Yes, set DST offset
5455 }
5456 }
5457 else if ( argument.startsWith ( "bat" ) ) // Battery ADC value?
5458 {
5459 if ( argument.indexOf ( "100" ) == 3 ) // 100 percent value?
5460 {
5461 ini_block.bat100 = ivalue ; // Yes, set it
5462 }
5463 else if ( argument.indexOf ( "0" ) == 3 ) // 0 percent value?
5464 {
5465 ini_block.bat0 = ivalue ; // Yes, set it
5466 }
5467 }
5468 else
5469 {
5470 sprintf ( reply, "%s called with illegal parameter: %s",
5471 NAME, argument.c_str() ) ;
5472 }
5473 return reply ; // Return reply to the caller
5474}
5475
5476
5477//**************************************************************************************************
5478// H T T P H E A D E R *
5479//**************************************************************************************************
5480// Set http headers to a string. *
5481//**************************************************************************************************
5482String httpheader ( String contentstype )
5483{
5484 return String ( "HTTP/1.1 200 OK\nContent-type:" ) +
5485 contentstype +
5486 String ( "\n"
5487 "Server: " NAME "\n"
5488 "Cache-Control: " "max-age=3600\n"
5489 "Last-Modified: " VERSION "\n\n" ) ;
5490}
5491
5492
5493//**************************************************************************************************
5494//* Function that are called from spftask. *
5495//* Note that some device dependent function are place in the *.h files. *
5496//**************************************************************************************************
5497
5498//**************************************************************************************************
5499// D I S P L A Y I N F O *
5500//**************************************************************************************************
5501// Show a string on the LCD at a specified y-position (0..2) in a specified color. *
5502// The parameter is the index in tftdata[]. *
5503//**************************************************************************************************
5504void displayinfo ( uint16_t inx )
5505{
5506 uint16_t width = dsp_getwidth() ; // Normal number of colums
5507 scrseg_struct* p = &tftdata[inx] ;
5508 uint16_t len ; // Length of string, later buffer length
5509
5510 if ( inx == 0 ) // Topline is shorter
5511 {
5512 width += TIMEPOS ; // Leave space for time
5513 }
5514 if ( tft ) // TFT active?
5515 {
5516 dsp_fillRect ( 0, p->y, width, p->height, BLACK ) ; // Clear the space for new info
5517 if ( ( dsp_getheight() > 64 ) && ( p->y > 1 ) ) // Need and space for divider?
5518 {
5519 dsp_fillRect ( 0, p->y - 4, width, 1, GREEN ) ; // Yes, show divider above text
5520 }
5521 len = p->str.length() ; // Required length of buffer
5522 if ( len++ ) // Check string length, set buffer length
5523 {
5524 char buf [ len ] ; // Need some buffer space
5525 p->str.toCharArray ( buf, len ) ; // Make a local copy of the string
5526 utf8ascii ( buf ) ; // Convert possible UTF8
5527 dsp_setTextColor ( p->color ) ; // Set the requested color
5528 dsp_setCursor ( 0, p->y ) ; // Prepare to show the info
5529 if ((inx == 1) && (len<46)) dsp_setTextSize ( 2 ) ;
5530 if ((inx == 2) && (len<=26)) dsp_setTextSize ( 2 ) ;
5531 dsp_println ( buf ) ; // Show the string
5532 dsp_setTextSize ( 1 ) ;
5533 }
5534 }
5535}
5536
5537
5538//**************************************************************************************************
5539// G E T T I M E *
5540//**************************************************************************************************
5541// Retrieve the local time from NTP server and convert to string. *
5542// Will be called every second. *
5543//**************************************************************************************************
5544void gettime()
5545{
5546 static int16_t delaycount = 0 ; // To reduce number of NTP requests
5547 static int16_t retrycount = 100 ;
5548
5549 if ( tft ) // TFT used?
5550 {
5551 if ( timeinfo.tm_year ) // Legal time found?
5552 {
5553 sprintf ( timetxt, "%02d:%02d:%02d", // Yes, format to a string
5554 timeinfo.tm_hour,
5555 timeinfo.tm_min,
5556 timeinfo.tm_sec ) ;
5557 }
5558 if ( --delaycount <= 0 ) // Sync every few hours
5559 {
5560 delaycount = 7200 ; // Reset counter
5561 if ( timeinfo.tm_year ) // Legal time found?
5562 {
5563 dbgprint ( "Sync TOD, old value is %s", timetxt ) ;
5564 }
5565 dbgprint ( "Sync TOD" ) ;
5566 if ( !getLocalTime ( &timeinfo ) ) // Read from NTP server
5567 {
5568 dbgprint ( "Failed to obtain time!" ) ; // Error
5569 timeinfo.tm_year = 0 ; // Set current time to illegal
5570 if ( retrycount ) // Give up syncing?
5571 {
5572 retrycount-- ; // No try again
5573 delaycount = 5 ; // Retry after 5 seconds
5574 }
5575 }
5576 else
5577 {
5578 sprintf ( timetxt, "%02d:%02d:%02d", // Format new time to a string
5579 timeinfo.tm_hour,
5580 timeinfo.tm_min,
5581 timeinfo.tm_sec ) ;
5582 dbgprint ( "Sync TOD, new value is %s", timetxt ) ;
5583 }
5584 }
5585 }
5586}
5587
5588
5589//**************************************************************************************************
5590// H A N D L E _ T F T _ T X T *
5591//**************************************************************************************************
5592// Check if tft refresh is requested. *
5593//**************************************************************************************************
5594bool handle_tft_txt()
5595{
5596 for ( uint16_t i = 0 ; i < TFTSECS ; i++ ) // Handle all sections
5597 {
5598 if ( tftdata[i].update_req ) // Refresh requested?
5599 {
5600 displayinfo ( i ) ; // Yes, do the refresh
5601 dsp_update() ; // Updates to the screen
5602 tftdata[i].update_req = false ; // Reset request
5603 return true ; // Just handle 1 request
5604 }
5605 }
5606 return false ; // Not a single request
5607}
5608
5609
5610//**************************************************************************************************
5611// P L A Y T A S K *
5612//**************************************************************************************************
5613// Play stream data from input queue. *
5614// Handle all I/O to VS1053B during normal playing. *
5615// Handles display of text, time and volume on TFT as well. *
5616//**************************************************************************************************
5617void playtask ( void * parameter )
5618{
5619 while ( true )
5620 {
5621 if ( xQueueReceive ( dataqueue, &inchunk, 5 ) )
5622 {
5623 while ( !vs1053player->data_request() ) // If FIFO is full..
5624 {
5625 vTaskDelay ( 1 ) ; // Yes, take a break
5626 }
5627 switch ( inchunk.datatyp ) // What kind of chunk?
5628 {
5629 case QDATA:
5630 claimSPI ( "chunk" ) ; // Claim SPI bus
5631 vs1053player->playChunk ( inchunk.buf, // DATA, send to player
5632 sizeof(inchunk.buf) ) ;
5633 releaseSPI() ; // Release SPI bus
5634 totalcount += sizeof(inchunk.buf) ; // Count the bytes
5635 break ;
5636 case QSTARTSONG:
5637 playingstat = 1 ; // Status for MQTT
5638 mqttpub.trigger ( MQTT_PLAYING ) ; // Request publishing to MQTT
5639 claimSPI ( "startsong" ) ; // Claim SPI bus
5640 vs1053player->startSong() ; // START, start player
5641 releaseSPI() ; // Release SPI bus
5642 break ;
5643 case QSTOPSONG:
5644 playingstat = 0 ; // Status for MQTT
5645 mqttpub.trigger ( MQTT_PLAYING ) ; // Request publishing to MQTT
5646 claimSPI ( "stopsong" ) ; // Claim SPI bus
5647 vs1053player->setVolume ( 0 ) ; // Mute
5648 vs1053player->stopSong() ; // STOP, stop player
5649 releaseSPI() ; // Release SPI bus
5650 vTaskDelay ( 500 / portTICK_PERIOD_MS ) ; // Pause for a short time
5651 break ;
5652 default:
5653 break ;
5654 }
5655 }
5656 //esp_task_wdt_reset() ; // Protect against idle cpu
5657 }
5658 //vTaskDelete ( NULL ) ; // Will never arrive here
5659}
5660
5661
5662//**************************************************************************************************
5663// H A N D L E _ S P E C *
5664//**************************************************************************************************
5665// Handle special (non-stream data) functions for spftask. *
5666//**************************************************************************************************
5667void handle_spec()
5668{
5669 // Do some special function if necessary
5670 if ( dsp_usesSPI() ) // Does display uses SPI?
5671 {
5672 claimSPI ( "hspectft" ) ; // Yes, claim SPI bus
5673 }
5674 if ( tft ) // Need to update TFT?
5675 {
5676 handle_tft_txt() ; // Yes, TFT refresh necessary
5677 dsp_update() ; // Be sure to paint physical screen
5678 }
5679 if ( dsp_usesSPI() ) // Does display uses SPI?
5680 {
5681 releaseSPI() ; // Yes, release SPI bus
5682 }
5683 if ( time_req && NetworkFound ) // Time to refresh time?
5684 {
5685 gettime() ; // Yes, get the current time
5686 }
5687 claimSPI ( "hspec" ) ; // Claim SPI bus
5688 if ( muteflag ) // Mute or not?
5689 {
5690 vs1053player->setVolume ( 0 ) ; // Mute
5691 }
5692 else
5693 {
5694 vs1053player->setVolume ( ini_block.reqvol ) ; // Unmute
5695 }
5696 if ( reqtone ) // Request to change tone?
5697 {
5698 reqtone = false ;
5699 vs1053player->setTone ( ini_block.rtone ) ; // Set SCI_BASS to requested value
5700 }
5701 if ( time_req ) // Time to refresh timetxt?
5702 {
5703 time_req = false ; // Yes, clear request
5704 if ( NetworkFound ) // Time available?
5705 {
5706 displaytime ( timetxt ) ; // Write to TFT screen
5707 displayvolume() ; // Show volume on display
5708 displaybattery() ; // Show battery charge on display
5709 }
5710 }
5711 releaseSPI() ; // Release SPI bus
5712 if ( mqtt_on )
5713 {
5714 if ( !mqttclient.connected() ) // See if connected
5715 {
5716 mqttreconnect() ; // No, reconnect
5717 }
5718 else
5719 {
5720 mqttpub.publishtopic() ; // Check if any publishing to do
5721 }
5722 }
5723}
5724
5725
5726//**************************************************************************************************
5727// S P F T A S K *
5728//**************************************************************************************************
5729// Handles display of text, time and volume on TFT. *
5730// Handles ADC meassurements. *
5731// This task runs on a low priority. *
5732//**************************************************************************************************
5733void spftask ( void * parameter )
5734{
5735 while ( true )
5736 {
5737 handle_spec() ; // Maybe some special funcs?
5738 vTaskDelay ( 100 / portTICK_PERIOD_MS ) ; // Pause for a short time
5739 adcval = ( 15 * adcval + // Read ADC and do some filtering
5740 adc1_get_raw ( ADC1_CHANNEL_0 ) ) / 16 ;
5741 }
5742 //vTaskDelete ( NULL ) ; // Will never arrive here
5743}