From 507414da7e9cd3a1ea9e50818b4c48a4488badf2 Mon Sep 17 00:00:00 2001 From: e2002 Date: Sun, 27 Jul 2025 18:00:01 +0300 Subject: [PATCH] v0.9.550 --- yoRadio/data/www/options.html.gz | Bin 2251 -> 2559 bytes yoRadio/data/www/player.html.gz | Bin 2966 -> 2961 bytes yoRadio/data/www/script.js.gz | Bin 7106 -> 7446 bytes yoRadio/data/www/style.css.gz | Bin 4382 -> 4455 bytes yoRadio/data/www/theme.css | 1 + yoRadio/src/AsyncWebServer/AsyncTCP.h | 11 +- yoRadio/src/audioI2S/Audio.cpp | 37 ++- yoRadio/src/audioI2S/AudioEx.h | 23 +- yoRadio/src/audioVS1053/audioVS1053Ex.cpp | 40 ++- yoRadio/src/audioVS1053/audioVS1053Ex.h | 41 ++- yoRadio/src/core/audiohandlers.h | 14 +- yoRadio/src/core/commandhandler.cpp | 31 +- yoRadio/src/core/config.cpp | 120 +++++-- yoRadio/src/core/config.h | 30 +- yoRadio/src/core/controls.cpp | 5 +- yoRadio/src/core/display.cpp | 100 +++--- yoRadio/src/core/display.h | 10 +- yoRadio/src/core/mqtt.cpp | 142 ++++---- yoRadio/src/core/mqtt.h | 7 +- yoRadio/src/core/netserver.cpp | 266 +++++++-------- yoRadio/src/core/netserver.h | 11 +- yoRadio/src/core/network.cpp | 307 +---------------- yoRadio/src/core/network.h | 6 +- yoRadio/src/core/options.h | 25 +- yoRadio/src/core/player.cpp | 109 +++--- yoRadio/src/core/player.h | 15 +- yoRadio/src/core/sdmanager.cpp | 9 +- yoRadio/src/core/telnet.cpp | 157 +++++---- yoRadio/src/core/telnet.h | 5 +- yoRadio/src/core/timekeeper.cpp | 386 ++++++++++++++++++++++ yoRadio/src/core/timekeeper.h | 42 +++ yoRadio/src/displays/widgets/pages.cpp | 44 ++- yoRadio/src/displays/widgets/pages.h | 14 +- yoRadio/src/displays/widgets/widgets.h | 8 + yoRadio/src/main.cpp | 60 +++- 35 files changed, 1257 insertions(+), 819 deletions(-) create mode 100644 yoRadio/src/core/timekeeper.cpp create mode 100644 yoRadio/src/core/timekeeper.h diff --git a/yoRadio/data/www/options.html.gz b/yoRadio/data/www/options.html.gz index fabd443d74eeeb3d9a38f594357c4677014203b9..524945730c3c7109a422cbfed8c53528d1f34007 100644 GIT binary patch literal 2559 zcmV|vViwFP!000001MM8!ZrnEZyzstQ;#78c59KtL^Qr44&3g(nINAotoT_OO=mZ-KwV zz#S?B5ATyVMs|jY4qV9qKxPX<*dSy!XG(BScEL~kSj+Tj5DG!yx#F)ktw8ck@$RB!$OxWyxeYL;?q8MJ~4L z86-?u%PWi+_F+w>ntCNfLXRK%u7jV)nS4F z?TK(o6e9yL?a&Qm?UCI?@Tf#wxr^-*`(ed*F-KCSIBo9xnCVYG7D~eWXk>+f4~(x; z^!K_Cy4Rg*^mL)abX`SseCq89w5+$nwGJ}!Y2cSYM z^jE-WJo5lZ)aD*O0ySuhy%74(Eac;zN7zQW*VY2wC`-RyTW9bvtuWU;KN-KJ30j5g zb#8rnOTs7)N(M{%m7tRqK{gg-9eP8hfi;|(YQ`TEVO%r{<028p{Cub|)+^+C$m2Hl zP(XJbBB_uSPkq5I7)^+%auFj@B9R6;ZZ~L4N%SKJa%X6U7U%F^7NkhcoPa)<~TKtp{Y;x%6CM|9iHQ!DYV{YE-~?nAC>%=u6iB`Imd+gnHPAD$|Fy!Vnuu%&T2h-N zFc>(GvWF}$SVL4Z(WdSLn!gKnmj|K&05~Z{BVhpZL;6G`8A0%>I+g<{fSLX3%&(&u zm$kg1!jHfxAJ(KsKoME3I2Mkz{89h3h!y1#-82d53Xz{l7Y0Or<;;Y8#A!)V@|ZBo zO?m}|N&Ye_D2$nTqM$HMWgx~V^0@YBP@7@+mPxiHaJqp$s3BV$OaNuuDt`Dxgv#XJ zM#8P>Hj0Rqpc_{)B4tq%iex!5;5GLgqv9Q6`4S3-c%7PlE3%Yxf}9R%Y2Vl!!lBwE z>u(@lkIIf@;1?4|0s7BL*CQ}iko`e6M!4i0e2Y)64Kx`_95{RV=s0hg^>FOqjW#3( zuu8*j>ESJD1X-DeytNX7&u6pDckAb)_@LrEF#bUClT3lBv0$w%!zUmeJ4*a^wqy(P zFF~wGKW*!f{l&$jL|=2%u?>+xo*3k};KO2O0*hl#b>vcyAO>Nd zHYSv9S|+T?6x*vPsg2Uawl>>aBP+u23V3ZdRZR|{XUaOjxOkX7ppugx#p$YpQ-kA$ z`P3y$4lC>gLaD*k2V2w&PdR%D=;nOqtde@ay|`HPC`CNCcOAV@j<86Au2qu2e_e|u z=vpla$A79tGp1w*Y<8`hgw0-yXP$Yncf;3CLz0zmp7c($YI`zyI+a7~lTZDWZP$#+ zs4`qf&Z$Ny$Ci7cPrxaYpe-J0l9Z={RiL z#3RCNRSuK_fWTEL=x?^2$zU;?H7p<3R`V88e zQPQa*b5@%JxPB(9aMOZpxR7Xz(mZlm-QEF(TBDdv(7(WBxq_^(Emx^Pewa>eXn5Fj z&)HCQ3V+;ibxQg4_D&F=EAr|7^Si~{w^N7j_TV(9uJXMp7>i^B^Y_H7INX-n^!OnWU0edt zmW`&0t31E3OsoRTUc9IYY7-Ar0g^;OXpH_kAGG@>Vd%VFNJ(>}COpTy7a3a&W^B>T z*kU|mN1NmF`6MosJvf}a-!uCZQN5oA?2Jmdv^4_&RV$x;&c$RLa())ze3q?@Cj&8m18JE+n)-9oC4)uTz4s~;Z!@$EfaUseZ}+!*6Q#Txxdy>eZ}+!&j|lO=(dqC3B9CdSns? z9>*}pvDow{%WSy>>e@P0JN0*TYRj#aq%2jk8j7F?@A~A2IU(mEyMXJfC7;Z1|MSI7 zuo705_jpUt_t+0E(VaP8JSMA4Z#eZ&>?+r$3b|bht5GuQIK+}FtdLt~mhf_Zvi{|Iu2V`SK z)N|Xcjg{+ralnE;-G8>0qB(e}!EXnyFFfHK_>Fl*_n&WXZtksR-1->!Nd%J7E{zhz zcdj-4@H}{F721=7cz>Bp^jS6{w5gj+lD0C9j^6A(6nQ2&O+uh!Hl~Z$#LbtbQ@)39 z5XC3OgFWOQJTkW%R(ov%DsH3VZj#d@Ag(D{%w~(}o|{`-Q)~I3KkoFCwi>amHmA0T Vkmi5LP+!{L{TF~19sp26004Hy;4lCH literal 2251 zcmV;+2sHN}iwFP!000001MOSuj@!5u{-38{RbV$;)MJk`lT9--2GZROkRM$H<8Aw+ zC>E4NTZAQ&peTEsx9Fqwje1B?_lqU1Gf4*tkg+6^=kT2KoeN0`A%t#yx<}N%b3(r5 z4)Q`A$8Zxns~1Q&&!OF32Ft8j;4M*9g~UOzI)+3)@qNm+Z_(n$S*?jua1-CogQXp- zx!A!B@97pR${E^9o{04CZ}Bl!Btil8eZm|wrJ|>uO_Puw(nH0^3eOalFx@+++{0O= zza{xj6ZdH>LVUD$j6514dU)9Zz|50`uu;rB&XnX~+($p_g^n7qhMT2JOevB`x7oy#e7bss)5v%SGTdiEcXj@Q&C!d zm?SWFtDMS=ks+9M=qIuX$>U7&LuGQ+E?z*qoy#9niQ+ip>G3GSOh1aSR592`Gb;_; zG45vU*SZf{>rRdN^s&QqQ)h=0pK{7JyqnuQ-394B1DRFOw@)fI+$Z%J?30KnjqNV} z|M)xlo9@w6{qM8|Wg<}(AQ!*{z*M?fSjBv!5Ee)hmve=DqC@D*h_W9n$W9V6x=xKB zYHC=d+Fx`KFoTdf3XMU`w!{vGq0*`{4)LBeDAsXhS{wv(y>k}uiy!ZtC0y)rm=OJ_ zrl@J!Hj|5d7HcOuyZWP_e_5M=GC)}hQd%*cyc$R!r!Jn&GgpF9OGwyO1)!WO2hC3? zrKSZJ1tAK#hr_!1Q^2G%v1JDEl3U7Vii@{sc_kiiP(bK5P;b$zAK?zr26i212hjMe z5&;td^B_YaU}i!Dx2nf%AY`kAn?uUWS>00^9k6UpeHftwab1q3@2vi+e>xO^iYzj> zX{3~GOo=ig+f=I|9umK!Fe>s;UJngQM7Cy{l!#JcK2svf$|s0xWf?ai6%7l+vs|+s z!TKKhpq2`8^aLo|*ZIREB2}*Ut|@#l-6l=KmD>LpiQ_U7%G2^=!C51=8dkhVEx#s` zA>N$LFpsR%xuB<0C>>f4uq0|2Sw92yhBWReKz>Ogmq=j$(RMw9U?tt36k|f_D9BIA z)WzsY+1hPM)isheGZd}0lzq7=`T zU~Qq;a}RbuhT7QL9&e4V2*WPO+8wIe5gZZE}OSx3CKj`;feS}$ErRp(EgT_-!r8JtA)d5NX} zyqrb!d5e|!t8$v2ZGs-B5q(}`#T?{3mw6U@Gv0Sv7ForH?cilrV;{I!P8HOKtfzj; zw}_{#sB&IsUQF{v)azG6>%{Wa^cl6Ws$##+%y+jZc>PS=WZS`Pypjm>xL=$ioE4-QZR~j@ zB+{H>O#v&=DU+ClSxuv^>y&O2v2FaS75$H;ihg;f)zmcIcb^gLs7M;ibk2wuQg0U@ zsPY0x0o^!*t~1jYqD*>JnGYJ0|Hea+ejXHlcmQY7wqzbb|AI6WNV2(e0;NR!c0Tu@ zVRz)AzoqH|{s)PBLHYc{gCr4G?SQB1$=Bn29Q zWPklRyIf}F!swHgDQ88LC5nufGU}5Oh;|$n)e_6qwPT7p5cc{^3)S;NfGL4uB4G`I z{&+d+I4y>um)%OT&8Wb;DU~77^q+KgPe<)_!nzl@DGI`$6*6&5{c%Wv0gE1_KywFBU=&Ui2djyY zC}`%J%@a6f4g)Zjy5Ft9YjLqy zEH0lQFov9MrwPK@IR*IwCgg%Ji>L?-gat)Aa%?&5sCK;Iay!RD=oD8!&JR5_-`lP9+%r$~#|H7#hNws?yy1zBb6C zXPP|F`}Id>B|8#Hm3lYn4F|*(r0>lwT7Ud-f4_ECY3p?AHWTFYhC7sfRHo++*9|+L zG<%7806P7Sxv{vZenMCO=}@jv3+Urbax007mnY#aap diff --git a/yoRadio/data/www/player.html.gz b/yoRadio/data/www/player.html.gz index 66c47f5d2e214935002d6a162f5dc3844a50bf99..b4f92f1665435809698d75ddbcfc2e22e7ddc416 100644 GIT binary patch delta 2899 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�mxbB^^K0r*&m8OQtp4dE9vo_Np|T<4r) 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 UjugvjR2-rI1#oJuwN`Eb06b7m(f|Me literal 7106 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 diff --git a/yoRadio/data/www/style.css.gz b/yoRadio/data/www/style.css.gz index 210bbf3826f2f52f605e145d7c893b9fc4952a30..b4cb83448a7a09eafe471a9b3503995351260366 100644 GIT binary patch literal 4455 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 Yi5uF;INceIB(#J71M-{;tOiN|02C693jhEB diff --git a/yoRadio/data/www/theme.css b/yoRadio/data/www/theme.css index 185a6c4..6ce043e 100644 --- a/yoRadio/data/www/theme.css +++ b/yoRadio/data/www/theme.css @@ -13,4 +13,5 @@ --playlist-hover: #323232; --section-gradient: #111111; --section-border: #555555; +--heapbar-color: #3ea220; } diff --git a/yoRadio/src/AsyncWebServer/AsyncTCP.h b/yoRadio/src/AsyncWebServer/AsyncTCP.h index c898bf0..ed381a6 100644 --- a/yoRadio/src/AsyncWebServer/AsyncTCP.h +++ b/yoRadio/src/AsyncWebServer/AsyncTCP.h @@ -40,20 +40,23 @@ extern "C" { #endif #ifndef XTASK_MEM_SIZE - #define XTASK_MEM_SIZE 6144 // 8192 / 2 + //#define XTASK_MEM_SIZE 6144 // 8192 / 2 + #define XTASK_MEM_SIZE 1024*5 #endif #ifndef XTASK_PRIOTITY - #define XTASK_PRIOTITY 5 //3 + #define XTASK_PRIOTITY 3 //3 #endif #ifndef ATCP_TASK_DELAY #define ATCP_TASK_DELAY 2 #endif #ifndef XQUEUE_SIZE - #define XQUEUE_SIZE 128 // (32) + //#define XQUEUE_SIZE 128 // (32) + #define XQUEUE_SIZE 32 #endif #ifndef SEND_ASYNC_EVENT_DELAY - #define SEND_ASYNC_EVENT_DELAY portMAX_DELAY + //#define SEND_ASYNC_EVENT_DELAY portMAX_DELAY + #define SEND_ASYNC_EVENT_DELAY pdMS_TO_TICKS(1000) #endif class AsyncClient; diff --git a/yoRadio/src/audioI2S/Audio.cpp b/yoRadio/src/audioI2S/Audio.cpp index e444487..d3799ff 100644 --- a/yoRadio/src/audioI2S/Audio.cpp +++ b/yoRadio/src/audioI2S/Audio.cpp @@ -61,9 +61,9 @@ size_t AudioBuffer::init() { if(m_buffer == NULL) { // PSRAM not found, not configured or not enough available m_f_psram = false; - m_buffSize = m_buffSizeRAM; + m_buffSize = m_buffSizeRAM * config.store.abuff; m_buffer = (uint8_t*) calloc(m_buffSize, sizeof(uint8_t)); - m_buffSize = m_buffSizeRAM - m_resBuffSizeRAM; + m_buffSize = m_buffSizeRAM * config.store.abuff - m_resBuffSizeRAM; } if(!m_buffer) return 0; @@ -183,7 +183,7 @@ Audio::Audio(bool internalDAC /* = false */, uint8_t channelEnabled /* = I2S_DAC m_i2s_config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT; m_i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; // interrupt priority #ifdef OLD_DMABUF_PARAMS - m_i2s_config.dma_buf_count = 16; // 4×512×16=32768 + m_i2s_config.dma_buf_count = 16; // 4×512×16=32768 #else m_i2s_config.dma_buf_count = psramInit()?16:DMA_BUFCOUNT; #endif @@ -370,6 +370,20 @@ void Audio::setConnectionTimeout(uint16_t timeout_ms, uint16_t timeout_ms_ssl){ if(timeout_ms_ssl) m_timeout_ms_ssl = timeout_ms_ssl; } +void Audio::connectTask(void* pvParams) { + ConnectParams* params = static_cast(pvParams); + Audio* self = params->instance; + if(self->_client){ + self->_connectionResult = self->_client->connect(params->hostwoext, params->port/*, self->m_f_ssl ? self->m_timeout_ms_ssl : self->m_timeout_ms*/); + }else{ + self->_connectionResult = false; + } + free((void*)params->hostwoext); + delete params; + self->_connectTaskHandle = nullptr; + vTaskDelete(nullptr); +} + //--------------------------------------------------------------------------------------------------------------------- bool Audio::connecttohost(const char* host, const char* user, const char* pwd) { // user and pwd for authentification only, can be empty @@ -480,7 +494,22 @@ bool Audio::connecttohost(const char* host, const char* user, const char* pwd) { uint32_t t = millis(); if(m_f_Log) AUDIO_INFO("connect to %s on port %d path %s", hostwoext, port, extension); - res = _client->connect(hostwoext, port, m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms); + if(!config.store.watchdog){ + res = _client->connect(hostwoext, port, m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms); + }else{ + ConnectParams* params = new ConnectParams{ strdup(hostwoext), port, this }; _connectionResult = false; + xTaskCreatePinnedToCore(connectTask, "ConnectTask", WATCHDOG_TASK_SIZE, params, WATCHDOG_TASK_PRIORITY, &_connectTaskHandle, WATCHDOG_TASK_CORE_ID); + for(;;){ + if(millis()-t>(m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms) || _connectionResult) break; + vTaskDelay(10); + } + res = _connectionResult; + if (_connectTaskHandle!=nullptr) { + vTaskDelete(_connectTaskHandle); + _connectTaskHandle = nullptr; + AUDIO_INFO("WATCH DOG HAS FINISHED A WORK, BYE!"); + } + } if(res){ uint32_t dt = millis() - t; strcpy(m_lastHost, l_host); diff --git a/yoRadio/src/audioI2S/AudioEx.h b/yoRadio/src/audioI2S/AudioEx.h index c25184c..0051d9f 100644 --- a/yoRadio/src/audioI2S/AudioEx.h +++ b/yoRadio/src/audioI2S/AudioEx.h @@ -32,10 +32,6 @@ #include #endif // SDFATFS_USED -#ifndef AUDIOBUFFER_MULTIPLIER2 -#define AUDIOBUFFER_MULTIPLIER2 8 -#endif - #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) #include "hal/gpio_ll.h" #endif @@ -149,7 +145,7 @@ public: protected: size_t m_buffSizePSRAM = 300000; // most webstreams limit the advance to 100...300Kbytes //size_t m_buffSizeRAM = 1600 * 5; - size_t m_buffSizeRAM = 1600 * AUDIOBUFFER_MULTIPLIER2; + size_t m_buffSizeRAM = 1600; size_t m_buffSize = 0; size_t m_freeSpace = 0; size_t m_writeSpace = 0; @@ -287,6 +283,7 @@ private: void IIR_calculateCoefficients(int8_t G1, int8_t G2, int8_t G3); bool ts_parsePacket(uint8_t* packet, uint8_t* packetStart, uint8_t* packetLength); void _computeVUlevel(int16_t sample[2]); + static void connectTask(void* pvParams); // implement several function with respect to the index of string void trim(char *s) { //fb trim in place @@ -425,11 +422,11 @@ private: if(str == NULL) return 0; uint32_t hash = 0; for(int i=0; i m_playlistContent; // m3u8 playlist buffer std::vector m_playlistURL; // m3u8 streamURLs buffer std::vector m_hashQueue; - + + struct ConnectParams { + char *hostwoext = NULL; + uint16_t port = 80; + Audio* instance; + }; + volatile bool _connectionResult; + TaskHandle_t _connectTaskHandle = nullptr; + const size_t m_frameSizeWav = 1600; const size_t m_frameSizeMP3 = 1600; const size_t m_frameSizeAAC = 1600; diff --git a/yoRadio/src/audioVS1053/audioVS1053Ex.cpp b/yoRadio/src/audioVS1053/audioVS1053Ex.cpp index 199dd7e..e6dea96 100644 --- a/yoRadio/src/audioVS1053/audioVS1053Ex.cpp +++ b/yoRadio/src/audioVS1053/audioVS1053Ex.cpp @@ -42,9 +42,9 @@ size_t AudioBuffer::init() { } } } else { // no PSRAM available, use ESP32 Flash Memory" - m_buffSize = m_buffSizeRAM; + m_buffSize = m_buffSizeRAM * config.store.abuff; m_buffer = (uint8_t*) calloc(m_buffSize, sizeof(uint8_t)); - m_buffSize = m_buffSizeRAM - m_resBuffSizeRAM; + m_buffSize = m_buffSizeRAM * config.store.abuff - m_resBuffSizeRAM; } if(!m_buffer) return 0; @@ -1713,6 +1713,22 @@ void Audio::setConnectionTimeout(uint16_t timeout_ms, uint16_t timeout_ms_ssl){ if(timeout_ms) m_timeout_ms = timeout_ms; if(timeout_ms_ssl) m_timeout_ms_ssl = timeout_ms_ssl; } + +void Audio::connectTask(void* pvParams) { + ConnectParams* params = static_cast(pvParams); + Audio* self = params->instance; + bool res = true; + if(self->_client){ + self->_connectionResult = self->_client->connect(params->hostwoext, params->port/*, self->m_f_ssl ? self->m_timeout_ms_ssl : self->m_timeout_ms*/); + }else{ + self->_connectionResult = false; + } + free((void*)params->hostwoext); + delete params; + self->_connectTaskHandle = nullptr; + vTaskDelete(nullptr); +} + //--------------------------------------------------------------------------------------------------------------------- bool Audio::connecttohost(String host){ return connecttohost(host.c_str()); @@ -1827,8 +1843,24 @@ bool Audio::connecttohost(const char* host, const char* user, const char* pwd) { uint32_t t = millis(); if(m_f_Log) AUDIO_INFO("connect to %s on port %d path %s", hostwoext, port, extension); - res = _client->connect(hostwoext, port, m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms); - + //res = _client->connect(hostwoext, port, m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms); + if(!config.store.watchdog){ + res = _client->connect(hostwoext, port, m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms); + }else{ + ConnectParams* params = new ConnectParams{ strdup(hostwoext), port, this }; _connectionResult = false; + xTaskCreatePinnedToCore(connectTask, "ConnectTask", WATCHDOG_TASK_SIZE, params, WATCHDOG_TASK_PRIORITY, &_connectTaskHandle, WATCHDOG_TASK_CORE_ID); + for(;;){ + if(millis()-t>(m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms) || _connectionResult) break; + vTaskDelay(10); + } + res = _connectionResult; + if (_connectTaskHandle!=nullptr) { + vTaskDelete(_connectTaskHandle); + _connectTaskHandle = nullptr; + AUDIO_INFO("WATCH DOG HAS FINISHED A WORK, BYE!"); + } + } + if(res){ uint32_t dt = millis() - t; strcpy(m_lastHost, l_host); diff --git a/yoRadio/src/audioVS1053/audioVS1053Ex.h b/yoRadio/src/audioVS1053/audioVS1053Ex.h index 9640c3e..441ad4c 100644 --- a/yoRadio/src/audioVS1053/audioVS1053Ex.h +++ b/yoRadio/src/audioVS1053/audioVS1053Ex.h @@ -9,11 +9,7 @@ #ifndef _vs1053_ext #define _vs1053_ext -#ifndef AUDIOBUFFER_MULTIPLIER2 -#define AUDIOBUFFER_MULTIPLIER2 10 -#endif - -#define VS1053VOLM 128 // 128 or 96 only +#define VS1053VOLM 128 // 128 or 96 only #define VS1053VOL(v) (VS1053VOLM==128?log10(((float)v+1)) * 50.54571334 + 128:log10(((float)v+1)) * 64.54571334 + 96) @@ -107,7 +103,7 @@ public: protected: const size_t m_buffSizePSRAM = 300000; // most webstreams limit the advance to 100...300Kbytes //const size_t m_buffSizeRAM = 1600 * 10; - const size_t m_buffSizeRAM = 1600 * AUDIOBUFFER_MULTIPLIER2; + const size_t m_buffSizeRAM = 1600; size_t m_buffSize = 0; size_t m_freeSpace = 0; size_t m_writeSpace = 0; @@ -136,6 +132,15 @@ private: std::vector m_playlistURL; // m3u8 streamURLs buffer std::vector m_hashQueue; + struct ConnectParams { + char *hostwoext = NULL; + uint16_t port = 80; + Audio* instance; + }; + volatile bool _connectionResult; + TaskHandle_t _connectTaskHandle = nullptr; + static void connectTask(void* pvParams); + private: enum : int { AUDIO_NONE, HTTP_RESPONSE_HEADER , AUDIO_DATA, AUDIO_LOCALFILE, AUDIO_METADATA, AUDIO_PLAYLISTINIT, AUDIO_PLAYLISTHEADER, AUDIO_PLAYLISTDATA, VS1053_SWM, VS1053_OGG}; @@ -146,10 +151,10 @@ private: enum : int { ST_NONE = 0, ST_WEBFILE = 1, ST_WEBSTREAM = 2}; private: - uint8_t cs_pin ; // Pin where CS line is connected - uint8_t dcs_pin ; // Pin where DCS line is connected - uint8_t dreq_pin ; // Pin where DREQ line is connected - uint8_t curvol ; // Current volume setting 0..100% + uint8_t cs_pin ; // Pin where CS line is connected + uint8_t dcs_pin ; // Pin where DCS line is connected + uint8_t dreq_pin ; // Pin where DREQ line is connected + uint8_t curvol ; // Current volume setting 0..100% const uint8_t vs1053_chunk_size = 32 ; int8_t m_balance = 0; // -16 (mute left) ... +16 (mute right) @@ -171,11 +176,11 @@ private: const uint8_t SCI_AICTRL2 = 0xE ; const uint8_t SCI_AICTRL3 = 0xF ; // SCI_MODE bits - const uint8_t SM_SDINEW = 11 ; // Bitnumber in SCI_MODE always on - const uint8_t SM_RESET = 2 ; // Bitnumber in SCI_MODE soft reset - const uint8_t SM_CANCEL = 3 ; // Bitnumber in SCI_MODE cancel song - const uint8_t SM_TESTS = 5 ; // Bitnumber in SCI_MODE for tests - const uint8_t SM_LINE1 = 14 ; // Bitnumber in SCI_MODE for Line input + const uint8_t SM_SDINEW = 11 ; // Bitnumber in SCI_MODE always on + const uint8_t SM_RESET = 2 ; // Bitnumber in SCI_MODE soft reset + const uint8_t SM_CANCEL = 3 ; // Bitnumber in SCI_MODE cancel song + const uint8_t SM_TESTS = 5 ; // Bitnumber in SCI_MODE for tests + const uint8_t SM_LINE1 = 14 ; // Bitnumber in SCI_MODE for Line input SPIClass* spi_VS1053 = NULL; SPISettings VS1053_SPI_DATA; // SPI settings normal speed @@ -242,7 +247,7 @@ protected: inline void DCS_LOW() {(dcs_pin&0x20) ? GPIO.out1_w1tc.data = 1 << (dcs_pin - 32) : GPIO.out_w1tc = 1 << dcs_pin;} inline void CS_HIGH() {( cs_pin&0x20) ? GPIO.out1_w1ts.data = 1 << ( cs_pin - 32) : GPIO.out_w1ts = 1 << cs_pin;} inline void CS_LOW() {( cs_pin&0x20) ? GPIO.out1_w1tc.data = 1 << ( cs_pin - 32) : GPIO.out_w1tc = 1 << cs_pin;} - inline void await_data_request() {while(!digitalRead(dreq_pin)) NOP();} // Very short delay + inline void await_data_request() {while(!digitalRead(dreq_pin)) NOP();} // Very short delay inline bool data_request() {return(digitalRead(dreq_pin) == HIGH);} void initInBuff(); @@ -319,8 +324,8 @@ public: size_t bufferFree(); size_t inBufferFilled(){ return bufferFilled(); } size_t inBufferFree(){ return bufferFree(); } - void setBalance(int8_t bal = 0); - void setTone(int8_t gainLowPass, int8_t gainBandPass, int8_t gainHighPass); + void setBalance(int8_t bal = 0); + void setTone(int8_t gainLowPass, int8_t gainBandPass, int8_t gainHighPass); void setDefaults(); void forceMono(bool m) {} // TODO /* VU METER */ diff --git a/yoRadio/src/core/audiohandlers.h b/yoRadio/src/core/audiohandlers.h index eb9231f..737648d 100644 --- a/yoRadio/src/core/audiohandlers.h +++ b/yoRadio/src/core/audiohandlers.h @@ -61,7 +61,7 @@ void audio_showstation(const char *info) { void audio_showstreamtitle(const char *info) { DBGH(); - if (strstr(info, "Account already in use") != NULL || strstr(info, "HTTP/1.0 401") != NULL) player.setError(info); + if (strstr(info, "Account already in use") != NULL || strstr(info, "HTTP/1.0 401") != NULL || strstr(info, "HTTP/1.1 401") != NULL) player.setError(info); bool p = printable(info) && (strlen(info) > 0); #ifdef DEBUG_TITLES config.setTitle(DEBUG_TITLES); @@ -73,7 +73,7 @@ void audio_showstreamtitle(const char *info) { void audio_error(const char *info) { //config.setTitle(info); player.setError(info); - telnet.printf("##ERROR#:\t%s\n", info); + //telnet.printf("##ERROR#:\t%s\n", info); } void audio_id3artist(const char *info){ @@ -88,11 +88,11 @@ void audio_id3album(const char *info){ if(strlen(config.station.title)==0){ config.setTitle(info); }else{ - char out[BUFLEN]= {0}; - strlcat(out, config.station.title, BUFLEN); - strlcat(out, " - ", BUFLEN); - strlcat(out, info, BUFLEN); - config.setTitle(out); + size_t tbs = sizeof(config.tmpBuf); + strlcat(config.tmpBuf, config.station.title, tbs); + strlcat(config.tmpBuf, " - ", tbs); + strlcat(config.tmpBuf, info, tbs); + config.setTitle(config.tmpBuf); } } } diff --git a/yoRadio/src/core/commandhandler.cpp b/yoRadio/src/core/commandhandler.cpp index 57c7969..deea5b4 100644 --- a/yoRadio/src/core/commandhandler.cpp +++ b/yoRadio/src/core/commandhandler.cpp @@ -5,6 +5,7 @@ #include "config.h" #include "controls.h" #include "options.h" +#include "telnet.h" CommandHandler cmd; @@ -65,11 +66,16 @@ bool CommandHandler::exec(const char *command, const char *value, uint8_t cid) { if (strEquals(command, "screensaverplayingenabled")){ config.setScreensaverPlayingEnabled(static_cast(atoi(value))); return true; } if (strEquals(command, "screensaverplayingtimeout")){ config.setScreensaverPlayingTimeout(static_cast(atoi(value))); return true; } if (strEquals(command, "screensaverplayingblank")) { config.setScreensaverPlayingBlank(static_cast(atoi(value))); return true; } + if (strEquals(command, "abuff")){ config.saveValue(&config.store.abuff, static_cast(atoi(value))); return true; } + if (strEquals(command, "telnet")){ config.saveValue(&config.store.telnet, static_cast(atoi(value))); telnet.toggle(); return true; } + if (strEquals(command, "watchdog")){ config.saveValue(&config.store.watchdog, static_cast(atoi(value))); return true; } - if (strEquals(command, "tzh")) { config.saveValue(&config.store.tzHour, static_cast(atoi(value))); return true; } - if (strEquals(command, "tzm")) { config.saveValue(&config.store.tzMin, static_cast(atoi(value))); return true; } - if (strEquals(command, "sntp2")) { config.saveValue(config.store.sntp2, value, 35, false); return true; } - if (strEquals(command, "sntp1")) { config.setSntpOne(value); return true; } + if (strEquals(command, "tzh")) { config.saveValue(&config.store.tzHour, static_cast(atoi(value))); return true; } + if (strEquals(command, "tzm")) { config.saveValue(&config.store.tzMin, static_cast(atoi(value))); return true; } + if (strEquals(command, "sntp2")) { config.saveValue(config.store.sntp2, value, 35, false); return true; } + if (strEquals(command, "sntp1")) { config.setSntpOne(value); return true; } + if (strEquals(command, "timeint")) { config.saveValue(&config.store.timeSyncInterval, static_cast(atoi(value))); return true; } + if (strEquals(command, "timeintrtc")) { config.saveValue(&config.store.timeSyncIntervalRTC, static_cast(atoi(value))); return true; } if (strEquals(command, "volsteps")) { config.saveValue(&config.store.volsteps, static_cast(atoi(value))); return true; } if (strEquals(command, "encacc")) { setEncAcceleration(static_cast(atoi(value))); return true; } @@ -79,32 +85,33 @@ bool CommandHandler::exec(const char *command, const char *value, uint8_t cid) { if (strEquals(command, "lat")) { config.saveValue(config.store.weatherlat, value, 10, false); return true; } if (strEquals(command, "lon")) { config.saveValue(config.store.weatherlon, value, 10, false); return true; } if (strEquals(command, "key")) { config.setWeatherKey(value); return true; } - //<-----TODO + if (strEquals(command, "wint")) { config.saveValue(&config.store.weatherSyncInterval, static_cast(atoi(value))); return true; } + if (strEquals(command, "volume")) { player.setVol(static_cast(atoi(value))); return true; } if (strEquals(command, "sdpos")) { config.setSDpos(static_cast(atoi(value))); return true; } if (strEquals(command, "snuffle")) { config.setSnuffle(strcmp(value, "true") == 0); return true; } if (strEquals(command, "balance")) { config.setBalance(static_cast(atoi(value))); return true; } if (strEquals(command, "reboot")) { ESP.restart(); return true; } + if (strEquals(command, "boot")) { ESP.restart(); return true; } if (strEquals(command, "format")) { SPIFFS.format(); ESP.restart(); return true; } - if (strEquals(command, "submitplaylist")) { return true; } - + if (strEquals(command, "submitplaylist")) { player.sendCommand({PR_STOP, 0}); return true; } + #if IR_PIN!=255 if (strEquals(command, "irbtn")) { config.setIrBtn(atoi(value)); return true; } if (strEquals(command, "chkid")) { config.irchck = static_cast(atoi(value)); return true; } if (strEquals(command, "irclr")) { config.ircodes.irVals[config.irindex][static_cast(atoi(value))] = 0; return true; } #endif if (strEquals(command, "reset")) { config.resetSystem(value, cid); return true; } - + if (strEquals(command, "smartstart")){ uint8_t ss = atoi(value) == 1 ? 1 : 2; if (!player.isRunning() && ss == 1) ss = 0; config.setSmartStart(ss); return true; } if (strEquals(command, "audioinfo")) { config.saveValue(&config.store.audioinfo, static_cast(atoi(value))); display.putRequest(AUDIOINFO); return true; } if (strEquals(command, "vumeter")) { config.saveValue(&config.store.vumeter, static_cast(atoi(value))); display.putRequest(SHOWVUMETER); return true; } if (strEquals(command, "softap")) { config.saveValue(&config.store.softapdelay, static_cast(atoi(value))); return true; } if (strEquals(command, "mdnsname")) { config.saveValue(config.store.mdnsname, value, MDNS_LENGTH); return true; } if (strEquals(command, "rebootmdns")){ - char buf[MDNS_LENGTH*2]; - if(strlen(config.store.mdnsname)>0) snprintf(buf, MDNS_LENGTH*2, "{\"redirect\": \"http://%s.local\"}", config.store.mdnsname); - else snprintf(buf, MDNS_LENGTH*2, "{\"redirect\": \"http://%s/\"}", WiFi.localIP().toString().c_str()); - websocket.text(cid, buf); delay(500); ESP.restart(); + if(strlen(config.store.mdnsname)>0) snprintf(config.tmpBuf, sizeof(config.tmpBuf), "{\"redirect\": \"http://%s.local/settings.html\"}", config.store.mdnsname); + else snprintf(config.tmpBuf, sizeof(config.tmpBuf), "{\"redirect\": \"http://%s/settings.html\"}", config.ipToStr(WiFi.localIP())); + websocket.text(cid, config.tmpBuf); delay(500); ESP.restart(); return true; } diff --git a/yoRadio/src/core/config.cpp b/yoRadio/src/core/config.cpp index d3c520b..21bba5f 100644 --- a/yoRadio/src/core/config.cpp +++ b/yoRadio/src/core/config.cpp @@ -6,6 +6,7 @@ #include "network.h" #include "netserver.h" #include "controls.h" +#include "timekeeper.h" #ifdef USE_SD #include "sdmanager.h" #endif @@ -13,6 +14,19 @@ Config config; +#ifdef HEAP_DBG +void printHeapFragmentationInfo(const char* title){ + size_t freeHeap = heap_caps_get_free_size(MALLOC_CAP_DEFAULT); + size_t largestBlock = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT); + float fragmentation = 100.0 * (1.0 - ((float)largestBlock / (float)freeHeap)); + Serial.printf("\n****** %s ******\n", title); + Serial.printf("* Free heap: %u bytes\n", freeHeap); + Serial.printf("* Largest free block: %u bytes\n", largestBlock); + Serial.printf("* Fragmentation: %.2f%%\n", fragmentation); + Serial.printf("*************************************\n\n"); +} +#endif + void u8fix(char *src){ char last = src[strlen(src)-1]; if ((uint8_t)last >= 0xC2) src[strlen(src)-1]='\0'; @@ -46,7 +60,8 @@ void Config::init() { screensaverPlayingTicks = 0; newConfigMode = 0; isScreensaver = false; - bootInfo(); + memset(tmpBuf, 0, BUFLEN); + //bootInfo(); #if RTCSUPPORTED _rtcFound = false; BOOTLOG("RTC begin(SDA=%d,SCL=%d)", RTC_SDA, RTC_SCL); @@ -69,7 +84,7 @@ void Config::init() { #endif #endif eepromRead(EEPROM_START, store); - + bootInfo(); // https://github.com/e2002/yoradio/pull/149 if (store.config_set != 4262) { setDefaults(); } @@ -93,6 +108,7 @@ void Config::init() { _SDplaylistFS = &SPIFFS; #endif _bootDone=false; + setTimeConf(); } void Config::_setupVersion(){ @@ -103,9 +119,8 @@ void Config::_setupVersion(){ saveValue(&store.screensaverTimeout, (uint16_t)20); break; case 2: - char buf[MDNS_LENGTH]; - snprintf(buf, MDNS_LENGTH, "yoradio-%x", getChipId()); - saveValue(store.mdnsname, buf, MDNS_LENGTH); + snprintf(tmpBuf, MDNS_LENGTH, "yoradio-%x", (unsigned int)getChipId()); + saveValue(store.mdnsname, tmpBuf, MDNS_LENGTH); saveValue(&store.skipPlaylistUpDown, false); break; case 3: @@ -114,6 +129,13 @@ void Config::_setupVersion(){ saveValue(&store.screensaverPlayingTimeout, (uint16_t)5); saveValue(&store.screensaverPlayingBlank, false); break; + case 4: + saveValue(&store.abuff, (uint16_t)(VS1053_CS==255?7:10)); + saveValue(&store.telnet, true); + saveValue(&store.watchdog, true); + saveValue(&store.timeSyncInterval, (uint16_t)60); //min + saveValue(&store.timeSyncIntervalRTC, (uint16_t)24); //hours + saveValue(&store.weatherSyncInterval, (uint16_t)30); // min default: break; } @@ -200,6 +222,44 @@ bool Config::spiffsCleanup(){ return ret; } +char * Config::ipToStr(IPAddress ip){ + snprintf(ipBuf, 16, "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]); + return ipBuf; +} +bool Config::prepareForPlaying(uint16_t stationId){ + setDspOn(1); + vuThreshold = 0; + screensaverTicks=SCREENSAVERSTARTUPDELAY; + screensaverPlayingTicks=SCREENSAVERSTARTUPDELAY; + if(getMode()!=PM_SDCARD) { + display.putRequest(PSTOP); + } + + if(!loadStation(stationId)) return false; + setTitle(getMode()==PM_WEB?const_PlConnect:"[next track]"); + station.bitrate=0; + setBitrateFormat(BF_UNCNOWN); + display.putRequest(DBITRATE); + display.putRequest(NEWSTATION); + display.putRequest(NEWMODE, PLAYER); + netserver.requestOnChange(STATION, 0); + netserver.requestOnChange(MODE, 0); + netserver.loop(); + netserver.loop(); + if(store.smartstart!=2) + setSmartStart(0); + return true; +} +void Config::configPostPlaying(uint16_t stationId){ + if(getMode()==PM_SDCARD) { + sdResumePos = 0; + saveValue(&store.lastSdStation, stationId); + } + if(store.smartstart!=2) setSmartStart(1); + netserver.requestOnChange(MODE, 0); + //display.putRequest(NEWMODE, PLAYER); + display.putRequest(PSTART); +} void Config::initPlaylistMode(){ uint16_t _lastStation = 0; uint16_t cs = playlistLength(); @@ -369,19 +429,17 @@ void Config::setSntpOne(const char *val){ tzdone = true; } if (tzdone) { - network.forceTimeSync = true; + timekeeper.forceTimeSync = true; saveValue(config.store.sntp1, val, 35); } } void Config::setShowweather(bool val){ config.saveValue(&config.store.showweather, val); - network.trueWeather=false; - network.forceWeather = true; + timekeeper.forceWeather = true; display.putRequest(SHOWWEATHER); } void Config::setWeatherKey(const char *val){ saveValue(store.weatherkey, val, WEATHERKEY_LENGTH); - network.trueWeather=false; display.putRequest(NEWMODE, CLEAR); display.putRequest(NEWMODE, PLAYER); } @@ -411,7 +469,10 @@ void Config::resetSystem(const char *val, uint8_t clientId){ saveValue(&store.audioinfo, false, false); saveValue(&store.vumeter, false, false); saveValue(&store.softapdelay, (uint8_t)0, false); - snprintf(store.mdnsname, MDNS_LENGTH, "yoradio-%x", getChipId()); + saveValue(&store.abuff, (uint16_t)(VS1053_CS==255?7:10), false); + saveValue(&store.telnet, true); + saveValue(&store.watchdog, true); + snprintf(store.mdnsname, MDNS_LENGTH, "yoradio-%x", (unsigned int)getChipId()); saveValue(store.mdnsname, store.mdnsname, MDNS_LENGTH, true, true); display.putRequest(NEWMODE, CLEAR); display.putRequest(NEWMODE, PLAYER); netserver.requestOnChange(GETSYSTEM, clientId); @@ -443,8 +504,10 @@ void Config::resetSystem(const char *val, uint8_t clientId){ saveValue(&store.tzMin, (int8_t)0, false); saveValue(store.sntp1, "pool.ntp.org", 35, false); saveValue(store.sntp2, "0.ru.pool.ntp.org", 35); + saveValue(&store.timeSyncInterval, (uint16_t)60); + saveValue(&store.timeSyncIntervalRTC, (uint16_t)24); configTime(store.tzHour * 3600 + store.tzMin * 60, getTimezoneOffset(), store.sntp1, store.sntp2); - network.forceTimeSync = true; + timekeeper.forceTimeSync = true; netserver.requestOnChange(GETTIMEZONE, clientId); return; } @@ -453,7 +516,8 @@ void Config::resetSystem(const char *val, uint8_t clientId){ saveValue(store.weatherlat, "55.7512", 10, false); saveValue(store.weatherlon, "37.6184", 10, false); saveValue(store.weatherkey, "", WEATHERKEY_LENGTH); - network.trueWeather=false; + saveValue(&store.weatherSyncInterval, (uint16_t)30); + //network.trueWeather=false; display.putRequest(NEWMODE, CLEAR); display.putRequest(NEWMODE, PLAYER); netserver.requestOnChange(GETWEATHER, clientId); return; @@ -530,11 +594,17 @@ void Config::setDefaults() { store.screensaverEnabled = false; store.screensaverTimeout = 20; store.screensaverBlank = false; - snprintf(store.mdnsname, MDNS_LENGTH, "yoradio-%x", getChipId()); + snprintf(store.mdnsname, MDNS_LENGTH, "yoradio-%x", (unsigned int)getChipId()); store.skipPlaylistUpDown = false; store.screensaverPlayingEnabled = false; store.screensaverPlayingTimeout = 5; store.screensaverPlayingBlank = false; + store.abuff = VS1053_CS==255?7:10; + store.telnet = true; + store.watchdog = true; + store.timeSyncInterval = 60; //min + store.timeSyncIntervalRTC = 24; //hour + store.weatherSyncInterval = 30; //min eepromWrite(EEPROM_START, store); } @@ -627,12 +697,11 @@ void Config::indexPlaylist() { if (!playlist) { return; } - char sName[BUFLEN], sUrl[BUFLEN]; int sOvol; File index = SPIFFS.open(INDEX_PATH, "w"); while (playlist.available()) { uint32_t pos = playlist.position(); - if (parseCSV(playlist.readStringUntil('\n').c_str(), sName, sUrl, sOvol)) { + if (parseCSV(playlist.readStringUntil('\n').c_str(), tmpBuf, tmpBuf2, sOvol)) { index.write((uint8_t *) &pos, 4); } } @@ -661,7 +730,6 @@ uint16_t Config::playlistLength(){ return out; } bool Config::loadStation(uint16_t ls) { - char sName[BUFLEN], sUrl[BUFLEN]; int sOvol; uint16_t cs = playlistLength(); if (cs == 0) { @@ -681,11 +749,11 @@ bool Config::loadStation(uint16_t ls) { index.readBytes((char *) &pos, 4); index.close(); playlist.seek(pos, SeekSet); - if (parseCSV(playlist.readStringUntil('\n').c_str(), sName, sUrl, sOvol)) { + if (parseCSV(playlist.readStringUntil('\n').c_str(), tmpBuf, tmpBuf2, sOvol)) { memset(station.url, 0, BUFLEN); memset(station.name, 0, BUFLEN); - strncpy(station.name, sName, BUFLEN); - strncpy(station.url, sUrl, BUFLEN); + strncpy(station.name, tmpBuf, BUFLEN); + strncpy(station.url, tmpBuf2, BUFLEN); station.ovol = sOvol; setLastStation(ls); } @@ -698,11 +766,11 @@ char * Config::stationByNum(uint16_t num){ File index = SDPLFS()->open(REAL_INDEX, "r"); index.seek((num - 1) * 4, SeekSet); uint32_t pos; - memset(_stationBuf, 0, BUFLEN/2); + memset(_stationBuf, 0, sizeof(_stationBuf)); index.readBytes((char *) &pos, 4); index.close(); playlist.seek(pos, SeekSet); - strncpy(_stationBuf, playlist.readStringUntil('\t').c_str(), BUFLEN/2); + strncpy(_stationBuf, playlist.readStringUntil('\t').c_str(), sizeof(_stationBuf)); playlist.close(); return _stationBuf; } @@ -880,6 +948,14 @@ bool Config::saveWifi() { return true; } +void Config::setTimeConf(){ + if(strlen(store.sntp1)>0 && strlen(store.sntp2)>0){ + configTime(store.tzHour * 3600 + store.tzMin * 60, getTimezoneOffset(), store.sntp1, store.sntp2); + }else if(strlen(store.sntp1)>0){ + configTime(store.tzHour * 3600 + store.tzMin * 60, getTimezoneOffset(), store.sntp1); + } +} + bool Config::initNetwork() { File file = SPIFFS.open(SSIDS_PATH, "r"); if (!file || file.isDirectory()) { @@ -973,7 +1049,7 @@ void Config::doSleepW(){ void Config::sleepForAfter(uint16_t sf, uint16_t sa){ sleepfor = sf; - if(sa > 0) _sleepTimer.attach(sa * 60, doSleep); + if(sa > 0) timekeeper.waitAndDo(sa * 60, doSleep); else doSleep(); } diff --git a/yoRadio/src/core/config.h b/yoRadio/src/core/config.h index 819e659..bbd577c 100644 --- a/yoRadio/src/core/config.h +++ b/yoRadio/src/core/config.h @@ -1,7 +1,6 @@ #ifndef config_h #define config_h #include "Arduino.h" -#include #include #include #include @@ -47,13 +46,23 @@ #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) #define ESP_ARDUINO_3 1 #endif -#define CONFIG_VERSION 4 + +#ifdef HEAP_DBG + #define HEAP_INFO() printHeapFragmentationInfo(__PRETTY_FUNCTION__) + void printHeapFragmentationInfo(const char* title); +#else + #define HEAP_INFO() +#endif + +#define CONFIG_VERSION 5 enum playMode_e : uint8_t { PM_WEB=0, PM_SDCARD=1 }; enum BitrateFormat { BF_UNCNOWN, BF_MP3, BF_AAC, BF_FLAC, BF_OGG, BF_WAV }; void u8fix(char *src); +void checkAllTasksStack(); + struct theme_t { uint16_t background; uint16_t meta; @@ -143,6 +152,12 @@ struct config_t bool screensaverPlayingBlank; char mdnsname[24]; bool skipPlaylistUpDown; + uint16_t abuff; + bool telnet; + bool watchdog; + uint16_t timeSyncInterval; + uint16_t timeSyncIntervalRTC; + uint16_t weatherSyncInterval; }; #if IR_PIN!=255 @@ -189,6 +204,10 @@ class Config { uint16_t screensaverPlayingTicks; bool isScreensaver; int newConfigMode; + char tmpBuf[BUFLEN]; + char tmpBuf2[BUFLEN]; + char ipBuf[16]; + char _stationBuf[BUFLEN/2]; public: Config() {}; //void save(); @@ -214,6 +233,7 @@ class Config { bool loadStation(uint16_t station); bool initNetwork(); bool saveWifi(); + void setTimeConf(); bool saveWifiFromNextion(const char* post); void setSmartStart(uint8_t ss); void setBitrateFormat(BitrateFormat fmt) { configFmt = fmt; } @@ -259,8 +279,10 @@ class Config { void setIrBtn(int val); #endif void resetSystem(const char *val, uint8_t clientId); - bool spiffsCleanup(); + char * ipToStr(IPAddress ip); + bool prepareForPlaying(uint16_t stationId); + void configPostPlaying(uint16_t stationId); FS* SDPLFS(){ return _SDplaylistFS; } #if RTCSUPPORTED bool isRTCFound(){ return _rtcFound; }; @@ -303,7 +325,6 @@ class Config { #endif FS* _SDplaylistFS; void setDefaults(); - Ticker _sleepTimer; static void doSleep(); uint16_t color565(uint8_t r, uint8_t g, uint8_t b); void _setupVersion(); @@ -314,7 +335,6 @@ class Config { uint16_t station = random(1, store.countStation); return station; } - char _stationBuf[BUFLEN/2]; }; extern Config config; diff --git a/yoRadio/src/core/controls.cpp b/yoRadio/src/core/controls.cpp index 7eb5803..2175c5d 100644 --- a/yoRadio/src/core/controls.cpp +++ b/yoRadio/src/core/controls.cpp @@ -56,12 +56,11 @@ constexpr uint8_t nrOfButtons = sizeof(button) / sizeof(button[0]); #include "../IRremoteESP8266/IRtext.h" #include "../IRremoteESP8266/IRutils.h" uint8_t irVolRepeat = 0; -const uint16_t kCaptureBufferSize = 1024; -const uint8_t kTimeout = IR_TIMEOUT; +//const uint16_t kCaptureBufferSize = 1024; const uint16_t kMinUnknownSize = 12; #define LEGACY_TIMING_INFO false -IRrecv irrecv(IR_PIN, kCaptureBufferSize, kTimeout, true); +IRrecv irrecv(IR_PIN, IR_BUFSIZE, IR_TIMEOUT, true); decode_results irResults; #endif diff --git a/yoRadio/src/core/display.cpp b/yoRadio/src/core/display.cpp index bba7421..fd866f3 100644 --- a/yoRadio/src/core/display.cpp +++ b/yoRadio/src/core/display.cpp @@ -5,13 +5,48 @@ #include "display.h" #include "player.h" #include "network.h" - +#include "netserver.h" +#include "timekeeper.h" Display display; #ifdef USE_NEXTION Nextion nextion; #endif +#ifndef CORE_STACK_SIZE + #define CORE_STACK_SIZE 1024*4 +#endif +#ifndef DSP_TASK_PRIORITY + #define DSP_TASK_PRIORITY 2 +#endif +#ifndef DSP_TASK_CORE_ID + #define DSP_TASK_CORE_ID 0 +#endif + +QueueHandle_t displayQueue; + +static void loopDspTask(void * pvParameters){ + while(true){ + #ifndef DUMMYDISPLAY + if(displayQueue==NULL) break; + netserver.loop(); + if(timekeeper.loop0()) + display.loop(); + // will NOT delay here, would use message dequeue timeout instead + //vTaskDelay(DSP_TASK_DELAY); + #else + netserver.loop(); + timekeeper.loop0(); + vTaskDelay(10); + #endif + } + vTaskDelete( NULL ); +} + +void Display::_createDspTask(){ + xTaskCreatePinnedToCore(loopDspTask, "DspTask", CORE_STACK_SIZE, NULL, DSP_TASK_PRIORITY, NULL, DSP_TASK_CORE_ID); +} + #ifndef DUMMYDISPLAY //============================================================================================================================ DspCore dsp; @@ -19,40 +54,27 @@ DspCore dsp; Page *pages[] = { new Page(), new Page(), new Page(), new Page() }; #ifndef DSQ_SEND_DELAY - #define DSQ_SEND_DELAY portMAX_DELAY + //#define DSQ_SEND_DELAY portMAX_DELAY + #define DSQ_SEND_DELAY pdMS_TO_TICKS(200) #endif -#ifndef CORE_STACK_SIZE - #define CORE_STACK_SIZE 1024*3 -#endif #ifndef DSP_TASK_DELAY - #define DSP_TASK_DELAY pdMS_TO_TICKS(10) + #define DSP_TASK_DELAY pdMS_TO_TICKS(20) // cap for 50 fps #endif + +// will use DSP_QUEUE_TICKS as delay interval for display task runner when there are no msgs in a queue to process +#define DSP_QUEUE_TICKS DSP_TASK_DELAY + #if !((DSP_MODEL==DSP_ST7735 && DTYPE==INITR_BLACKTAB) || DSP_MODEL==DSP_ST7789 || DSP_MODEL==DSP_ST7796 || DSP_MODEL==DSP_ILI9488 || DSP_MODEL==DSP_ILI9486 || DSP_MODEL==DSP_ILI9341 || DSP_MODEL==DSP_ILI9225) #undef BITRATE_FULL #define BITRATE_FULL false #endif -TaskHandle_t DspTask; -QueueHandle_t displayQueue; + void returnPlayer(){ display.putRequest(NEWMODE, PLAYER); } -void Display::_createDspTask(){ - xTaskCreatePinnedToCore(loopDspTask, "DspTask", CORE_STACK_SIZE, NULL, 4, &DspTask, !xPortGetCoreID()); -} - -void loopDspTask(void * pvParameters){ - while(true){ - if(displayQueue==NULL) break; - display.loop(); - vTaskDelay(DSP_TASK_DELAY); - } - vTaskDelete( NULL ); - DspTask=NULL; -} - void Display::init() { Serial.print("##[BOOT]#\tdisplay.init\t"); #ifdef USE_NEXTION @@ -117,7 +139,7 @@ void Display::_buildPager(){ _volbar = new SliderWidget(volbarConf, config.theme.volbarin, config.theme.background, 254, config.theme.volbarout); #endif #ifndef HIDE_HEAPBAR - _heapbar = new SliderWidget(heapbarConf, config.theme.buffer, config.theme.background, psramInit()?300000:1600 * AUDIOBUFFER_MULTIPLIER2); + _heapbar = new SliderWidget(heapbarConf, config.theme.buffer, config.theme.background, psramInit()?300000:1600 * config.store.abuff); #endif #ifndef HIDE_VOL _voltxt = new TextWidget(voltxtConf, 10, false, config.theme.vol, config.theme.background); @@ -197,7 +219,7 @@ void Display::_apScreen() { TextWidget *appass2 = (TextWidget*) &_boot->addWidget(new TextWidget(apPass2Conf, 30, false, config.theme.clock, config.theme.background)); appass2->setText(apPassword); ScrollWidget *bootSett = (ScrollWidget*) &_boot->addWidget(new ScrollWidget("*", apSettConf, config.theme.title2, config.theme.background)); - bootSett->setText(WiFi.softAPIP().toString().c_str(), apSettFmt); + bootSett->setText(config.ipToStr(WiFi.softAPIP()), apSettFmt); _pager.addPage(_boot); _pager.setPage(_boot); #else @@ -234,7 +256,7 @@ void Display::_start() { if(_vuwidget) _vuwidget->lock(); if(_rssi) _setRSSI(WiFi.RSSI()); #ifndef HIDE_IP - if(_volip) _volip->setText(WiFi.localIP().toString().c_str(), iptxtFmt); + if(_volip) _volip->setText(config.ipToStr(WiFi.localIP()), iptxtFmt); #endif _pager.setPage( pages[PG_PLAYER]); _volume(); @@ -254,11 +276,6 @@ void Display::_showDialog(const char *title){ _meta.setText(title); } -void Display::_setReturnTicker(uint8_t time_s){ - _returnTicker.detach(); - _returnTicker.once(time_s, returnPlayer); -} - void Display::_swichMode(displayMode_e newmode) { #ifdef USE_NEXTION //nextion.swichMode(newmode); @@ -276,7 +293,6 @@ void Display::_swichMode(displayMode_e newmode) { dsp.clearDsp(); #endif numOfNextStation = 0; - _returnTicker.detach(); #ifdef META_MOVE _meta.moveBack(); #endif @@ -304,7 +320,7 @@ void Display::_swichMode(displayMode_e newmode) { #ifndef HIDE_IP _showDialog(const_DlgVolume); #else - _showDialog(WiFi.localIP().toString().c_str()); + _showDialog(config.ipToStr(WiFi.localIP())); #endif _nums.setText(config.store.volume, numtxtFmt); } @@ -329,11 +345,11 @@ void Display::resetQueue(){ void Display::_drawPlaylist() { dsp.drawPlaylist(currentPlItem); - _setReturnTicker(30); + timekeeper.waitAndReturnPlayer(30); } void Display::_drawNextStationNum(uint16_t num) { - _setReturnTicker(30); + timekeeper.waitAndReturnPlayer(30); _meta.setText(config.stationByNum(num)); _nums.setText(num, "%d"); } @@ -374,9 +390,7 @@ void Display::_layoutChange(bool played){ } } } -#ifndef DSP_QUEUE_TICKS - #define DSP_QUEUE_TICKS 0 -#endif + void Display::loop() { if(_bootStep==0) { _pager.begin(); @@ -429,7 +443,7 @@ void Display::loop() { if(_weather) _weather->lock(!config.store.showweather); if(!config.store.showweather){ #ifndef HIDE_IP - if(_volip) _volip->setText(WiFi.localIP().toString().c_str(), iptxtFmt); + if(_volip) _volip->setText(config.ipToStr(WiFi.localIP()), iptxtFmt); #endif }else{ if(_weather) _weather->setText(const_getWeather); @@ -463,13 +477,19 @@ void Display::loop() { case DSP_START: _start(); break; case NEWIP: { #ifndef HIDE_IP - if(_volip) _volip->setText(WiFi.localIP().toString().c_str(), iptxtFmt); + if(_volip) _volip->setText(config.ipToStr(WiFi.localIP()), iptxtFmt); #endif break; } default: break; + + // check if there are more messages waiting in the Q, in this case break the loop() and go + // for another round to evict next message, do not waste time to redraw the screen, etc... + if (uxQueueMessagesWaiting(displayQueue)) + return; } } + dsp.loop(); #if I2S_DOUT==255 player.computeVUlevel(); @@ -561,7 +581,7 @@ void Display::_volume() { if(_voltxt) _voltxt->setText(config.store.volume, voltxtFmt); #endif if(_mode==VOL) { - _setReturnTicker(3); + timekeeper.waitAndReturnPlayer(3); _nums.setText(config.store.volume, numtxtFmt); } /*#ifdef USE_NEXTION diff --git a/yoRadio/src/core/display.h b/yoRadio/src/core/display.h index 6d2247a..cb3b14c 100644 --- a/yoRadio/src/core/display.h +++ b/yoRadio/src/core/display.h @@ -3,20 +3,18 @@ #include "options.h" #include "Arduino.h" -#include #include "config.h" #include "common.h" #include "../displays/dspcore.h" - - #if NEXTION_RX!=255 && NEXTION_TX!=255 #define USE_NEXTION #include "../displays/nextion.h" #endif +//static void loopDspTask(void * pvParameters); + #ifndef DUMMYDISPLAY - void loopDspTask(void * pvParameters); class Display { public: @@ -54,7 +52,6 @@ class Display { ClockWidget _clock; Page *_boot; TextWidget *_bootstring, *_volip, *_voltxt, *_rssi, *_bitrate; - Ticker _returnTicker; uint8_t _bootStep; void _time(bool redraw = false); void _apScreen(); @@ -68,7 +65,6 @@ class Display { void _showDialog(const char *title); void _buildPager(); void _bootScreen(); - void _setReturnTicker(uint8_t time_s); void _layoutChange(bool played); void _setRSSI(int rssi); }; @@ -98,6 +94,8 @@ class Display { bool deepsleep(){return true;} void wakeup(){} void printPLitem(uint8_t pos, const char* item){} + private: + void _createDspTask(); }; #endif diff --git a/yoRadio/src/core/mqtt.cpp b/yoRadio/src/core/mqtt.cpp index 497c83a..1eda8dd 100644 --- a/yoRadio/src/core/mqtt.cpp +++ b/yoRadio/src/core/mqtt.cpp @@ -9,7 +9,7 @@ AsyncMqttClient mqttClient; TimerHandle_t mqttReconnectTimer; -char topic[140], status[BUFLEN*3], vol[5], buf[20]; +char topic[100], status[BUFLEN+50]; void connectToMqtt() { mqttClient.connect(); @@ -25,8 +25,10 @@ void mqttInit() { connectToMqtt(); } +void zeroBuffer(){ memset(topic, 0, sizeof(topic)); memset(status, 0, sizeof(status)); } + void onMqttConnect(bool sessionPresent) { - memset(topic, 0, 140); + zeroBuffer(); sprintf(topic, "%s%s", MQTT_ROOT_TOPIC, "command"); mqttClient.subscribe(topic, 2); mqttPublishStatus(); @@ -36,13 +38,12 @@ void onMqttConnect(bool sessionPresent) { void mqttPublishStatus() { if(mqttClient.connected()){ - memset(topic, 0, 140); - memset(status, 0, BUFLEN*3); + zeroBuffer(); sprintf(topic, "%s%s", MQTT_ROOT_TOPIC, "status"); - char name[BUFLEN*2]; - char title[BUFLEN*2]; - config.escapeQuotes(config.station.name, name, sizeof(name)); - config.escapeQuotes(config.station.title, title, sizeof(name)); + char name[BUFLEN/2]; + char title[BUFLEN/2]; + config.escapeQuotes(config.station.name, name, sizeof(name)-10); + config.escapeQuotes(config.station.title, title, sizeof(title)-10); sprintf(status, "{\"status\": %d, \"station\": %d, \"name\": \"%s\", \"title\": \"%s\", \"on\": %d}", player.status()==PLAYING?1:0, config.lastStation(), name, title, config.store.dspon); mqttClient.publish(topic, 0, true, status); } @@ -50,17 +51,17 @@ void mqttPublishStatus() { void mqttPublishPlaylist() { if(mqttClient.connected()){ - memset(topic, 0, 140); - memset(status, 0, BUFLEN*3); + zeroBuffer(); sprintf(topic, "%s%s", MQTT_ROOT_TOPIC, "playlist"); - sprintf(status, "http://%s%s", WiFi.localIP().toString().c_str(), PLAYLIST_PATH); + sprintf(status, "http://%s%s", config.ipToStr(WiFi.localIP()), PLAYLIST_PATH); mqttClient.publish(topic, 0, true, status); } } void mqttPublishVolume(){ if(mqttClient.connected()){ - memset(topic, 0, 140); + zeroBuffer(); + char vol[5]; memset(vol, 0, 5); sprintf(topic, "%s%s", MQTT_ROOT_TOPIC, "volume"); sprintf(vol, "%d", config.store.volume); @@ -76,75 +77,64 @@ void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { if (len == 0) return; - memset(buf, 0, 20); - strlcpy(buf, payload, len+1); - if (strcmp(buf, "prev") == 0) { - player.prev(); + if(len<20){ + char buf[len+1]; + strncpy(buf, payload, len); + buf[len]='\0'; + if (strcmp(buf, "prev") == 0) { player.sendCommand({PR_PREV, 0}); return; } + if (strcmp(buf, "next") == 0) { player.sendCommand({PR_NEXT, 0}); return; } + if (strcmp(buf, "toggle") == 0) { player.sendCommand({PR_TOGGLE, 0}); return; } + if (strcmp(buf, "stop") == 0) { player.sendCommand({PR_STOP, 0}); return; } + if (strcmp(buf, "start") == 0 || strcmp(buf, "play") == 0) { player.sendCommand({PR_PLAY, config.lastStation()}); return; } + if (strcmp(buf, "boot") == 0 || strcmp(buf, "reboot") == 0) { ESP.restart(); return; } + if (strcmp(buf, "volm") == 0) { + player.stepVol(false); + return; + } + if (strcmp(buf, "volp") == 0) { + player.stepVol(true); + return; + } + if (strcmp(buf, "turnoff") == 0) { + uint8_t sst = config.store.smartstart; + config.setDspOn(0); + player.sendCommand({PR_STOP, 0}); + delay(100); + config.saveValue(&config.store.smartstart, sst); + return; + } + if (strcmp(buf, "turnon") == 0) { + config.setDspOn(1); + if (config.store.smartstart == 1) player.sendCommand({PR_PLAY, config.lastStation()}); + return; + } + int volume; + if ( sscanf(buf, "vol %d", &volume) == 1) { + if (volume < 0) volume = 0; + if (volume > 254) volume = 254; + player.setVol(volume); + return; + } + int sb; + if (sscanf(buf, "play %d", &sb) == 1 ) { + if (sb < 1) sb = 1; + uint16_t cs = config.playlistLength(); + if (sb >= cs) sb = cs; + player.sendCommand({PR_PLAY, (uint16_t)sb}); + return; + } + }else{ + if(len>MQTT_BURL_SIZE) return; + strncpy(player.burl, payload, len); + player.burl[len]='\0'; + player.sendCommand({PR_BURL, 0}); return; } - if (strcmp(buf, "next") == 0) { - player.next(); - return; - } - if (strcmp(buf, "toggle") == 0) { - player.toggle(); - return; - } - if (strcmp(buf, "stop") == 0) { - player.sendCommand({PR_STOP, 0}); - //telnet.info(); - return; - } - if (strcmp(buf, "start") == 0 || strcmp(buf, "play") == 0) { - player.sendCommand({PR_PLAY, config.lastStation()}); - return; - } - if (strcmp(buf, "boot") == 0 || strcmp(buf, "reboot") == 0) { - ESP.restart(); - return; - } - if (strcmp(buf, "volm") == 0) { - player.stepVol(false); - return; - } - if (strcmp(buf, "volp") == 0) { - player.stepVol(true); - return; - } - if (strcmp(buf, "turnoff") == 0) { - uint8_t sst = config.store.smartstart; - config.setDspOn(0); - player.sendCommand({PR_STOP, 0}); - //telnet.info(); - delay(100); - config.saveValue(&config.store.smartstart, sst); - return; - } - if (strcmp(buf, "turnon") == 0) { - config.setDspOn(1); - if (config.store.smartstart == 1) player.sendCommand({PR_PLAY, config.lastStation()}); - return; - } - int volume; - if ( sscanf(buf, "vol %d", &volume) == 1) { - if (volume < 0) volume = 0; - if (volume > 254) volume = 254; - player.setVol(volume); - return; - } - int sb; - if (sscanf(buf, "play %d", &sb) == 1 ) { - if (sb < 1) sb = 1; - uint16_t cs = config.playlistLength(); - if (sb >= cs) sb = cs; - player.sendCommand({PR_PLAY, (uint16_t)sb}); - return; - } - if (strstr(buf, "http")==buf){ + /*if (strstr(buf, "http")==0){ if(len+1>sizeof(player.burl)) return; strlcpy(player.burl, payload, len+1); return; - } + }*/ } #endif // #ifdef MQTT_ROOT_TOPIC diff --git a/yoRadio/src/core/mqtt.h b/yoRadio/src/core/mqtt.h index 074da59..bc76960 100644 --- a/yoRadio/src/core/mqtt.h +++ b/yoRadio/src/core/mqtt.h @@ -1,11 +1,9 @@ #ifndef mqtt_h #define mqtt_h #include "options.h" -#ifdef MQTT_ROOT_TOPIC -//#if __has_include("../../mqttoptions.h") -//#include "../../mqttoptions.h" -#include "../async-mqtt-client/AsyncMqttClient.h" +#ifdef MQTT_ROOT_TOPIC +#include "../async-mqtt-client/AsyncMqttClient.h" void mqttInit(); void connectToMqtt(); @@ -15,6 +13,7 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties void mqttPublishStatus(); void mqttPublishPlaylist(); void mqttPublishVolume(); +void zeroBuffer(); #endif // #ifdef MQTT_ROOT_TOPIC diff --git a/yoRadio/src/core/netserver.cpp b/yoRadio/src/core/netserver.cpp index e58366e..e808ca3 100644 --- a/yoRadio/src/core/netserver.cpp +++ b/yoRadio/src/core/netserver.cpp @@ -1,7 +1,6 @@ #include "netserver.h" #include -#include "config.h" #include "player.h" #include "telnet.h" #include "display.h" @@ -10,17 +9,10 @@ #include "mqtt.h" #include "controls.h" #include "commandhandler.h" +#include "timekeeper.h" #include #include - -#if USE_OTA -#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) -#include -#else -#include -#endif -#include -#endif +//#include #ifdef USE_SD #include "sdmanager.h" @@ -29,7 +21,7 @@ #define MIN_MALLOC 24112 #endif #ifndef NSQ_SEND_DELAY - #define NSQ_SEND_DELAY (TickType_t)100 //portMAX_DELAY? + #define NSQ_SEND_DELAY pdMS_TO_TICKS(100) //portMAX_DELAY? #endif //#define CORS_DEBUG //Enable CORS policy: 'Access-Control-Allow-Origin' (for testing) @@ -46,20 +38,19 @@ void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventTyp bool shouldReboot = false; #ifdef MQTT_ROOT_TOPIC -Ticker mqttplaylistticker; +//Ticker mqttplaylistticker; bool mqttplaylistblock = false; void mqttplaylistSend() { mqttplaylistblock = true; - mqttplaylistticker.detach(); +// mqttplaylistticker.detach(); mqttPublishPlaylist(); mqttplaylistblock = false; } #endif char* updateError() { - static char ret[140] = {0}; - sprintf(ret, "Update failed with error (%d)
%s", (int)Update.getError(), Update.errorString()); - return ret; + sprintf(netserver.nsBuf, "Update failed with error (%d)
%s", (int)Update.getError(), Update.errorString()); + return netserver.nsBuf; } bool NetServer::begin(bool quiet) { @@ -67,6 +58,7 @@ bool NetServer::begin(bool quiet) { if(!quiet) Serial.print("##[BOOT]#\tnetserver.begin\t"); importRequest = IMDONE; irRecordEnable = false; + playerBufMax = psramInit()?300000:1600 * config.store.abuff; nsQueue = xQueueCreate( 20, sizeof( nsRequestParams_t ) ); while(nsQueue==NULL){;} @@ -82,43 +74,8 @@ bool NetServer::begin(bool quiet) { webserver.begin(); if(strlen(config.store.mdnsname)>0) MDNS.begin(config.store.mdnsname); - websocket.onEvent(onWsEvent); webserver.addHandler(&websocket); -#if USE_OTA - if(strlen(config.store.mdnsname)>0) - ArduinoOTA.setHostname(config.store.mdnsname); -#ifdef OTA_PASS - ArduinoOTA.setPassword(OTA_PASS); -#endif - ArduinoOTA - .onStart([]() { - display.putRequest(NEWMODE, UPDATING); - telnet.printf("Start OTA updating %s\n", ArduinoOTA.getCommand() == U_FLASH?"firmware":"filesystem"); - }) - .onEnd([]() { - telnet.printf("\nEnd OTA update, Rebooting...\n"); - }) - .onProgress([](unsigned int progress, unsigned int total) { - telnet.printf("Progress OTA: %u%%\r", (progress / (total / 100))); - }) - .onError([](ota_error_t error) { - telnet.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) { - telnet.printf("Auth Failed\n"); - } else if (error == OTA_BEGIN_ERROR) { - telnet.printf("Begin Failed\n"); - } else if (error == OTA_CONNECT_ERROR) { - telnet.printf("Connect Failed\n"); - } else if (error == OTA_RECEIVE_ERROR) { - telnet.printf("Receive Failed\n"); - } else if (error == OTA_END_ERROR) { - telnet.printf("End Failed\n"); - } - }); - ArduinoOTA.begin(); -#endif - if(!quiet) Serial.println("done"); return true; } @@ -152,6 +109,7 @@ void NetServer::chunkedHtmlPage(const String& contentType, AsyncWebServerRequest strlcpy(chunkedPathBuffer, path, sizeof(chunkedPathBuffer)-1); AsyncWebServerResponse *response; response = request->beginChunkedResponse(contentType, chunkedHtmlPageCallback); + response->addHeader("Cache-Control","max-age=31536000"); request->send(response); } @@ -181,13 +139,12 @@ const char *getFormat(BitrateFormat _format) { } } -char wsbuf[BUFLEN * 2]; void NetServer::processQueue(){ if(nsQueue==NULL) return; nsRequestParams_t request; if(xQueueReceive(nsQueue, &request, NS_QUEUE_TICKS)){ - memset(wsbuf, 0, BUFLEN * 2); uint8_t clientId = request.clientId; + wsBuf[0]='\0'; switch (request.type) { case PLAYLIST: getPlaylist(clientId); break; case PLAYLISTSAVED: { @@ -205,36 +162,46 @@ void NetServer::processQueue(){ } case GETACTIVE: { bool dbgact = false, nxtn=false; - String act = F("\"group_wifi\","); + //String act = F("\"group_wifi\","); + nsBuf[0]='\0'; + APPEND_GROUP("group_wifi"); if (network.status == CONNECTED) { - act += F("\"group_system\","); - if (BRIGHTNESS_PIN != 255 || DSP_CAN_FLIPPED || DSP_MODEL == DSP_NOKIA5110 || dbgact) act += F("\"group_display\","); + //act += F("\"group_system\","); + APPEND_GROUP("group_system"); + if (BRIGHTNESS_PIN != 255 || DSP_CAN_FLIPPED || DSP_MODEL == DSP_NOKIA5110 || dbgact) APPEND_GROUP("group_display"); #ifdef USE_NEXTION - act += F("\"group_nextion\","); - if (!SHOW_WEATHER || dbgact) act += F("\"group_weather\","); + APPEND_GROUP("group_nextion"); + if (!SHOW_WEATHER || dbgact) APPEND_GROUP("group_weather"); nxtn=true; #endif #if defined(LCD_I2C) || defined(DSP_OLED) - act += F("\"group_oled\","); + APPEND_GROUP("group_oled"); #endif - #ifndef HIDE_VU - act += F("\"group_vu\","); + #if !defined(HIDE_VU) && !defined(DUMMYDISPLAY) + APPEND_GROUP("group_vu"); + #endif + if (BRIGHTNESS_PIN != 255 || nxtn || dbgact) APPEND_GROUP("group_brightness"); + if (DSP_CAN_FLIPPED || dbgact) APPEND_GROUP("group_tft"); + if (TS_MODEL != TS_MODEL_UNDEFINED || dbgact) APPEND_GROUP("group_touch"); + if (DSP_MODEL == DSP_NOKIA5110) APPEND_GROUP("group_nokia"); + APPEND_GROUP("group_timezone"); + if (SHOW_WEATHER || dbgact) APPEND_GROUP("group_weather"); + APPEND_GROUP("group_controls"); + if (ENC_BTNL != 255 || ENC2_BTNL != 255 || dbgact) APPEND_GROUP("group_encoder"); + if (IR_PIN != 255 || dbgact) APPEND_GROUP("group_ir"); + if (!psramInit()) APPEND_GROUP("group_buffer"); + #if RTCSUPPORTED + APPEND_GROUP("group_rtc"); + #else + APPEND_GROUP("group_wortc"); #endif - if (BRIGHTNESS_PIN != 255 || nxtn || dbgact) act += F("\"group_brightness\","); - if (DSP_CAN_FLIPPED || dbgact) act += F("\"group_tft\","); - if (TS_MODEL != TS_MODEL_UNDEFINED || dbgact) act += F("\"group_touch\","); - if (DSP_MODEL == DSP_NOKIA5110) act += F("\"group_nokia\","); - act += F("\"group_timezone\","); - if (SHOW_WEATHER || dbgact) act += F("\"group_weather\","); - act += F("\"group_controls\","); - if (ENC_BTNL != 255 || ENC2_BTNL != 255 || dbgact) act += F("\"group_encoder\","); - if (IR_PIN != 255 || dbgact) act += F("\"group_ir\","); } - act = act.substring(0, act.length() - 1); - sprintf (wsbuf, "{\"act\":[%s]}", act.c_str()); + size_t len = strlen(nsBuf); + if (len > 0 && nsBuf[len - 1] == ',') nsBuf[len - 1] = '\0'; + + snprintf(wsBuf, sizeof(wsBuf), "{\"act\":[%s]}", nsBuf); break; } - //case STARTUP: sprintf (wsbuf, "{\"command\":\"startup\", \"payload\": {\"mode\":\"%s\", \"version\":\"%s\"}}", network.status == CONNECTED ? "player" : "ap", YOVERSION); break; case GETINDEX: { requestOnChange(STATION, clientId); requestOnChange(TITLE, clientId); @@ -249,15 +216,19 @@ void NetServer::processQueue(){ return; break; } - case GETSYSTEM: sprintf (wsbuf, "{\"sst\":%d,\"aif\":%d,\"vu\":%d,\"softr\":%d,\"vut\":%d,\"mdns\":\"%s\"}", + case GETSYSTEM: sprintf (wsBuf, "{\"sst\":%d,\"aif\":%d,\"vu\":%d,\"softr\":%d,\"vut\":%d,\"mdns\":\"%s\",\"ipaddr\":\"%s\", \"abuff\": %d, \"telnet\": %d, \"watchdog\": %d }", config.store.smartstart != 2, config.store.audioinfo, config.store.vumeter, config.store.softapdelay, config.vuThreshold, - config.store.mdnsname); + config.store.mdnsname, + config.ipToStr(WiFi.localIP()), + config.store.abuff, + config.store.telnet, + config.store.watchdog); break; - case GETSCREEN: sprintf (wsbuf, "{\"flip\":%d,\"inv\":%d,\"nump\":%d,\"tsf\":%d,\"tsd\":%d,\"dspon\":%d,\"br\":%d,\"con\":%d,\"scre\":%d,\"scrt\":%d,\"scrb\":%d,\"scrpe\":%d,\"scrpt\":%d,\"scrpb\":%d}", + case GETSCREEN: sprintf (wsBuf, "{\"flip\":%d,\"inv\":%d,\"nump\":%d,\"tsf\":%d,\"tsd\":%d,\"dspon\":%d,\"br\":%d,\"con\":%d,\"scre\":%d,\"scrt\":%d,\"scrb\":%d,\"scrpe\":%d,\"scrpt\":%d,\"scrpb\":%d}", config.store.flipscreen, config.store.invertdisplay, config.store.numplaylist, @@ -273,52 +244,55 @@ void NetServer::processQueue(){ config.store.screensaverPlayingTimeout, config.store.screensaverPlayingBlank); break; - case GETTIMEZONE: sprintf (wsbuf, "{\"tzh\":%d,\"tzm\":%d,\"sntp1\":\"%s\",\"sntp2\":\"%s\"}", + case GETTIMEZONE: sprintf (wsBuf, "{\"tzh\":%d,\"tzm\":%d,\"sntp1\":\"%s\",\"sntp2\":\"%s\", \"timeint\":%d,\"timeintrtc\":%d}", config.store.tzHour, config.store.tzMin, config.store.sntp1, - config.store.sntp2); + config.store.sntp2, + config.store.timeSyncInterval, + config.store.timeSyncIntervalRTC); break; - case GETWEATHER: sprintf (wsbuf, "{\"wen\":%d,\"wlat\":\"%s\",\"wlon\":\"%s\",\"wkey\":\"%s\"}", + case GETWEATHER: sprintf (wsBuf, "{\"wen\":%d,\"wlat\":\"%s\",\"wlon\":\"%s\",\"wkey\":\"%s\",\"wint\":%d}", config.store.showweather, config.store.weatherlat, config.store.weatherlon, - config.store.weatherkey); + config.store.weatherkey, + config.store.weatherSyncInterval); break; - case GETCONTROLS: sprintf (wsbuf, "{\"vols\":%d,\"enca\":%d,\"irtl\":%d,\"skipup\":%d}", + case GETCONTROLS: sprintf (wsBuf, "{\"vols\":%d,\"enca\":%d,\"irtl\":%d,\"skipup\":%d}", config.store.volsteps, config.store.encacc, config.store.irtlp, config.store.skipPlaylistUpDown); break; - case DSPON: sprintf (wsbuf, "{\"dspontrue\":%d}", 1); break; + case DSPON: sprintf (wsBuf, "{\"dspontrue\":%d}", 1); break; case STATION: requestOnChange(STATIONNAME, clientId); requestOnChange(ITEM, clientId); break; - case STATIONNAME: sprintf (wsbuf, "{\"payload\":[{\"id\":\"nameset\", \"value\": \"%s\"}]}", config.station.name); break; - case ITEM: sprintf (wsbuf, "{\"current\": %d}", config.lastStation()); break; - case TITLE: sprintf (wsbuf, "{\"payload\":[{\"id\":\"meta\", \"value\": \"%s\"}]}", config.station.title); telnet.printf("##CLI.META#: %s\n> ", config.station.title); break; - case VOLUME: sprintf (wsbuf, "{\"payload\":[{\"id\":\"volume\", \"value\": %d}]}", config.store.volume); telnet.printf("##CLI.VOL#: %d\n", config.store.volume); break; - case NRSSI: sprintf (wsbuf, "{\"payload\":[{\"id\":\"rssi\", \"value\": %d}]}", rssi); /*rssi = 255;*/ break; - case SDPOS: sprintf (wsbuf, "{\"sdpos\": %d,\"sdend\": %d,\"sdtpos\": %d,\"sdtend\": %d}", + case STATIONNAME: sprintf (wsBuf, "{\"payload\":[{\"id\":\"nameset\", \"value\": \"%s\"}]}", config.station.name); break; + case ITEM: sprintf (wsBuf, "{\"current\": %d}", config.lastStation()); break; + case TITLE: sprintf (wsBuf, "{\"payload\":[{\"id\":\"meta\", \"value\": \"%s\"}]}", config.station.title); telnet.printf("##CLI.META#: %s\n> ", config.station.title); break; + case VOLUME: sprintf (wsBuf, "{\"payload\":[{\"id\":\"volume\", \"value\": %d}]}", config.store.volume); telnet.printf("##CLI.VOL#: %d\n", config.store.volume); break; + case NRSSI: sprintf (wsBuf, "{\"payload\":[{\"id\":\"rssi\", \"value\": %d}, {\"id\":\"heap\", \"value\": %d}]}", rssi, (player.isRunning() && config.store.audioinfo)?(int)(100*player.inBufferFilled()/playerBufMax):0); /*rssi = 255;*/ break; + case SDPOS: sprintf (wsBuf, "{\"sdpos\": %lu,\"sdend\": %lu,\"sdtpos\": %lu,\"sdtend\": %lu}", player.getFilePos(), player.getFileSize(), player.getAudioCurrentTime(), player.getAudioFileDuration()); break; - case SDLEN: sprintf (wsbuf, "{\"sdmin\": %d,\"sdmax\": %d}", player.sd_min, player.sd_max); break; - case SDSNUFFLE: sprintf (wsbuf, "{\"snuffle\": %d}", config.store.sdsnuffle); break; - case BITRATE: sprintf (wsbuf, "{\"payload\":[{\"id\":\"bitrate\", \"value\": %d}, {\"id\":\"fmt\", \"value\": \"%s\"}]}", config.station.bitrate, getFormat(config.configFmt)); break; - case MODE: sprintf (wsbuf, "{\"payload\":[{\"id\":\"playerwrap\", \"value\": \"%s\"}]}", player.status() == PLAYING ? "playing" : "stopped"); telnet.info(); break; - case EQUALIZER: sprintf (wsbuf, "{\"payload\":[{\"id\":\"bass\", \"value\": %d}, {\"id\": \"middle\", \"value\": %d}, {\"id\": \"trebble\", \"value\": %d}]}", config.store.bass, config.store.middle, config.store.trebble); break; - case BALANCE: sprintf (wsbuf, "{\"payload\":[{\"id\": \"balance\", \"value\": %d}]}", config.store.balance); break; - case SDINIT: sprintf (wsbuf, "{\"sdinit\": %d}", SDC_CS!=255); break; - case GETPLAYERMODE: sprintf (wsbuf, "{\"playermode\": \"%s\"}", config.getMode()==PM_SDCARD?"modesd":"modeweb"); break; + case SDLEN: sprintf (wsBuf, "{\"sdmin\": %lu,\"sdmax\": %lu}", player.sd_min, player.sd_max); break; + case SDSNUFFLE: sprintf (wsBuf, "{\"snuffle\": %d}", config.store.sdsnuffle); break; + case BITRATE: sprintf (wsBuf, "{\"payload\":[{\"id\":\"bitrate\", \"value\": %d}, {\"id\":\"fmt\", \"value\": \"%s\"}]}", config.station.bitrate, getFormat(config.configFmt)); break; + case MODE: sprintf (wsBuf, "{\"payload\":[{\"id\":\"playerwrap\", \"value\": \"%s\"}]}", player.status() == PLAYING ? "playing" : "stopped"); telnet.info(); break; + case EQUALIZER: sprintf (wsBuf, "{\"payload\":[{\"id\":\"bass\", \"value\": %d}, {\"id\": \"middle\", \"value\": %d}, {\"id\": \"trebble\", \"value\": %d}]}", config.store.bass, config.store.middle, config.store.trebble); break; + case BALANCE: sprintf (wsBuf, "{\"payload\":[{\"id\": \"balance\", \"value\": %d}]}", config.store.balance); break; + case SDINIT: sprintf (wsBuf, "{\"sdinit\": %d}", SDC_CS!=255); break; + case GETPLAYERMODE: sprintf (wsBuf, "{\"playermode\": \"%s\"}", config.getMode()==PM_SDCARD?"modesd":"modeweb"); break; #ifdef USE_SD case CHANGEMODE: config.changeMode(config.newConfigMode); return; break; #endif default: break; } - if (strlen(wsbuf) > 0) { - if (clientId == 0) { websocket.textAll(wsbuf); }else{ websocket.text(clientId, wsbuf); } + if (strlen(wsBuf) > 0) { + if (clientId == 0) { websocket.textAll(wsBuf); }else{ websocket.text(clientId, wsBuf); } #ifdef MQTT_ROOT_TOPIC if (clientId == 0 && (request.type == STATION || request.type == ITEM || request.type == TITLE || request.type == MODE)) mqttPublishStatus(); if (clientId == 0 && request.type == VOLUME) mqttPublishVolume(); @@ -341,22 +315,19 @@ void NetServer::loop() { default: break; } processQueue(); -#if USE_OTA - ArduinoOTA.handle(); -#endif } #if IR_PIN!=255 void NetServer::irToWs(const char* protocol, uint64_t irvalue) { - char buf[BUFLEN] = { 0 }; - sprintf (buf, "{\"ircode\": %llu, \"protocol\": \"%s\"}", irvalue, protocol); - websocket.textAll(buf); + wsBuf[0]='\0'; + sprintf (wsBuf, "{\"ircode\": %llu, \"protocol\": \"%s\"}", irvalue, protocol); + websocket.textAll(wsBuf); } void NetServer::irValsToWs() { if (!irRecordEnable) return; - char buf[BUFLEN] = { 0 }; - sprintf (buf, "{\"irvals\": [%llu, %llu, %llu]}", config.ircodes.irVals[config.irindex][0], config.ircodes.irVals[config.irindex][1], config.ircodes.irVals[config.irindex][2]); - websocket.textAll(buf); + wsBuf[0]='\0'; + sprintf (wsBuf, "{\"irvals\": [%llu, %llu, %llu]}", config.ircodes.irVals[config.irindex][0], config.ircodes.irVals[config.irindex][1], config.ircodes.irVals[config.irindex][2]); + websocket.textAll(wsBuf); } #endif @@ -364,32 +335,36 @@ void NetServer::onWsMessage(void *arg, uint8_t *data, size_t len, uint8_t client AwsFrameInfo *info = (AwsFrameInfo*)arg; if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { data[len] = 0; - char comnd[65], val[65]; - if (config.parseWsCommand((const char*)data, comnd, val, 65)) { - if (strcmp(comnd, "trebble") == 0) { - int8_t valb = atoi(val); + if (config.parseWsCommand((const char*)data, _wscmd, _wsval, 65)) { + if (strcmp(_wscmd, "ping") == 0) { + websocket.text(clientId, "{\"pong\": 1}"); + return; + } + if (strcmp(_wscmd, "trebble") == 0) { + int8_t valb = atoi(_wsval); config.setTone(config.store.bass, config.store.middle, valb); return; } - if (strcmp(comnd, "middle") == 0) { - int8_t valb = atoi(val); + if (strcmp(_wscmd, "middle") == 0) { + int8_t valb = atoi(_wsval); config.setTone(config.store.bass, valb, config.store.trebble); return; } - if (strcmp(comnd, "bass") == 0) { - int8_t valb = atoi(val); + if (strcmp(_wscmd, "bass") == 0) { + int8_t valb = atoi(_wsval); config.setTone(valb, config.store.middle, config.store.trebble); return; } - if (strcmp(comnd, "submitplaylistdone") == 0) { + if (strcmp(_wscmd, "submitplaylistdone") == 0) { #ifdef MQTT_ROOT_TOPIC - mqttplaylistticker.attach(5, mqttplaylistSend); + //mqttplaylistticker.attach(5, mqttplaylistSend); + timekeeper.waitAndDo(5, mqttplaylistSend); #endif if (player.isRunning()) player.sendCommand({PR_PLAY, -config.lastStation()}); return; } - if(cmd.exec(comnd, val, clientId)){ + if(cmd.exec(_wscmd, _wsval, clientId)){ return; } } @@ -397,9 +372,8 @@ void NetServer::onWsMessage(void *arg, uint8_t *data, size_t len, uint8_t client } void NetServer::getPlaylist(uint8_t clientId) { - char buf[160] = {0}; - sprintf(buf, "{\"file\": \"http://%s%s\"}", WiFi.localIP().toString().c_str(), PLAYLIST_PATH); - if (clientId == 0) { websocket.textAll(buf); } else { websocket.text(clientId, buf); } + sprintf(nsBuf, "{\"file\": \"http://%s%s\"}", config.ipToStr(WiFi.localIP()), PLAYLIST_PATH); + if (clientId == 0) { websocket.textAll(nsBuf); } else { websocket.text(clientId, nsBuf); } } int NetServer::_readPlaylistLine(File &file, char * line, size_t size){ @@ -413,27 +387,28 @@ int NetServer::_readPlaylistLine(File &file, char * line, size_t size){ bool NetServer::importPlaylist() { if(config.getMode()==PM_SDCARD) return false; + //player.sendCommand({PR_STOP, 0}); File tempfile = SPIFFS.open(TMP_PATH, "r"); if (!tempfile) { return false; } - char sName[BUFLEN], sUrl[BUFLEN], linePl[BUFLEN*3];; + char linePl[BUFLEN*3]; int sOvol; _readPlaylistLine(tempfile, linePl, sizeof(linePl)-1); - if (config.parseCSV(linePl, sName, sUrl, sOvol)) { + if (config.parseCSV(linePl, nsBuf, nsBuf2, sOvol)) { tempfile.close(); SPIFFS.rename(TMP_PATH, PLAYLIST_PATH); requestOnChange(PLAYLISTSAVED, 0); return true; } - if (config.parseJSON(linePl, sName, sUrl, sOvol)) { + if (config.parseJSON(linePl, nsBuf, nsBuf2, sOvol)) { File playlistfile = SPIFFS.open(PLAYLIST_PATH, "w"); - snprintf(linePl, sizeof(linePl)-1, "%s\t%s\t%d", sName, sUrl, 0); + snprintf(linePl, sizeof(linePl)-1, "%s\t%s\t%d", nsBuf, nsBuf2, 0); playlistfile.println(linePl); while (tempfile.available()) { _readPlaylistLine(tempfile, linePl, sizeof(linePl)-1); - if (config.parseJSON(linePl, sName, sUrl, sOvol)) { - snprintf(linePl, sizeof(linePl)-1, "%s\t%s\t%d", sName, sUrl, 0); + if (config.parseJSON(linePl, nsBuf, nsBuf2, sOvol)) { + snprintf(linePl, sizeof(linePl)-1, "%s\t%s\t%d", nsBuf, nsBuf2, 0); playlistfile.println(linePl); } } @@ -461,11 +436,12 @@ void NetServer::resetQueue(){ if(nsQueue!=NULL) xQueueReset(nsQueue); } -int freeSpace; void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + static int freeSpace = 0; if(request->url()=="/upload"){ if (!index) { if(filename!="tempwifi.csv"){ + //player.sendCommand({PR_STOP, 0}); if(SPIFFS.exists(PLAYLIST_PATH)) SPIFFS.remove(PLAYLIST_PATH); if(SPIFFS.exists(INDEX_PATH)) SPIFFS.remove(INDEX_PATH); if(SPIFFS.exists(PLAYLIST_SD_PATH)) SPIFFS.remove(PLAYLIST_SD_PATH); @@ -473,6 +449,8 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, } freeSpace = (float)SPIFFS.totalBytes()/100*68-SPIFFS.usedBytes(); request->_tempFile = SPIFFS.open(TMP_PATH , "w"); + }else{ + } if (len) { if(freeSpace>index+len){ @@ -481,6 +459,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, } if (final) { request->_tempFile.close(); + freeSpace = 0; } }else if(request->url()=="/update"){ if (!index) { @@ -510,6 +489,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, }else{ // "/webboard" DBGVB("File: %s, size:%u bytes, index: %u, final: %s\n", filename.c_str(), len, index, final?"true":"false"); if (!index) { + player.sendCommand({PR_STOP, 0}); String spath = "/www/"; if(filename=="playlist.csv" || filename=="wifi.csv") spath = "/data/"; request->_tempFile = SPIFFS.open(spath + filename , "w"); @@ -526,8 +506,8 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { switch (type) { - case WS_EVT_CONNECT: /*netserver.requestOnChange(STARTUP, client->id()); */if (config.store.audioinfo) Serial.printf("[WEBSOCKET] client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); break; - case WS_EVT_DISCONNECT: if (config.store.audioinfo) Serial.printf("[WEBSOCKET] client #%u disconnected\n", client->id()); break; + case WS_EVT_CONNECT: /*netserver.requestOnChange(STARTUP, client->id()); */if (config.store.audioinfo) Serial.printf("[WEBSOCKET] client #%lu connected from %s\n", client->id(), config.ipToStr(client->remoteIP())); break; + case WS_EVT_DISCONNECT: if (config.store.audioinfo) Serial.printf("[WEBSOCKET] client #%lu disconnected\n", client->id()); break; case WS_EVT_DATA: netserver.onWsMessage(arg, data, len, client->id()); break; case WS_EVT_PONG: case WS_EVT_ERROR: @@ -548,7 +528,7 @@ void handleNotFound(AsyncWebServerRequest * request) { if(request->url()=="/emergency") { request->send_P(200, "text/html", emergency_form); return; } if(request->method() == HTTP_POST && request->url()=="/webboard" && config.emptyFS) { request->redirect("/"); ESP.restart(); return; } if (request->method() == HTTP_GET) { - DBGVB("[%s] client ip=%s request of %s", __func__, request->client()->remoteIP().toString().c_str(), request->url().c_str()); + DBGVB("[%s] client ip=%s request of %s", __func__, config.ipToStr(request->client()->remoteIP()), request->url().c_str()); if (strcmp(request->url().c_str(), PLAYLIST_PATH) == 0 || strcmp(request->url().c_str(), SSIDS_PATH) == 0 || strcmp(request->url().c_str(), INDEX_PATH) == 0 || @@ -595,13 +575,15 @@ void handleNotFound(AsyncWebServerRequest * request) { return; } if (request->url() == "/variables.js") { - char varjsbuf[BUFLEN]; - sprintf (varjsbuf, "var yoVersion='%s';\nvar formAction='%s';\nvar playMode='%s';\n", YOVERSION, (network.status == CONNECTED && !config.emptyFS)?"webboard":"", (network.status == CONNECTED)?"player":"ap"); - request->send(200, "text/html", varjsbuf); + sprintf (netserver.nsBuf, "var yoVersion='%s';\nvar formAction='%s';\nvar playMode='%s';\n", YOVERSION, (network.status == CONNECTED && !config.emptyFS)?"webboard":"", (network.status == CONNECTED)?"player":"ap"); + request->send(200, "text/html", netserver.nsBuf); return; } if (strcmp(request->url().c_str(), "/settings.html") == 0 || strcmp(request->url().c_str(), "/update.html") == 0 || strcmp(request->url().c_str(), "/ir.html") == 0){ - request->send_P(200, "text/html", index_html); + //request->send_P(200, "text/html", index_html); + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", index_html); + response->addHeader("Cache-Control","max-age=31536000"); + request->send(response); return; } if (request->method() == HTTP_GET && request->url() == "/webboard") { @@ -618,11 +600,10 @@ void handleIndex(AsyncWebServerRequest * request) { if(request->url()=="/" && request->method() == HTTP_GET ) { request->send_P(200, "text/html", emptyfs_html); return; } if(request->url()=="/" && request->method() == HTTP_POST) { if(request->arg("ssid")!="" && request->arg("pass")!=""){ - char buf[BUFLEN]; - memset(buf, 0, BUFLEN); - snprintf(buf, BUFLEN, "%s\t%s", request->arg("ssid").c_str(), request->arg("pass").c_str()); + netserver.nsBuf[0]='\0'; + snprintf(netserver.nsBuf, sizeof(netserver.nsBuf), "%s\t%s", request->arg("ssid").c_str(), request->arg("pass").c_str()); request->redirect("/"); - config.saveWifiFromNextion(buf); + config.saveWifiFromNextion(netserver.nsBuf); return; } request->redirect("/"); @@ -641,7 +622,12 @@ void handleIndex(AsyncWebServerRequest * request) { } #endif if (strcmp(request->url().c_str(), "/") == 0 && request->params() == 0) { - if(network.status == CONNECTED) request->send_P(200, "text/html", index_html); else request->redirect("/settings.html"); + if(network.status == CONNECTED) { + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", index_html); + response->addHeader("Cache-Control","max-age=31536000"); + request->send(response); + //request->send_P(200, "text/html", index_html); + } else request->redirect("/settings.html"); return; } if(network.status == CONNECTED){ diff --git a/yoRadio/src/core/netserver.h b/yoRadio/src/core/netserver.h index a6bbba0..1ab21a6 100644 --- a/yoRadio/src/core/netserver.h +++ b/yoRadio/src/core/netserver.h @@ -1,9 +1,11 @@ #ifndef netserver_h #define netserver_h #include "Arduino.h" - +#include "config.h" #include "../AsyncWebServer/ESPAsyncWebServer.h" +#define APPEND_GROUP(name) strcat(nsBuf, "\"" name "\",") + enum requestType_e : uint8_t { PLAYLIST=1, STATION=2, STATIONNAME=3, ITEM=4, TITLE=5, VOLUME=6, NRSSI=7, BITRATE=8, MODE=9, EQUALIZER=10, BALANCE=11, PLAYLISTSAVED=12, STARTUP=13, GETINDEX=14, GETACTIVE=15, GETSYSTEM=16, GETSCREEN=17, GETTIMEZONE=18, GETWEATHER=19, GETCONTROLS=20, DSPON=21, SDPOS=22, SDLEN=23, SDSNUFFLE=24, SDINIT=25, GETPLAYERMODE=26, CHANGEMODE=27 }; enum import_e : uint8_t { IMDONE=0, IMPL=1, IMWIFI=2 }; const char emptyfs_html[] PROGMEM = R"( @@ -74,6 +76,7 @@ const char index_html[] PROGMEM = R"(
+
)"; @@ -96,6 +99,7 @@ class NetServer { import_e importRequest; bool resumePlay; char chunkedPathBuffer[40]; + char nsBuf[BUFLEN], nsBuf2[BUFLEN]; public: NetServer() {}; bool begin(bool quiet=false); @@ -110,11 +114,14 @@ class NetServer { void irToWs(const char* protocol, uint64_t irvalue); void irValsToWs(); #endif - void resetQueue(); + void resetQueue(); private: requestType_e request; QueueHandle_t nsQueue; + char _wscmd[65], _wsval[65]; + char wsBuf[BUFLEN*2]; int rssi; + uint32_t playerBufMax; void getPlaylist(uint8_t clientId); bool importPlaylist(); static size_t chunkedHtmlPageCallback(uint8_t* buffer, size_t maxLen, size_t index); diff --git a/yoRadio/src/core/network.cpp b/yoRadio/src/core/network.cpp index 20d4b56..028112b 100644 --- a/yoRadio/src/core/network.cpp +++ b/yoRadio/src/core/network.cpp @@ -6,98 +6,17 @@ #include "netserver.h" #include "player.h" #include "mqtt.h" +#include "timekeeper.h" #ifndef WIFI_ATTEMPTS #define WIFI_ATTEMPTS 16 #endif +#ifndef SEARCH_WIFI_CORE_ID + #define SEARCH_WIFI_CORE_ID 0 +#endif MyNetwork network; -TaskHandle_t syncTaskHandle; -//TaskHandle_t reconnectTaskHandle; - -bool getWeather(char *wstr); -void doSync(void * pvParameters); - -void ticks() { - if(!display.ready()) return; //waiting for SD is ready - pm.on_ticker(); - static const uint16_t weatherSyncInterval=1800; - //static const uint16_t weatherSyncIntervalFail=10; -#if RTCSUPPORTED - static const uint32_t timeSyncInterval=86400; - static uint32_t timeSyncTicks = 0; -#else - static const uint16_t timeSyncInterval=3600; - static uint16_t timeSyncTicks = 0; -#endif - static uint16_t weatherSyncTicks = 0; - static bool divrssi; - timeSyncTicks++; - weatherSyncTicks++; - divrssi = !divrssi; - if(network.status == CONNECTED){ - if(network.forceTimeSync || network.forceWeather){ - xTaskCreatePinnedToCore(doSync, "doSync", 1024 * 4, NULL, 0, &syncTaskHandle, 0); - } - if(timeSyncTicks >= timeSyncInterval){ - timeSyncTicks=0; - network.forceTimeSync = true; - } - if(weatherSyncTicks >= weatherSyncInterval){ - weatherSyncTicks=0; - network.forceWeather = true; - } - } -#ifndef DSP_LCD - if(config.store.screensaverEnabled && display.mode()==PLAYER && !player.isRunning()){ - config.screensaverTicks++; - if(config.screensaverTicks > config.store.screensaverTimeout+SCREENSAVERSTARTUPDELAY){ - if(config.store.screensaverBlank){ - display.putRequest(NEWMODE, SCREENBLANK); - }else{ - display.putRequest(NEWMODE, SCREENSAVER); - } - } - } - if(config.store.screensaverPlayingEnabled && display.mode()==PLAYER && player.isRunning()){ - config.screensaverPlayingTicks++; - if(config.screensaverPlayingTicks > config.store.screensaverPlayingTimeout*60+SCREENSAVERSTARTUPDELAY){ - if(config.store.screensaverPlayingBlank){ - display.putRequest(NEWMODE, SCREENBLANK); - }else{ - display.putRequest(NEWMODE, SCREENSAVER); - } - } - } -#endif -#if RTCSUPPORTED - if(config.isRTCFound()){ - rtc.getTime(&network.timeinfo); - mktime(&network.timeinfo); - display.putRequest(CLOCK); - } -#else - if(network.timeinfo.tm_year>100 || network.status == SDREADY) { - network.timeinfo.tm_sec++; - mktime(&network.timeinfo); - display.putRequest(CLOCK); - } -#endif - if(player.isRunning() && config.getMode()==PM_SDCARD) netserver.requestOnChange(SDPOS, 0); - if(divrssi) { - if(network.status == CONNECTED){ - netserver.setRSSI(WiFi.RSSI()); - netserver.requestOnChange(NRSSI, 0); - display.putRequest(DSPRSSI, netserver.getRSSI()); - } -#ifdef USE_SD - if(display.mode()!=SDCHANGE) player.sendCommand({PR_CHECKSD, 0}); -#endif - player.sendCommand({PR_VUTONUS, 0}); - } -} - void MyNetwork::WiFiReconnected(WiFiEvent_t event, WiFiEventInfo_t info){ network.beginReconnect = false; player.lockOutput = false; @@ -152,6 +71,8 @@ bool MyNetwork::wifiBegin(bool silent){ Serial.print("##[BOOT]#\t"); display.putRequest(BOOTSTRING, ls); } + WiFi.disconnect(true, true); //disconnect & erase internal credentials https://github.com/e2002/yoradio/pull/164/commits/89d8b4450dde99cd7930b84bb14d81dab920b879 + delay(100); WiFi.begin(config.ssids[ls].ssid, config.ssids[ls].password); while (WiFi.status() != WL_CONNECTED) { if(!silent) Serial.print("."); @@ -180,7 +101,7 @@ bool MyNetwork::wifiBegin(bool silent){ void searchWiFi(void * pvParameters){ if(!network.wifiBegin(true)){ delay(10000); - xTaskCreatePinnedToCore(searchWiFi, "searchWiFi", 1024 * 4, NULL, 0, NULL, 0); + xTaskCreatePinnedToCore(searchWiFi, "searchWiFi", 1024 * 4, NULL, 0, NULL, SEARCH_WIFI_CORE_ID); }else{ network.status = CONNECTED; netserver.begin(true); @@ -196,8 +117,6 @@ void searchWiFi(void * pvParameters){ void MyNetwork::begin() { BOOTLOG("network.begin"); config.initNetwork(); - ctimer.detach(); - forceTimeSync = forceWeather = true; if (config.ssidsCount == 0 || DBGAP) { raiseSoftAP(); return; @@ -213,7 +132,7 @@ void MyNetwork::begin() { setWifiParams(); }else{ status = SDREADY; - xTaskCreatePinnedToCore(searchWiFi, "searchWiFi", 1024 * 4, NULL, 0, NULL, 0); + xTaskCreatePinnedToCore(searchWiFi, "searchWiFi", 1024 * 4, NULL, 0, NULL, SEARCH_WIFI_CORE_ID); } Serial.println("##[BOOT]#\tdone"); @@ -226,7 +145,6 @@ void MyNetwork::begin() { display.putRequest(CLOCK); } #endif - ctimer.attach(1, ticks); if (network_on_connect) network_on_connect(); pm.on_connect(); } @@ -236,16 +154,11 @@ void MyNetwork::setWifiParams(){ WiFi.onEvent(WiFiReconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP); WiFi.onEvent(WiFiLostConnection, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); weatherBuf=NULL; - trueWeather = false; #if (DSP_MODEL!=DSP_DUMMY || defined(USE_NEXTION)) && !defined(HIDE_WEATHER) weatherBuf = (char *) malloc(sizeof(char) * WEATHER_STRING_L); memset(weatherBuf, 0, WEATHER_STRING_L); #endif - if(strlen(config.store.sntp1)>0 && strlen(config.store.sntp2)>0){ - configTime(config.store.tzHour * 3600 + config.store.tzMin * 60, config.getTimezoneOffset(), config.store.sntp1, config.store.sntp2); - }else if(strlen(config.store.sntp1)>0){ - configTime(config.store.tzHour * 3600 + config.store.tzMin * 60, config.getTimezoneOffset(), config.store.sntp1); - } + //config.setTimeConf(); //?? } void MyNetwork::requestTimeSync(bool withTelnetOutput, uint8_t clientId) { @@ -275,209 +188,9 @@ void MyNetwork::raiseSoftAP() { BOOTLOG("************************************************"); status = SOFT_AP; if(config.store.softapdelay>0) - rtimer.once(config.store.softapdelay*60, rebootTime); + timekeeper.waitAndDo(config.store.softapdelay*60, rebootTime); } void MyNetwork::requestWeatherSync(){ display.putRequest(NEWWEATHER); } - - -void doSync( void * pvParameters ) { - static uint8_t tsFailCnt = 0; - //static uint8_t wsFailCnt = 0; - if(network.forceTimeSync){ - network.forceTimeSync = false; - if(getLocalTime(&network.timeinfo)){ - tsFailCnt = 0; - network.forceTimeSync = false; - mktime(&network.timeinfo); - display.putRequest(CLOCK); - network.requestTimeSync(true); - #if RTCSUPPORTED - if (config.isRTCFound()) rtc.setTime(&network.timeinfo); - #endif - }else{ - if(tsFailCnt<4){ - network.forceTimeSync = true; - tsFailCnt++; - }else{ - network.forceTimeSync = false; - tsFailCnt=0; - } - } - } - if(network.weatherBuf && (strlen(config.store.weatherkey)!=0 && config.store.showweather) && network.forceWeather){ - network.forceWeather = false; - network.trueWeather=getWeather(network.weatherBuf); - } - vTaskDelete( NULL ); -} - -bool getWeather(char *wstr) { -#if (DSP_MODEL!=DSP_DUMMY || defined(USE_NEXTION)) && !defined(HIDE_WEATHER) - WiFiClient client; - const char* host = "api.openweathermap.org"; - - if (!client.connect(host, 80)) { - Serial.println("##WEATHER###: connection failed"); - return false; - } - char httpget[250] = {0}; - sprintf(httpget, "GET /data/2.5/weather?lat=%s&lon=%s&units=%s&lang=%s&appid=%s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", config.store.weatherlat, config.store.weatherlon, weatherUnits, weatherLang, config.store.weatherkey, host); - client.print(httpget); - unsigned long timeout = millis(); - while (client.available() == 0) { - if (millis() - timeout > 2000UL) { - Serial.println("##WEATHER###: client available timeout !"); - client.stop(); - return false; - } - } - timeout = millis(); - String line = ""; - if (client.connected()) { - while (client.available()) - { - line = client.readStringUntil('\n'); - if (strstr(line.c_str(), "\"temp\"") != NULL) { - client.stop(); - break; - } - if ((millis() - timeout) > 500) - { - client.stop(); - Serial.println("##WEATHER###: client read timeout !"); - return false; - } - } - } - if (strstr(line.c_str(), "\"temp\"") == NULL) { - Serial.println("##WEATHER###: weather not found !"); - return false; - } - char *tmpe; - char *tmps; - char *tmpc; - const char* cursor = line.c_str(); - char desc[120], temp[20], hum[20], press[20], icon[5]; - - tmps = strstr(cursor, "\"description\":\""); - if (tmps == NULL) { Serial.println("##WEATHER###: description not found !"); return false;} - tmps += 15; - tmpe = strstr(tmps, "\",\""); - if (tmpe == NULL) { Serial.println("##WEATHER###: description not found !"); return false;} - strlcpy(desc, tmps, tmpe - tmps + 1); - cursor = tmpe + 2; - - // "ясно","icon":"01d"}], - tmps = strstr(cursor, "\"icon\":\""); - if (tmps == NULL) { Serial.println("##WEATHER###: icon not found !"); return false;} - tmps += 8; - tmpe = strstr(tmps, "\"}"); - if (tmpe == NULL) { Serial.println("##WEATHER###: icon not found !"); return false;} - strlcpy(icon, tmps, tmpe - tmps + 1); - cursor = tmpe + 2; - - tmps = strstr(cursor, "\"temp\":"); - if (tmps == NULL) { Serial.println("##WEATHER###: temp not found !"); return false;} - tmps += 7; - tmpe = strstr(tmps, ",\""); - if (tmpe == NULL) { Serial.println("##WEATHER###: temp not found !"); return false;} - strlcpy(temp, tmps, tmpe - tmps + 1); - cursor = tmpe + 1; - float tempf = atof(temp); - - tmps = strstr(cursor, "\"feels_like\":"); - if (tmps == NULL) { Serial.println("##WEATHER###: feels_like not found !"); return false;} - tmps += 13; - tmpe = strstr(tmps, ",\""); - if (tmpe == NULL) { Serial.println("##WEATHER###: feels_like not found !"); return false;} - strlcpy(temp, tmps, tmpe - tmps + 1); - cursor = tmpe + 2; - float tempfl = atof(temp); (void)tempfl; - - tmps = strstr(cursor, "\"pressure\":"); - if (tmps == NULL) { Serial.println("##WEATHER###: pressure not found !"); return false;} - tmps += 11; - tmpe = strstr(tmps, ",\""); - if (tmpe == NULL) { Serial.println("##WEATHER###: pressure not found !"); return false;} - strlcpy(press, tmps, tmpe - tmps + 1); - cursor = tmpe + 2; - int pressi = (float)atoi(press) / 1.333; - - tmps = strstr(cursor, "humidity\":"); - if (tmps == NULL) { Serial.println("##WEATHER###: humidity not found !"); return false;} - tmps += 10; - tmpe = strstr(tmps, ",\""); - tmpc = strstr(tmps, "}"); - if (tmpe == NULL) { Serial.println("##WEATHER###: humidity not found !"); return false;} - strlcpy(hum, tmps, tmpe - tmps + (tmpc>tmpe?1:0)); - - tmps = strstr(cursor, "\"grnd_level\":"); - bool grnd_level_pr = (tmps != NULL); - if(grnd_level_pr){ - tmps += 13; - tmpe = strstr(tmps, ",\""); - if (tmpe == NULL) { Serial.println("##WEATHER###: grnd_level not found !"); return false;} - strlcpy(press, tmps, tmpe - tmps + 1); - cursor = tmpe + 2; - pressi = (float)atoi(press) / 1.333; - } - - tmps = strstr(cursor, "\"speed\":"); - if (tmps == NULL) { Serial.println("##WEATHER###: wind speed not found !"); return false;} - tmps += 8; - tmpe = strstr(tmps, ",\""); - if (tmpe == NULL) { Serial.println("##WEATHER###: wind speed not found !"); return false;} - strlcpy(temp, tmps, tmpe - tmps + 1); - cursor = tmpe + 1; - float wind_speed = atof(temp); (void)wind_speed; - - tmps = strstr(cursor, "\"deg\":"); - if (tmps == NULL) { Serial.println("##WEATHER###: wind deg not found !"); return false;} - tmps += 6; - tmpe = strstr(tmps, ",\""); - if (tmpe == NULL) { Serial.println("##WEATHER###: wind deg not found !"); return false;} - strlcpy(temp, tmps, tmpe - tmps + 1); - cursor = tmpe + 1; - int wind_deg = atof(temp)/22.5; - if(wind_deg<0) wind_deg = 16+wind_deg; - - - #ifdef USE_NEXTION - nextion.putcmdf("press_txt.txt=\"%dmm\"", pressi); - nextion.putcmdf("hum_txt.txt=\"%d%%\"", atoi(hum)); - char cmd[30]; - snprintf(cmd, sizeof(cmd)-1,"temp_txt.txt=\"%.1f\"", tempf); - nextion.putcmd(cmd); - int iconofset; - if(strstr(icon,"01")!=NULL) iconofset = 0; - else if(strstr(icon,"02")!=NULL) iconofset = 1; - else if(strstr(icon,"03")!=NULL) iconofset = 2; - else if(strstr(icon,"04")!=NULL) iconofset = 3; - else if(strstr(icon,"09")!=NULL) iconofset = 4; - else if(strstr(icon,"10")!=NULL) iconofset = 5; - else if(strstr(icon,"11")!=NULL) iconofset = 6; - else if(strstr(icon,"13")!=NULL) iconofset = 7; - else if(strstr(icon,"50")!=NULL) iconofset = 8; - else iconofset = 9; - nextion.putcmd("cond_img.pic", 50+iconofset); - nextion.weatherVisible(1); - #endif - - Serial.printf("##WEATHER###: description: %s, temp:%.1f C, pressure:%dmmHg, humidity:%s%%\n", desc, tempf, pressi, hum); - #ifdef WEATHER_FMT_SHORT - sprintf(wstr, weatherFmt, tempf, pressi, hum); - #else - #if EXT_WEATHER - sprintf(wstr, weatherFmt, desc, tempf, tempfl, pressi, hum, wind_speed, wind[wind_deg]); - #else - sprintf(wstr, weatherFmt, desc, tempf, pressi, hum); - #endif - #endif - network.requestWeatherSync(); - return true; -#endif // if (DSP_MODEL!=DSP_DUMMY || defined(USE_NEXTION)) && !defined(HIDE_WEATHER) - return false; -} diff --git a/yoRadio/src/core/network.h b/yoRadio/src/core/network.h index e125d24..020efaf 100644 --- a/yoRadio/src/core/network.h +++ b/yoRadio/src/core/network.h @@ -1,6 +1,5 @@ #ifndef network_h #define network_h -#include #include "time.h" #include "WiFi.h" #include "rtcsupport.h" @@ -17,12 +16,10 @@ class MyNetwork { public: n_Status_e status; struct tm timeinfo; - bool firstRun, forceTimeSync, forceWeather; bool lostPlaying = false, beginReconnect = false; //uint8_t tsFailCnt, wsFailCnt; - Ticker ctimer; char *weatherBuf; - bool trueWeather; + //bool trueWeather; public: MyNetwork() {}; void begin(); @@ -31,7 +28,6 @@ class MyNetwork { void setWifiParams(); bool wifiBegin(bool silent=false); private: - Ticker rtimer; void raiseSoftAP(); static void WiFiLostConnection(WiFiEvent_t event, WiFiEventInfo_t info); static void WiFiReconnected(WiFiEvent_t event, WiFiEventInfo_t info); diff --git a/yoRadio/src/core/options.h b/yoRadio/src/core/options.h index 67a20a3..5e636b3 100644 --- a/yoRadio/src/core/options.h +++ b/yoRadio/src/core/options.h @@ -1,7 +1,7 @@ #ifndef options_h #define options_h -#define YOVERSION "0.9.533" +#define YOVERSION "0.9.550" /******************************************************* DO NOT EDIT THIS FILE. @@ -361,6 +361,9 @@ The connection tables are located here https://github.com/e2002/yoradio#connecti #ifndef IR_TIMEOUT #define IR_TIMEOUT 80 // kTimeout, see IRremoteESP8266 documentation #endif +#ifndef IR_BUFSIZE + #define IR_BUFSIZE 128 +#endif /* THEMES */ /* color name R G B */ @@ -488,10 +491,26 @@ The connection tables are located here https://github.com/e2002/yoradio#connecti #ifndef USE_OTA #define USE_OTA false #endif +#ifndef WATCHDOG_INTERVAL + #define WATCHDOG_INTERVAL 3 //sec. +#endif //#define OTA_PASS "myotapassword12345" //#define HTTP_USER "user" //#define HTTP_PASS "password" - - +#ifndef WATCHDOG_TASK_SIZE + #define WATCHDOG_TASK_SIZE 1024*6 +#endif +#ifndef WATCHDOG_TASK_PRIORITY + #define WATCHDOG_TASK_PRIORITY 3 +#endif +#ifndef WATCHDOG_TASK_CORE_ID + #define WATCHDOG_TASK_CORE_ID 1 +#endif +#ifndef CONNECTION_TIMEOUT + #define CONNECTION_TIMEOUT 5700 +#endif +#ifndef CONNECTION_TIMEOUT_SSL + #define CONNECTION_TIMEOUT_SSL 5700 +#endif #endif diff --git a/yoRadio/src/core/player.cpp b/yoRadio/src/core/player.cpp index 0436afc..05495c6 100644 --- a/yoRadio/src/core/player.cpp +++ b/yoRadio/src/core/player.cpp @@ -5,6 +5,7 @@ #include "display.h" #include "sdmanager.h" #include "netserver.h" +#include "timekeeper.h" Player player; QueueHandle_t playerQueue; @@ -35,10 +36,10 @@ void Player::init() { Serial.print("##[BOOT]#\tplayer.init\t"); playerQueue=NULL; _resumeFilePos = 0; + _hasError=false; playerQueue = xQueueCreate( 5, sizeof( playerRequestParams_t ) ); setOutputPins(false); delay(50); - memset(_plError, 0, PLERR_LN); #ifdef MQTT_ROOT_TOPIC memset(burl, 0, MQTT_BURL_SIZE); #endif @@ -56,14 +57,13 @@ void Player::init() { setTone(config.store.bass, config.store.middle, config.store.trebble); setVolume(0); _status = STOPPED; - //setOutputPins(false); _volTimer=false; //randomSeed(analogRead(0)); #if PLAYER_FORCE_MONO forceMono(true); #endif _loadVol(config.store.volume); - setConnectionTimeout(1700, 3700); + setConnectionTimeout(CONNECTION_TIMEOUT, CONNECTION_TIMEOUT_SSL); Serial.println("done"); } @@ -73,21 +73,23 @@ void Player::sendCommand(playerRequestParams_t request){ } void Player::resetQueue(){ - if(playerQueue!=NULL) xQueueReset(playerQueue); + if(playerQueue!=NULL) xQueueReset(playerQueue); } void Player::stopInfo() { config.setSmartStart(0); - //telnet.info(); netserver.requestOnChange(MODE, 0); } +void Player::setError(){ + _hasError=true; + config.setTitle(config.tmpBuf); + telnet.printf("##ERROR#:\t%s\n", config.tmpBuf); +} + void Player::setError(const char *e){ - strlcpy(_plError, e, PLERR_LN); - if(hasError()) { - config.setTitle(_plError); - telnet.printf("##ERROR#:\t%s\n", e); - } + strlcpy(config.tmpBuf, e, sizeof(config.tmpBuf)); + setError(); } void Player::_stop(bool alreadyStopped){ @@ -95,17 +97,19 @@ void Player::_stop(bool alreadyStopped){ if(config.getMode()==PM_SDCARD && !alreadyStopped) config.sdResumePos = player.getFilePos(); _status = STOPPED; setOutputPins(false); - if(!hasError()) config.setTitle((display.mode()==LOST || display.mode()==UPDATING)?"":const_PlStopped); + if(!_hasError) config.setTitle((display.mode()==LOST || display.mode()==UPDATING)?"":const_PlStopped); config.station.bitrate = 0; config.setBitrateFormat(BF_UNCNOWN); #ifdef USE_NEXTION nextion.bitrate(config.station.bitrate); #endif + setDefaults(); + if(!alreadyStopped) stopSong(); netserver.requestOnChange(BITRATE, 0); display.putRequest(DBITRATE); display.putRequest(PSTOP); - setDefaults(); - if(!alreadyStopped) stopSong(); + //setDefaults(); + //if(!alreadyStopped) stopSong(); if(!lockOutput) stopInfo(); if (player_on_stop_play) player_on_stop_play(); pm.on_stop_play(); @@ -119,6 +123,12 @@ void Player::initHeaders(const char *file) { //netserver.requestOnChange(SDPOS, 0); setDefaults(); } +void resetPlayer(){ + if(!config.store.watchdog) return; + player.resetQueue(); + player.sendCommand({PR_STOP, 0}); + player.loop(); +} #ifndef PL_QUEUE_TICKS #define PL_QUEUE_TICKS 0 @@ -141,6 +151,10 @@ void Player::loop() { pm.on_station_change(); break; } + case PR_TOGGLE: { + toggle(); + break; + } case PR_VOL: { config.setVolume(requestP.payload); Audio::setVolume(volToI2S(requestP.payload)); @@ -157,8 +171,19 @@ void Player::loop() { break; } #endif - case PR_VUTONUS: + case PR_VUTONUS: { if(config.vuThreshold>10) config.vuThreshold -=10; + break; + } + case PR_BURL: { + #ifdef MQTT_ROOT_TOPIC + if(strlen(burl)>0){ + browseUrl(); + } + #endif + break; + } + default: break; } } @@ -170,11 +195,12 @@ void Player::loop() { _volTimer=false; } } + /* #ifdef MQTT_ROOT_TOPIC if(strlen(burl)>0){ browseUrl(); } -#endif +#endif*/ } void Player::setOutputPins(bool isPlaying) { @@ -185,32 +211,15 @@ void Player::setOutputPins(bool isPlaying) { void Player::_play(uint16_t stationId) { log_i("%s called, stationId=%d", __func__, stationId); - setError(""); + _hasError=false; setDefaults(); - remoteStationName = false; - config.setDspOn(1); - config.vuThreshold = 0; - //display.putRequest(PSTOP); - config.screensaverTicks=SCREENSAVERSTARTUPDELAY; - config.screensaverPlayingTicks=SCREENSAVERSTARTUPDELAY; - if(config.getMode()!=PM_SDCARD) { - display.putRequest(PSTOP); - } + _status = STOPPED; setOutputPins(false); - //config.setTitle(config.getMode()==PM_WEB?const_PlConnect:""); - if(!config.loadStation(stationId)) return; - config.setTitle(config.getMode()==PM_WEB?const_PlConnect:"[next track]"); - config.station.bitrate=0; - config.setBitrateFormat(BF_UNCNOWN); + remoteStationName = false; + if(!config.prepareForPlaying(stationId)) return; _loadVol(config.store.volume); - display.putRequest(DBITRATE); - display.putRequest(NEWSTATION); - netserver.requestOnChange(STATION, 0); - netserver.loop(); - netserver.loop(); - if(config.store.smartstart!=2) - config.setSmartStart(0); + bool isConnected = false; if(config.getMode()==PM_SDCARD && SDC_CS!=255){ isConnected=connecttoFS(sdman,config.station.url,config.sdResumePos==0?_resumeFilePos:config.sdResumePos-player.sd_min); @@ -219,36 +228,25 @@ void Player::_play(uint16_t stationId) { } if(config.getMode()==PM_WEB) isConnected=connecttohost(config.station.url); if(isConnected){ - //if (config.store.play_mode==PM_WEB?connecttohost(config.station.url):connecttoFS(SD,config.station.url,config.sdResumePos==0?_resumeFilePos:config.sdResumePos-player.sd_min)) { _status = PLAYING; - if(config.getMode()==PM_SDCARD) { - config.sdResumePos = 0; - config.saveValue(&config.store.lastSdStation, stationId); - } - //config.setTitle(""); - if(config.store.smartstart!=2) - config.setSmartStart(1); - netserver.requestOnChange(MODE, 0); + config.configPostPlaying(stationId); setOutputPins(true); - display.putRequest(NEWMODE, PLAYER); - display.putRequest(PSTART); if (player_on_start_play) player_on_start_play(); pm.on_start_play(); }else{ - telnet.printf("##ERROR#:\tError connecting to %s\n", config.station.url); - SET_PLAY_ERROR("Error connecting to %s", config.station.url); + telnet.printf("##ERROR#:\tError connecting to %.128s\n", config.station.url); + snprintf(config.tmpBuf, sizeof(config.tmpBuf), "Error connecting to %.128s", config.station.url); setError(); _stop(true); }; } #ifdef MQTT_ROOT_TOPIC void Player::browseUrl(){ - setError(""); + _hasError=false; remoteStationName = true; config.setDspOn(1); resumeAfterUrl = _status==PLAYING; display.putRequest(PSTOP); -// setDefaults(); setOutputPins(false); config.setTitle(const_PlConnect); if (connecttohost(burl)){ @@ -260,16 +258,15 @@ void Player::browseUrl(){ if (player_on_start_play) player_on_start_play(); pm.on_start_play(); }else{ - telnet.printf("##ERROR#:\tError connecting to %s\n", burl); - SET_PLAY_ERROR("Error connecting to %s", burl); + telnet.printf("##ERROR#:\tError connecting to %.128s\n", burl); + snprintf(config.tmpBuf, sizeof(config.tmpBuf), "Error connecting to %.128s", burl); setError(); _stop(true); } - memset(burl, 0, MQTT_BURL_SIZE); + //memset(burl, 0, MQTT_BURL_SIZE); } #endif void Player::prev() { - uint16_t lastStation = config.lastStation(); if(config.getMode()==PM_WEB || !config.store.sdsnuffle){ if (lastStation == 1) config.lastStation(config.playlistLength()); else config.lastStation(lastStation-1); diff --git a/yoRadio/src/core/player.h b/yoRadio/src/core/player.h index a10e6a9..d1fbf9c 100644 --- a/yoRadio/src/core/player.h +++ b/yoRadio/src/core/player.h @@ -13,13 +13,14 @@ #endif #ifndef PLQ_SEND_DELAY - #define PLQ_SEND_DELAY portMAX_DELAY + //#define PLQ_SEND_DELAY portMAX_DELAY + #define PLQ_SEND_DELAY pdMS_TO_TICKS(1000) #endif -#define PLERR_LN 64 -#define SET_PLAY_ERROR(...) {char buff[512 + 64]; sprintf(buff,__VA_ARGS__); setError(buff);} +//#define PLERR_LN 64 +//#define SET_PLAY_ERROR(...) {char buff[512 + 64]; sprintf(buff,__VA_ARGS__); setError(buff);} -enum playerRequestType_e : uint8_t { PR_PLAY = 1, PR_STOP = 2, PR_PREV = 3, PR_NEXT = 4, PR_VOL = 5, PR_CHECKSD = 6, PR_VUTONUS = 7 }; +enum playerRequestType_e : uint8_t { PR_PLAY = 1, PR_STOP = 2, PR_PREV = 3, PR_NEXT = 4, PR_VOL = 5, PR_CHECKSD = 6, PR_VUTONUS = 7, PR_BURL = 8, PR_TOGGLE = 9 }; struct playerRequestParams_t { playerRequestType_e type; @@ -34,11 +35,12 @@ class Player: public Audio { bool _volTimer; /* delayed volume save */ uint32_t _resumeFilePos; plStatus_e _status; - char _plError[PLERR_LN]; + //char _plError[PLERR_LN]; private: void _stop(bool alreadyStopped = false); void _play(uint16_t stationId); void _loadVol(uint8_t volume); + bool _hasError; public: bool lockOutput = true; bool resumeAfterUrl = false; @@ -51,8 +53,9 @@ class Player: public Audio { void init(); void loop(); void initHeaders(const char *file); + void setError(); void setError(const char *e); - bool hasError() { return strlen(_plError)>0; } + //bool hasError() { return strlen(_plError)>0; } void sendCommand(playerRequestParams_t request); void resetQueue(); #ifdef MQTT_ROOT_TOPIC diff --git a/yoRadio/src/core/sdmanager.cpp b/yoRadio/src/core/sdmanager.cpp index fd8cf1d..90a908d 100644 --- a/yoRadio/src/core/sdmanager.cpp +++ b/yoRadio/src/core/sdmanager.cpp @@ -45,10 +45,11 @@ bool SDManager::cardPresent() { } bool SDManager::_checkNoMedia(const char* path){ - char nomedia[BUFLEN]= {0}; - strlcat(nomedia, path, BUFLEN); - strlcat(nomedia, "/.nomedia", BUFLEN); - bool nm = exists(nomedia); + if (path[strlen(path) - 1] == '/') + snprintf(config.tmpBuf, sizeof(config.tmpBuf), "%s%s", path, ".nomedia"); + else + snprintf(config.tmpBuf, sizeof(config.tmpBuf), "%s/%s", path, ".nomedia"); + bool nm = exists(config.tmpBuf); return nm; } diff --git a/yoRadio/src/core/telnet.cpp b/yoRadio/src/core/telnet.cpp index 1d6a8ad..2d672f7 100644 --- a/yoRadio/src/core/telnet.cpp +++ b/yoRadio/src/core/telnet.cpp @@ -5,11 +5,12 @@ #include "player.h" #include "network.h" #include "telnet.h" +#include "esp_heap_caps.h" Telnet telnet; bool Telnet::_isIPSet(IPAddress ip) { - return ip.toString() == "0.0.0.0"; + return strcmp(config.ipToStr(ip), "0.0.0.0") == 0; } bool Telnet::begin(bool quiet) { @@ -21,12 +22,11 @@ bool Telnet::begin(bool quiet) { } if(!quiet) Serial.print("##[BOOT]#\ttelnet.begin\t"); if (WiFi.status() == WL_CONNECTED || _isIPSet(WiFi.softAPIP())) { - server.begin(); - server.setNoDelay(true); + toggle(); if(!quiet){ Serial.println("done"); Serial.println("##[BOOT]#"); - BOOTLOG("Ready! Go to http:/%s/ to configure", WiFi.localIP().toString().c_str()); + BOOTLOG("Ready! Go to http:/%s/ to configure", config.ipToStr(WiFi.localIP())); BOOTLOG("------------------------------------------------"); Serial.println("##[BOOT]#"); } @@ -36,10 +36,19 @@ bool Telnet::begin(bool quiet) { } } +void Telnet::start() { + server.begin(); + server.setNoDelay(true); +} + void Telnet::stop() { server.stop(); } +void Telnet::toggle() { + if(config.store.telnet) { start(); }else{ stop(); } +} + void Telnet::emptyClientStream(WiFiClient client) { client.flush(); delay(50); @@ -72,40 +81,41 @@ void Telnet::loop() { return; } uint8_t i; - if (WiFi.status() == WL_CONNECTED) { - if (server.hasClient()) { - for (i = 0; i < MAX_TLN_CLIENTS; i++) { - if (!clients[i] || !clients[i].connected()) { - if (clients[i]) { - clients[i].stop(); + if(config.store.telnet) + if (WiFi.status() == WL_CONNECTED) { + if (server.hasClient()) { + for (i = 0; i < MAX_TLN_CLIENTS; i++) { + if (!clients[i] || !clients[i].connected()) { + if (clients[i]) { + clients[i].stop(); + } + clients[i] = server.available(); + if (!clients[i]) Serial.println("available broken"); + on_connect(config.ipToStr(clients[i].remoteIP()), i); + clients[i].setNoDelay(true); + emptyClientStream(clients[i]); + break; } - clients[i] = server.available(); - if (!clients[i]) Serial.println("available broken"); - on_connect(clients[i].remoteIP().toString().c_str(), i); - clients[i].setNoDelay(true); - emptyClientStream(clients[i]); - break; + } + if (i >= MAX_TLN_CLIENTS) { + server.available().stop(); } } - if (i >= MAX_TLN_CLIENTS) { - server.available().stop(); + for (i = 0; i < MAX_TLN_CLIENTS; i++) { + if (clients[i] && clients[i].connected() && clients[i].available()) { + String inputstr = clients[i].readStringUntil('\n'); + inputstr.trim(); + on_input(inputstr.c_str(), i); + } } - } - for (i = 0; i < MAX_TLN_CLIENTS; i++) { - if (clients[i] && clients[i].connected() && clients[i].available()) { - String inputstr = clients[i].readStringUntil('\n'); - inputstr.trim(); - on_input(inputstr.c_str(), i); + } else { + for (i = 0; i < MAX_TLN_CLIENTS; i++) { + if (clients[i]) { + clients[i].stop(); + } } + delay(1000); } - } else { - for (i = 0; i < MAX_TLN_CLIENTS; i++) { - if (clients[i]) { - clients[i].stop(); - } - } - delay(1000); - } handleSerial(); } @@ -125,35 +135,33 @@ void Telnet::print(uint8_t id, const char *buf) { } void Telnet::printf(const char *format, ...) { - char buf[MAX_PRINTF_LEN]; va_list args; va_start (args, format ); - vsnprintf(buf, MAX_PRINTF_LEN, format, args); + vsnprintf(cmBuf, sizeof(cmBuf), format, args); va_end (args); for (int id = 0; id < MAX_TLN_CLIENTS; id++) { if (clients[id] && clients[id].connected()) { - clients[id].print(buf); + clients[id].print(cmBuf); } } - if (strcmp(buf, "> ") == 0) return; + if (strcmp(cmBuf, "> ") == 0) return; //if(strstr(buf,"\n> ")==NULL) Serial.print(buf); - char *nl = strstr(buf, "\n> "); - if (nl != NULL) { buf[nl-buf+1] = '\0'; } - Serial.print(buf); + char *nl = strstr(cmBuf, "\n> "); + if (nl != NULL) { cmBuf[nl-cmBuf+1] = '\0'; } + Serial.print(cmBuf); } void Telnet::printf(uint8_t id, const char *format, ...) { - char buf[MAX_PRINTF_LEN]; va_list argptr; va_start(argptr, format); - vsnprintf(buf, MAX_PRINTF_LEN, format, argptr); + vsnprintf(cmBuf, sizeof(cmBuf), format, argptr); va_end(argptr); if(id>MAX_TLN_CLIENTS){ - Serial.print(buf); + Serial.print(cmBuf); return; } if (clients[id] && clients[id].connected()) { - clients[id].print(buf); + clients[id].print(cmBuf); } } @@ -164,9 +172,8 @@ void Telnet::on_connect(const char* str, uint8_t clientId) { void Telnet::info() { telnet.printf("##CLI.INFO#\n"); - char timeStringBuff[50]; - strftime(timeStringBuff, sizeof(timeStringBuff), "%Y-%m-%dT%H:%M:%S+03:00", &network.timeinfo); - telnet.printf("##SYS.DATE#: %s\n", timeStringBuff); //TODO timezone offset + strftime(config.tmpBuf, sizeof(config.tmpBuf), "%Y-%m-%dT%H:%M:%S+03:00", &network.timeinfo); + telnet.printf("##SYS.DATE#: %s\n", config.tmpBuf); //TODO timezone offset telnet.printf("##CLI.NAMESET#: %d %s\n", config.lastStation(), config.station.name); if (player.status() == PLAYING) { telnet.printf("##CLI.META#: %s\n", config.station.title); @@ -180,6 +187,16 @@ void Telnet::info() { telnet.printf("> "); } +void Telnet::printHeapFragmentationInfo(uint8_t id){ + size_t freeHeap = heap_caps_get_free_size(MALLOC_CAP_DEFAULT); + size_t largestBlock = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT); + float fragmentation = 100.0 * (1.0 - ((float)largestBlock / (float)freeHeap)); + printf(id, "\n*************************************\n"); + printf(id, "* Free heap: %u bytes\n", freeHeap); + printf(id, "* Largest free block: %u bytes\n", largestBlock); + printf(id, "* Fragmentation: %.2f%%\n", fragmentation); + printf(id, "*************************************\n\n"); +} void Telnet::on_input(const char* str, uint8_t clientId) { if (strlen(str) == 0) return; if(network.status == CONNECTED){ @@ -253,12 +270,11 @@ void Telnet::on_input(const char* str, uint8_t clientId) { if (!file || file.isDirectory()) { return; } - char sName[BUFLEN], sUrl[BUFLEN]; int sOvol; uint8_t c = 1; while (file.available()) { - if (config.parseCSV(file.readStringUntil('\n').c_str(), sName, sUrl, sOvol)) { - printf(clientId, "#CLI.LISTNUM#: %*d: %s, %s\n", 3, c, sName, sUrl); + if (config.parseCSV(file.readStringUntil('\n').c_str(), config.tmpBuf, config.tmpBuf2, sOvol)) { + printf(clientId, "#CLI.LISTNUM#: %*d: %s, %s\n", 3, c, config.tmpBuf, config.tmpBuf2); c++; } } @@ -268,12 +284,11 @@ void Telnet::on_input(const char* str, uint8_t clientId) { } if (strcmp(str, "cli.info") == 0 || strcmp(str, "info") == 0) { printf(clientId, "##CLI.INFO#\n"); - char timeStringBuff[50]; - strftime(timeStringBuff, sizeof(timeStringBuff), "%Y-%m-%dT%H:%M:%S", &network.timeinfo); + strftime(config.tmpBuf, sizeof(config.tmpBuf), "%Y-%m-%dT%H:%M:%S", &network.timeinfo); if (config.store.tzHour < 0) { - printf(clientId, "##SYS.DATE#: %s%03d:%02d\n", timeStringBuff, config.store.tzHour, config.store.tzMin); + printf(clientId, "##SYS.DATE#: %s%03d:%02d\n", config.tmpBuf, config.store.tzHour, config.store.tzMin); } else { - printf(clientId, "##SYS.DATE#: %s+%02d:%02d\n", timeStringBuff, config.store.tzHour, config.store.tzMin); + printf(clientId, "##SYS.DATE#: %s+%02d:%02d\n", config.tmpBuf, config.store.tzHour, config.store.tzMin); } printf(clientId, "##CLI.NAMESET#: %d %s\n", config.lastStation(), config.station.name); if (player.status() == PLAYING) { @@ -406,11 +421,10 @@ void Telnet::on_input(const char* str, uint8_t clientId) { printf(clientId, "#WIFI.CON#\n"); File file = SPIFFS.open(SSIDS_PATH, "r"); if (file && !file.isDirectory()) { - char sSid[BUFLEN], sPas[BUFLEN]; uint8_t c = 1; while (file.available()) { - if (config.parseSsid(file.readStringUntil('\n').c_str(), sSid, sPas)) { - printf(clientId, "%d: %s, %s\n", c, sSid, sPas); + if (config.parseSsid(file.readStringUntil('\n').c_str(), config.tmpBuf, config.tmpBuf2)) { + printf(clientId, "%d: %s, %s\n", c, config.tmpBuf, config.tmpBuf2); c++; } } @@ -422,11 +436,10 @@ void Telnet::on_input(const char* str, uint8_t clientId) { printf(clientId, "#WIFI.STATION#\n"); File file = SPIFFS.open(SSIDS_PATH, "r"); if (file && !file.isDirectory()) { - char sSid[BUFLEN], sPas[BUFLEN]; uint8_t c = 1; while (file.available()) { - if (config.parseSsid(file.readStringUntil('\n').c_str(), sSid, sPas)) { - if(c==config.store.lastSSID) printf(clientId, "%d: %s, %s\n", c, sSid, sPas); + if (config.parseSsid(file.readStringUntil('\n').c_str(), config.tmpBuf, config.tmpBuf2)) { + if(c==config.store.lastSSID) printf(clientId, "%d: %s, %s\n", c, config.tmpBuf, config.tmpBuf2); c++; } } @@ -434,23 +447,21 @@ void Telnet::on_input(const char* str, uint8_t clientId) { printf(clientId, "##WIFI.STATION#\n> "); return; } - char newssid[30], newpass[40]; - if (sscanf(str, "wifi.con(\"%[^\"]\",\"%[^\"]\")", newssid, newpass) == 2 || sscanf(str, "wifi.con(%[^,],%[^)])", newssid, newpass) == 2 || sscanf(str, "wifi.con(%[^ ] %[^)])", newssid, newpass) == 2 || sscanf(str, "wifi %[^ ] %s", newssid, newpass) == 2) { - char buf[BUFLEN]; - snprintf(buf, BUFLEN, "New SSID: \"%s\" with PASS: \"%s\" for next boot\n> ", newssid, newpass); - printf(clientId, buf); + if (sscanf(str, "wifi.con(\"%[^\"]\",\"%[^\"]\")", config.tmpBuf, config.tmpBuf2) == 2 || sscanf(str, "wifi.con(%[^,],%[^)])", config.tmpBuf, config.tmpBuf2) == 2 || sscanf(str, "wifi.con(%[^ ] %[^)])", config.tmpBuf, config.tmpBuf2) == 2 || sscanf(str, "wifi %[^ ] %s", config.tmpBuf, config.tmpBuf2) == 2) { + snprintf(cmBuf, sizeof(cmBuf), "New SSID: \"%s\" with PASS: \"%s\" for next boot\n> ", config.tmpBuf, config.tmpBuf2); + printf(clientId, cmBuf); printf(clientId, "...REBOOTING...\n> "); - memset(buf, 0, BUFLEN); - snprintf(buf, BUFLEN, "%s\t%s", newssid, newpass); - config.saveWifiFromNextion(buf); + memset(cmBuf, 0, sizeof(cmBuf)); + snprintf(cmBuf, sizeof(cmBuf), "%s\t%s", config.tmpBuf, config.tmpBuf2); + config.saveWifiFromNextion(cmBuf); return; } if (strcmp(str, "wifi.status") == 0 || strcmp(str, "status") == 0) { printf(clientId, "#WIFI.STATUS#\nStatus:\t\t%d\nMode:\t\t%s\nIP:\t\t%s\nMask:\t\t%s\nGateway:\t%s\nRSSI:\t\t%d dBm\n##WIFI.STATUS#\n> ", WiFi.status(), WiFi.getMode()==WIFI_STA?"WIFI_STA":"WIFI_AP", - WiFi.getMode()==WIFI_STA?WiFi.localIP().toString():WiFi.softAPIP().toString(), - WiFi.getMode()==WIFI_STA?WiFi.subnetMask().toString():"255.255.255.0", - WiFi.getMode()==WIFI_STA?WiFi.gatewayIP().toString():WiFi.softAPIP().toString(), + WiFi.getMode()==WIFI_STA?config.ipToStr(WiFi.localIP()):config.ipToStr(WiFi.softAPIP()), + WiFi.getMode()==WIFI_STA?config.ipToStr(WiFi.subnetMask()):"255.255.255.0", + WiFi.getMode()==WIFI_STA?config.ipToStr(WiFi.gatewayIP()):config.ipToStr(WiFi.softAPIP()), WiFi.RSSI() ); return; @@ -460,12 +471,14 @@ void Telnet::on_input(const char* str, uint8_t clientId) { return; } if (strcmp(str, "sys.heap") == 0 || strcmp(str, "heap") == 0) { - printf(clientId, "Free heap:\t%d bytes\n> ", xPortGetFreeHeapSize()); + //printf(clientId, "Free heap:\t%d bytes\n> ", xPortGetFreeHeapSize()); + printHeapFragmentationInfo(clientId); return; } if (strcmp(str, "sys.config") == 0 || strcmp(str, "config") == 0) { config.bootInfo(); - printf(clientId, "Free heap:\t%d bytes\n> ", xPortGetFreeHeapSize()); + //printf(clientId, "Free heap:\t%d bytes\n> ", xPortGetFreeHeapSize()); + printHeapFragmentationInfo(clientId); return; } if (strcmp(str, "wifi.discon") == 0 || strcmp(str, "discon") == 0 || strcmp(str, "disconnect") == 0) { diff --git a/yoRadio/src/core/telnet.h b/yoRadio/src/core/telnet.h index 03ab99d..6e45ebd 100644 --- a/yoRadio/src/core/telnet.h +++ b/yoRadio/src/core/telnet.h @@ -4,14 +4,15 @@ #include #define MAX_TLN_CLIENTS 5 -#define MAX_PRINTF_LEN BUFLEN+50 class Telnet { public: Telnet() {}; bool begin(bool quiet=false); void loop(); + void start(); void stop(); + void toggle(); void print(uint8_t id, const char *buf); void print(const char *buf); void printf(uint8_t id, const char *format, ...); @@ -25,8 +26,10 @@ class Telnet { void on_connect(const char* str, uint8_t clientId); void on_input(const char* str, uint8_t clientId); private: + char cmBuf[220]; bool _isIPSet(IPAddress ip); void handleSerial(); + void printHeapFragmentationInfo(uint8_t id); }; extern Telnet telnet; diff --git a/yoRadio/src/core/timekeeper.cpp b/yoRadio/src/core/timekeeper.cpp new file mode 100644 index 0000000..a2b60b6 --- /dev/null +++ b/yoRadio/src/core/timekeeper.cpp @@ -0,0 +1,386 @@ +#include "timekeeper.h" +#include "options.h" +#include "config.h" +#include "network.h" +#include "display.h" +#include "player.h" +#include "netserver.h" +#include "rtcsupport.h" + +#if RTCSUPPORTED + //#define TIME_SYNC_INTERVAL 24*60*60*1000 + #define TIME_SYNC_INTERVAL config.store.timeSyncIntervalRTC*60*60*1000 +#else + #define TIME_SYNC_INTERVAL config.store.timeSyncInterval*60*1000 +#endif +#define WEATHER_SYNC_INTERVAL config.store.weatherSyncInterval*60*1000 + +#define SYNC_STACK_SIZE 1024 * 4 +#define SYNC_TASK_CORE 0 +#define SYNC_TASK_PRIORITY 1 + +TimeKeeper timekeeper; + +void _syncTask(void *pvParameters) { + if (timekeeper.forceWeather && timekeeper.forceTimeSync) { + timekeeper.weatherTask(); + timekeeper.timeTask(); + } + else if (timekeeper.forceWeather) { + timekeeper.weatherTask(); + } + else if (timekeeper.forceTimeSync) { + timekeeper.timeTask(); + } + timekeeper.busy = false; + vTaskDelete(NULL); +} + +bool TimeKeeper::loop0(){ // core0 (display) + uint32_t currentTime = millis(); + static uint32_t _last1s = 0; + static uint32_t _last2s = 0; + static uint32_t _last5s = 0; + if (currentTime - _last1s >= 1000) { // 1sec + _last1s = currentTime; +#ifndef DUMMYDISPLAY + #ifndef UPCLOCK_CORE1 + _upClock(); + #endif +#endif + } + if (currentTime - _last2s >= 2000) { // 2sec + _last2s = currentTime; + _upRSSI(); + } + if (currentTime - _last5s >= 5000) { // 2sec + _last5s = currentTime; + //HEAP_INFO(); + } + #ifdef DUMMYDISPLAY + return true; + #endif + static uint32_t lastWeatherTime = 0; + if (currentTime - lastWeatherTime >= WEATHER_SYNC_INTERVAL) { + lastWeatherTime = currentTime; + forceWeather = true; + } + static uint32_t lastTimeTime = 0; + if (currentTime - lastTimeTime >= TIME_SYNC_INTERVAL) { + lastTimeTime = currentTime; + forceTimeSync = true; + } + if (!busy && (forceWeather || forceTimeSync) && network.status == CONNECTED) { + busy = true; + //config.setTimeConf(); + xTaskCreatePinnedToCore( + _syncTask, + "syncTask", + SYNC_STACK_SIZE, + NULL, // Params + SYNC_TASK_PRIORITY, + NULL, // Descriptor + SYNC_TASK_CORE + ); + } + return true; // just in case +} + +bool TimeKeeper::loop1(){ // core1 (player) + uint32_t currentTime = millis(); + static uint32_t _last1s = 0; + static uint32_t _last2s = 0; + if (currentTime - _last1s >= 1000) { // 1sec + pm.on_ticker(); + _last1s = currentTime; +#ifndef DUMMYDISPLAY + #ifdef UPCLOCK_CORE1 + _upClock(); + #endif +#endif + _upScreensaver(); + _upSDPos(); + _returnPlayer(); + _doAfterWait(); + } + if (currentTime - _last2s >= 2000) { // 2sec + _last2s = currentTime; + } + return true; // just in case +} + +void TimeKeeper::waitAndReturnPlayer(uint8_t time_s){ + _returnPlayerTime = millis()+time_s*1000; +} +void TimeKeeper::_returnPlayer(){ + if(_returnPlayerTime>0 && millis()>=_returnPlayerTime){ + _returnPlayerTime = 0; + display.putRequest(NEWMODE, PLAYER); + } +} + +void TimeKeeper::waitAndDo(uint8_t time_s, void (*callback)()){ + _doAfterTime = millis()+time_s*1000; + _aftercallback = callback; +} +void TimeKeeper::_doAfterWait(){ + if(_doAfterTime>0 && millis()>=_doAfterTime){ + _doAfterTime = 0; + _aftercallback(); + } +} + +void TimeKeeper::_upClock(){ +#if RTCSUPPORTED + if(config.isRTCFound()){ + rtc.getTime(&network.timeinfo); + mktime(&network.timeinfo); + if(display.ready()) display.putRequest(CLOCK); + } +#else + if(network.timeinfo.tm_year>100 || network.status == SDREADY) { + network.timeinfo.tm_sec++; + mktime(&network.timeinfo); + if(display.ready()) display.putRequest(CLOCK); + } +#endif +} + +void TimeKeeper::_upScreensaver(){ +#ifndef DSP_LCD + if(!display.ready()) return; + if(config.store.screensaverEnabled && display.mode()==PLAYER && !player.isRunning()){ + config.screensaverTicks++; + if(config.screensaverTicks > config.store.screensaverTimeout+SCREENSAVERSTARTUPDELAY){ + if(config.store.screensaverBlank){ + display.putRequest(NEWMODE, SCREENBLANK); + }else{ + display.putRequest(NEWMODE, SCREENSAVER); + } + } + } + if(config.store.screensaverPlayingEnabled && display.mode()==PLAYER && player.isRunning()){ + config.screensaverPlayingTicks++; + if(config.screensaverPlayingTicks > config.store.screensaverPlayingTimeout*60+SCREENSAVERSTARTUPDELAY){ + if(config.store.screensaverPlayingBlank){ + display.putRequest(NEWMODE, SCREENBLANK); + }else{ + display.putRequest(NEWMODE, SCREENSAVER); + } + } + } +#endif +} + +void TimeKeeper::_upRSSI(){ + if(network.status == CONNECTED){ + netserver.setRSSI(WiFi.RSSI()); + netserver.requestOnChange(NRSSI, 0); + if(display.ready()) display.putRequest(DSPRSSI, netserver.getRSSI()); + } +#ifdef USE_SD + if(display.mode()!=SDCHANGE) player.sendCommand({PR_CHECKSD, 0}); +#endif + player.sendCommand({PR_VUTONUS, 0}); +} + +void TimeKeeper::_upSDPos(){ + if(player.isRunning() && config.getMode()==PM_SDCARD) netserver.requestOnChange(SDPOS, 0); +} + +void TimeKeeper::timeTask(){ + static uint8_t tsFailCnt = 0; + if(getLocalTime(&network.timeinfo)){ + tsFailCnt = 0; + forceTimeSync = false; + mktime(&network.timeinfo); + display.putRequest(CLOCK); + network.requestTimeSync(true); + #if RTCSUPPORTED + if (config.isRTCFound()) rtc.setTime(&network.timeinfo); + #endif + }else{ + if(tsFailCnt<4){ + forceTimeSync = true; + tsFailCnt++; + }else{ + forceTimeSync = false; + tsFailCnt=0; + } + } +} +void TimeKeeper::weatherTask(){ + if(!network.weatherBuf || strlen(config.store.weatherkey)==0 || !config.store.showweather) return; + forceWeather = false; + _getWeather(network.weatherBuf); +} + +bool _getWeather(char *wstr) { +#if (DSP_MODEL!=DSP_DUMMY || defined(USE_NEXTION)) && !defined(HIDE_WEATHER) + + WiFiClient client; + const char* host = "api.openweathermap.org"; + if (!client.connect(host, 80)) { + Serial.println("##WEATHER###: connection failed"); + return false; + } + char httpget[250] = {0}; + sprintf(httpget, "GET /data/2.5/weather?lat=%s&lon=%s&units=%s&lang=%s&appid=%s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", config.store.weatherlat, config.store.weatherlon, weatherUnits, weatherLang, config.store.weatherkey, host); + client.print(httpget); + unsigned long timeout = millis(); + while (client.available() == 0) { + if (millis() - timeout > 2000UL) { + Serial.println("##WEATHER###: client available timeout !"); + client.stop(); + return false; + } + } + timeout = millis(); + String line = ""; + if (client.connected()) { + while (client.available()) + { + line = client.readStringUntil('\n'); + if (strstr(line.c_str(), "\"temp\"") != NULL) { + client.stop(); + break; + } + if ((millis() - timeout) > 500) + { + client.stop(); + Serial.println("##WEATHER###: client read timeout !"); + return false; + } + } + } + if (strstr(line.c_str(), "\"temp\"") == NULL) { + Serial.println("##WEATHER###: weather not found !"); + return false; + } + char *tmpe; + char *tmps; + char *tmpc; + const char* cursor = line.c_str(); + char desc[120], temp[20], hum[20], press[20], icon[5]; + + tmps = strstr(cursor, "\"description\":\""); + if (tmps == NULL) { Serial.println("##WEATHER###: description not found !"); return false;} + tmps += 15; + tmpe = strstr(tmps, "\",\""); + if (tmpe == NULL) { Serial.println("##WEATHER###: description not found !"); return false;} + strlcpy(desc, tmps, tmpe - tmps + 1); + cursor = tmpe + 2; + + // "ясно","icon":"01d"}], + tmps = strstr(cursor, "\"icon\":\""); + if (tmps == NULL) { Serial.println("##WEATHER###: icon not found !"); return false;} + tmps += 8; + tmpe = strstr(tmps, "\"}"); + if (tmpe == NULL) { Serial.println("##WEATHER###: icon not found !"); return false;} + strlcpy(icon, tmps, tmpe - tmps + 1); + cursor = tmpe + 2; + + tmps = strstr(cursor, "\"temp\":"); + if (tmps == NULL) { Serial.println("##WEATHER###: temp not found !"); return false;} + tmps += 7; + tmpe = strstr(tmps, ",\""); + if (tmpe == NULL) { Serial.println("##WEATHER###: temp not found !"); return false;} + strlcpy(temp, tmps, tmpe - tmps + 1); + cursor = tmpe + 1; + float tempf = atof(temp); + + tmps = strstr(cursor, "\"feels_like\":"); + if (tmps == NULL) { Serial.println("##WEATHER###: feels_like not found !"); return false;} + tmps += 13; + tmpe = strstr(tmps, ",\""); + if (tmpe == NULL) { Serial.println("##WEATHER###: feels_like not found !"); return false;} + strlcpy(temp, tmps, tmpe - tmps + 1); + cursor = tmpe + 2; + float tempfl = atof(temp); (void)tempfl; + + tmps = strstr(cursor, "\"pressure\":"); + if (tmps == NULL) { Serial.println("##WEATHER###: pressure not found !"); return false;} + tmps += 11; + tmpe = strstr(tmps, ",\""); + if (tmpe == NULL) { Serial.println("##WEATHER###: pressure not found !"); return false;} + strlcpy(press, tmps, tmpe - tmps + 1); + cursor = tmpe + 2; + int pressi = (float)atoi(press) / 1.333; + + tmps = strstr(cursor, "humidity\":"); + if (tmps == NULL) { Serial.println("##WEATHER###: humidity not found !"); return false;} + tmps += 10; + tmpe = strstr(tmps, ",\""); + tmpc = strstr(tmps, "}"); + if (tmpe == NULL) { Serial.println("##WEATHER###: humidity not found !"); return false;} + strlcpy(hum, tmps, tmpe - tmps + (tmpc>tmpe?1:0)); + + tmps = strstr(cursor, "\"grnd_level\":"); + bool grnd_level_pr = (tmps != NULL); + if(grnd_level_pr){ + tmps += 13; + tmpe = strstr(tmps, ",\""); + if (tmpe == NULL) { Serial.println("##WEATHER###: grnd_level not found !"); return false;} + strlcpy(press, tmps, tmpe - tmps + 1); + cursor = tmpe + 2; + pressi = (float)atoi(press) / 1.333; + } + + tmps = strstr(cursor, "\"speed\":"); + if (tmps == NULL) { Serial.println("##WEATHER###: wind speed not found !"); return false;} + tmps += 8; + tmpe = strstr(tmps, ",\""); + if (tmpe == NULL) { Serial.println("##WEATHER###: wind speed not found !"); return false;} + strlcpy(temp, tmps, tmpe - tmps + 1); + cursor = tmpe + 1; + float wind_speed = atof(temp); (void)wind_speed; + + tmps = strstr(cursor, "\"deg\":"); + if (tmps == NULL) { Serial.println("##WEATHER###: wind deg not found !"); return false;} + tmps += 6; + tmpe = strstr(tmps, ",\""); + if (tmpe == NULL) { Serial.println("##WEATHER###: wind deg not found !"); return false;} + strlcpy(temp, tmps, tmpe - tmps + 1); + cursor = tmpe + 1; + int wind_deg = atof(temp)/22.5; + if(wind_deg<0) wind_deg = 16+wind_deg; + + + #ifdef USE_NEXTION + nextion.putcmdf("press_txt.txt=\"%dmm\"", pressi); + nextion.putcmdf("hum_txt.txt=\"%d%%\"", atoi(hum)); + char cmd[30]; + snprintf(cmd, sizeof(cmd)-1,"temp_txt.txt=\"%.1f\"", tempf); + nextion.putcmd(cmd); + int iconofset; + if(strstr(icon,"01")!=NULL) iconofset = 0; + else if(strstr(icon,"02")!=NULL) iconofset = 1; + else if(strstr(icon,"03")!=NULL) iconofset = 2; + else if(strstr(icon,"04")!=NULL) iconofset = 3; + else if(strstr(icon,"09")!=NULL) iconofset = 4; + else if(strstr(icon,"10")!=NULL) iconofset = 5; + else if(strstr(icon,"11")!=NULL) iconofset = 6; + else if(strstr(icon,"13")!=NULL) iconofset = 7; + else if(strstr(icon,"50")!=NULL) iconofset = 8; + else iconofset = 9; + nextion.putcmd("cond_img.pic", 50+iconofset); + nextion.weatherVisible(1); + #endif + + Serial.printf("##WEATHER###: description: %s, temp:%.1f C, pressure:%dmmHg, humidity:%s%%\n", desc, tempf, pressi, hum); + #ifdef WEATHER_FMT_SHORT + sprintf(wstr, weatherFmt, tempf, pressi, hum); + #else + #if EXT_WEATHER + sprintf(wstr, weatherFmt, desc, tempf, tempfl, pressi, hum, wind_speed, wind[wind_deg]); + #else + sprintf(wstr, weatherFmt, desc, tempf, pressi, hum); + #endif + #endif + display.putRequest(NEWWEATHER); + return true; +#endif // if (DSP_MODEL!=DSP_DUMMY || defined(USE_NEXTION)) && !defined(HIDE_WEATHER) + return false; +} + +//****************** diff --git a/yoRadio/src/core/timekeeper.h b/yoRadio/src/core/timekeeper.h new file mode 100644 index 0000000..6f5aefc --- /dev/null +++ b/yoRadio/src/core/timekeeper.h @@ -0,0 +1,42 @@ +#ifndef timekeeper_h +#define timekeeper_h +#include "Arduino.h" + +void _syncTask(void * pvParameters); +bool _getWeather(char *wstr); + +class TimeKeeper { + public: + volatile bool forceWeather; + volatile bool forceTimeSync; + volatile bool busy; + public: + TimeKeeper() { + busy = false; + forceWeather = true; + forceTimeSync = true; + _returnPlayerTime = _doAfterTime = 0; + } + bool loop0(); + bool loop1(); + void timeTask(); + void weatherTask(); + void waitAndReturnPlayer(uint8_t time_s); + void waitAndDo(uint8_t time_s, void (*callback)()); + private: + uint32_t _returnPlayerTime, _doAfterTime; + void (*_aftercallback)(); + void (*_watchdogcallback)(); + void _upRSSI(); + void _upSDPos(); + void _upClock(); + void _upScreensaver(); + void _returnPlayer(); + void _doAfterWait(); + void _doWatchDog(); + +}; + +extern TimeKeeper timekeeper; + +#endif diff --git a/yoRadio/src/displays/widgets/pages.cpp b/yoRadio/src/displays/widgets/pages.cpp index 61f0bc8..d056cb7 100644 --- a/yoRadio/src/displays/widgets/pages.cpp +++ b/yoRadio/src/displays/widgets/pages.cpp @@ -13,7 +13,7 @@ void Pager::loop(){ } Page& Pager::addPage(Page* page, bool setNow){ - _pages.add(page); + _pages.push_back(page); if(setNow) setPage(page); return *page; } @@ -21,7 +21,15 @@ Page& Pager::addPage(Page* page, bool setNow){ bool Pager::removePage(Page* page){ page->setActive(false); dsp.clearDsp(); - return _pages.remove(page); + auto i = std::find_if(_pages.begin(), _pages.end(), [&page](const Page* pn){ return page == pn; }); + if (i != _pages.end()){ + delete (*i); + (*i) = nullptr; + _pages.erase(i); + return true; + } + return false; + //return _pages.remove(page); } void Pager::setPage(Page* page, bool black){ @@ -33,12 +41,13 @@ void Pager::setPage(Page* page, bool black){ /*******************************************************/ -Page::Page() : _widgets(LinkedList([](Widget * wd) { delete wd;})), _pages(LinkedList([](Page* pg){ delete pg; })) { - _active = false; -} +//Page::Page() : _widgets(LinkedList([](Widget * wd) { delete wd;})), _pages(LinkedList([](Page* pg){ delete pg; })) { +// _active = false; +//} Page::~Page() { for (const auto& w : _widgets) removeWidget(w); + // what about deleting _pages ??? } void Page::loop() { @@ -46,23 +55,40 @@ void Page::loop() { } Widget& Page::addWidget(Widget* widget) { - _widgets.add(widget); + _widgets.push_back(widget); widget->setActive(_active, _active); return *widget; } bool Page::removeWidget(Widget* widget){ widget->setActive(false, _active); - return _widgets.remove(widget); + auto i = std::find_if(_widgets.begin(), _widgets.end(), [&widget](const Widget* wn){ return widget == wn; }); + if (i != _widgets.end()){ + delete (*i); + (*i) = nullptr; + _widgets.erase(i); + return true; + } + return false; + + //return _widgets.remove(widget); } Page& Page::addPage(Page* page){ - _pages.add(page); + _pages.push_back(page); return *page; } bool Page::removePage(Page* page){ - return _pages.remove(page); + auto i = std::find_if(_pages.begin(), _pages.end(), [&page](const Page* pn){ return page == pn; }); + if (i != _pages.end()){ + delete (*i); + (*i) = nullptr; + _pages.erase(i); + return true; + } + return false; +// return _pages.remove(page); } void Page::setActive(bool act) { diff --git a/yoRadio/src/displays/widgets/pages.h b/yoRadio/src/displays/widgets/pages.h index d79ee85..f675462 100644 --- a/yoRadio/src/displays/widgets/pages.h +++ b/yoRadio/src/displays/widgets/pages.h @@ -1,16 +1,16 @@ #ifndef pages_h #define pages_h -#include "Arduino.h" -#include "../../AsyncWebServer/StringArray.h" +#include + class Page { protected: - LinkedList _widgets; - LinkedList _pages; + std::list _widgets; + std::list _pages; bool _active; public: - Page(); + //Page(); ~Page(); void loop(); Widget& addWidget(Widget* widget); @@ -23,14 +23,14 @@ class Page { class Pager{ public: - Pager() : _pages(LinkedList([](Page* pg){ delete pg; })) {} + //Pager() : _pages(std::list([](Page* pg){ delete pg; })) {} void begin(); void loop(); Page& addPage(Page* page, bool setNow = false); bool removePage(Page* page); void setPage(Page* page, bool black=false); private: - LinkedList _pages; + std::list _pages; }; diff --git a/yoRadio/src/displays/widgets/widgets.h b/yoRadio/src/displays/widgets/widgets.h index 897e4c7..9d05e5e 100644 --- a/yoRadio/src/displays/widgets/widgets.h +++ b/yoRadio/src/displays/widgets/widgets.h @@ -119,6 +119,7 @@ class TextWidget: public Widget { TextWidget() {} TextWidget(WidgetConfig wconf, uint16_t buffsize, bool uppercase, uint16_t fgcolor, uint16_t bgcolor) { init(wconf, buffsize, uppercase, fgcolor, bgcolor); } ~TextWidget(); + using Widget::init; void init(WidgetConfig wconf, uint16_t buffsize, bool uppercase, uint16_t fgcolor, uint16_t bgcolor); void setText(const char* txt); void setText(int val, const char *format); @@ -139,6 +140,7 @@ class FillWidget: public Widget { public: FillWidget() {} FillWidget(FillConfig conf, uint16_t bgcolor) { init(conf, bgcolor); } + using Widget::init; void init(FillConfig conf, uint16_t bgcolor); void setHeight(uint16_t newHeight); protected: @@ -151,6 +153,7 @@ class ScrollWidget: public TextWidget { ScrollWidget(){} ScrollWidget(const char* separator, ScrollConfig conf, uint16_t fgcolor, uint16_t bgcolor); ~ScrollWidget(); + using Widget::init; void init(const char* separator, ScrollConfig conf, uint16_t fgcolor, uint16_t bgcolor); void loop(); void setText(const char* txt); @@ -182,6 +185,7 @@ class SliderWidget: public Widget { SliderWidget(FillConfig conf, uint16_t fgcolor, uint16_t bgcolor, uint32_t maxval, uint16_t oucolor=0){ init(conf, fgcolor, bgcolor, maxval, oucolor); } + using Widget::init; void init(FillConfig conf, uint16_t fgcolor, uint16_t bgcolor, uint32_t maxval, uint16_t oucolor=0); void setValue(uint32_t val); protected: @@ -199,6 +203,7 @@ class VuWidget: public Widget { VuWidget() {} VuWidget(WidgetConfig wconf, VUBandsConfig bands, uint16_t vumaxcolor, uint16_t vumincolor, uint16_t bgcolor) { init(wconf, bands, vumaxcolor, vumincolor, bgcolor); } ~VuWidget(); + using Widget::init; void init(WidgetConfig wconf, VUBandsConfig bands, uint16_t vumaxcolor, uint16_t vumincolor, uint16_t bgcolor); void loop(); protected: @@ -213,6 +218,7 @@ class VuWidget: public Widget { class NumWidget: public TextWidget { public: + using Widget::init; void init(WidgetConfig wconf, uint16_t buffsize, bool uppercase, uint16_t fgcolor, uint16_t bgcolor); void setText(const char* txt); void setText(int val, const char *format); @@ -227,6 +233,7 @@ class ProgressWidget: public TextWidget { ProgressWidget(WidgetConfig conf, ProgressConfig pconf, uint16_t fgcolor, uint16_t bgcolor) { init(conf, pconf, fgcolor, bgcolor); } + using Widget::init; void init(WidgetConfig conf, ProgressConfig pconf, uint16_t fgcolor, uint16_t bgcolor){ TextWidget::init(conf, pconf.width, false, fgcolor, bgcolor); _speed = pconf.speed; _width = pconf.width; _barwidth = pconf.barwidth; @@ -254,6 +261,7 @@ class BitrateWidget: public Widget { BitrateWidget() {} BitrateWidget(BitrateConfig bconf, uint16_t fgcolor, uint16_t bgcolor) { init(bconf, fgcolor, bgcolor); } ~BitrateWidget(){} + using Widget::init; void init(BitrateConfig bconf, uint16_t fgcolor, uint16_t bgcolor); void setBitrate(uint16_t bitrate); void setFormat(BitrateFormat format); diff --git a/yoRadio/src/main.cpp b/yoRadio/src/main.cpp index 70e5345..6a39482 100644 --- a/yoRadio/src/main.cpp +++ b/yoRadio/src/main.cpp @@ -9,6 +9,16 @@ #include "core/controls.h" #include "core/mqtt.h" #include "core/optionschecker.h" +#include "core/timekeeper.h" + +#if USE_OTA +#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) +#include +#else +#include +#endif +#include +#endif #if DSP_HSPI || TS_HSPI || VS_HSPI SPIClass SPI2(HOOPSENb); @@ -16,6 +26,44 @@ SPIClass SPI2(HOOPSENb); extern __attribute__((weak)) void yoradio_on_setup(); +#if USE_OTA +void setupOTA(){ + if(strlen(config.store.mdnsname)>0) + ArduinoOTA.setHostname(config.store.mdnsname); +#ifdef OTA_PASS + ArduinoOTA.setPassword(OTA_PASS); +#endif + ArduinoOTA + .onStart([]() { + player.sendCommand({PR_STOP, 0}); + display.putRequest(NEWMODE, UPDATING); + telnet.printf("Start OTA updating %s\n", ArduinoOTA.getCommand() == U_FLASH?"firmware":"filesystem"); + }) + .onEnd([]() { + telnet.printf("\nEnd OTA update, Rebooting...\n"); + ESP.restart(); + }) + .onProgress([](unsigned int progress, unsigned int total) { + telnet.printf("Progress OTA: %u%%\r", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + telnet.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) { + telnet.printf("Auth Failed\n"); + } else if (error == OTA_BEGIN_ERROR) { + telnet.printf("Begin Failed\n"); + } else if (error == OTA_CONNECT_ERROR) { + telnet.printf("Connect Failed\n"); + } else if (error == OTA_RECEIVE_ERROR) { + telnet.printf("Receive Failed\n"); + } else if (error == OTA_END_ERROR) { + telnet.printf("End Failed\n"); + } + }); + ArduinoOTA.begin(); +} +#endif + void setup() { Serial.begin(115200); if(REAL_LEDBUILTIN!=255) pinMode(REAL_LEDBUILTIN, OUTPUT); @@ -45,23 +93,29 @@ void setup() { #ifdef MQTT_ROOT_TOPIC mqttInit(); #endif + #if USE_OTA + setupOTA(); + #endif if (config.getMode()==PM_SDCARD) player.initHeaders(config.station.url); player.lockOutput=false; if (config.store.smartstart == 1) { - delay(99); + delay(250); player.sendCommand({PR_PLAY, config.lastStation()}); } pm.on_end_setup(); } void loop() { + timekeeper.loop1(); telnet.loop(); if (network.status == CONNECTED || network.status==SDREADY) { player.loop(); - //loopControls(); +#if USE_OTA + ArduinoOTA.handle(); +#endif } loopControls(); - netserver.loop(); + //netserver.loop(); } #include "core/audiohandlers.h"